图片PDF向量化处理的探索与实践

在构建AI本地知识库时,我们不可避免地需要对PDF文件进行处理。PDF文件大致分为两种:扫描的图片文件和非图片文件。对于非图片类型的PDF,可以直接提取文本并进行向量化处理;但对于图片类型的PDF(如扫描件),处理起来就复杂得多。


问题背景

图片类型的PDF文件通常存在以下问题:

  • 扫描件可能不是一页一页扫描的,而是多页合并在一起。
  • 文字方向多样,可能包含横排、竖排甚至不同语言的文字。
  • 不同段落的文字容易被错误合并成一行,导致内容混乱。

幸运的是,借助现代AI技术,我们可以高效地解决这些问题。


处理流程

以下是图片PDF向量化处理的整体流程:

graph TD A[PDF输入] --> B[文本检测+方向分类+文本识别] B --> D[Ai整理合并] D --> E[小模型Ai校对] E --校对失败--> D E --校对成功--> F[Ai输出清理] F --> G[小模型校验] G --校验失败--> F G --校验成功--> H[输出整理完成的文档] H --> I[向量化]
  1. PDF输入
    将PDF文件作为输入源。

  2. 文本检测与方向分类
    使用OCR技术检测文字区域,并识别文字方向。提取文字内容,生成初步的文本数据。

  3. AI整理与合并
    使用大模型对提取的文本进行整理和合并,确保段落结构正确。

  4. 小模型校对
    使用小模型对整理后的文本进行校对。如果校对失败,则重新执行AI整理与合并步骤。

  5. AI输出清理
    对文本进行进一步清理,去除冗余信息。

  6. 小模型校验
    再次使用小模型对清理后的文本进行校验。如果校验失败,则重新执行清理步骤。

  7. 输出整理完成的文档
    将最终整理好的文档输出。

  8. 向量化
    将整理完成的文档转化为向量形式,用于后续的知识库构建。


技术选型

1. 文本检测与识别

经过多次测试,发现百度飞桨(PaddlePaddle)的PP-OCR模型效果最佳,能够很好地识别繁体中文和其他复杂字符。相比一些收费的OCR工具,PP-OCR的表现更加出色。

2. AI整理与合并

为了确保文本整理的准确性,我选择了通义千问的Qwen2.5-72B-Instruct模型。该模型具有强大的指令遵循能力,并且通过将temperature设置为0,可以严格遵循输入文档的结构,避免不必要的改动。

3. 校对与校验

在校对环节,我使用了DeepSeek-R1:7B模型。经过测试,即使是较小的DeepSeek-R1:1.5B模型也能满足需求,但为了保险起见,最终选择了7B版本。


示例代码

以下是关键步骤的部分代码示例:

文本检测与识别

from paddleocr import PaddleOCR

# OCR核心配置
ocr = PaddleOCR(
    use_angle_cls=True,  # 自动旋转文本方向
    lang="ch",           # 支持繁体识别
    det_model_dir='ch_PP-OCRv4_det_infer',  # 文本检测模型
    rec_model_dir='ch_PP-OCRv4_rec_infer',  # 文本识别模型
    gpu_mem=4000        # 控制显存占用
)

AI整理与合并


def merge_text_with_ai(blocks_metadata, page_size, model_name=MERGE_MODEL):
    """智能版面分析及多语言文本合并"""
    system_prompt = """你是一个专业的OCR文本处理专家,擅长分析和重组OCR识别的文本块。
你的任务是将散乱的文本块重新组织成有序、连贯的文章。
只能输出重组后的纯文本内容,不要解释处理过程。"""

    user_prompt = """# OCR文本重组任务
【输入信息】
页面尺寸:{width}x{height}
文本块数据:
{blocks_metadata}

【重组规则】
1. 文本分析
   - 识别段落关系:通过内容连贯性和位置关系
   - 重排段落:基于语义和排版位置

2. 内容处理
   - 合并断行:修复被错误分割的句子
   - 保持段落完整性

3. 质量要求
   - 保持语义连贯性
   - 避免重复内容
   - 维护完整段落结构
   - 保证内容完整不遗漏"""

    # 初始化merged变量
    merged = ""
    original_text = '\n'.join(block['text'] for block in blocks_metadata)
    
    for attempt in range(MAX_RETRIES):
        try:
            print("开始合并文本...")
            response = client_sf.chat.completions.create(
                model=model_name,
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt.format(
                        width=page_size[0],
                        height=page_size[1],
                        blocks_metadata=blocks_metadata  # 修正变量名
                    )}
                ],
                temperature=0,
                timeout=MODEL_TIMEOUT
            )
            merged = response.choices[0].message.content.strip()
            
            # 后处理流程
            merged = re.sub(r'<think>.*?</think>', '', merged, flags=re.DOTALL)
            merged = re.sub(r'[※★◆▁▂▃▄▅▆▇█▏▎▍▌▋▊▉]+', '', merged)
            merged = re.sub(r'[,,]+', ',', merged)
            merged = re.sub(r'\n{3,}', '\n\n', merged)
            
            if validate_output(original_text, merged):
                print("校验通过,返回合并结果")
                return merged
            
            print(f"校验未通过,开始第{attempt+1}次重试...")
            
        except Exception as e:
            if 'timeout' in str(e).lower():
                logging.error(f"AI响应超时({MODEL_TIMEOUT}秒),尝试第{attempt+1}次重试...")
            else:
                logging.error(f"合并失败: {str(e)}")
            
            if attempt == MAX_RETRIES - 1:
                break
            time.sleep(RETRY_BASE_DELAY ** attempt)
    
    return original_text  # 如果所有尝试都失败,返回原始文本

小模型校对


def validate_output(original_text, processed_text):
    """严格单结果校验"""
    system_prompt = """你是一个严格的文本质量检查员。
你的任务是对比原始文本和处理后的文本,检查是否存在内容缺失、非法删除或重复问题。
只需回答"是"或"否",不需要解释原因。"""

    user_prompt = """请对比原文和处理后文本,严格检查:
1. 内容缺失:处理后文本是否删除了原文中的句子
2. 非法删除:是否错误删除正文内容
3. 重复问题:是否产生了原文中不存在的重复内容

▼原始文本▼
{original_sample}

▼处理后文本▼
{processed_sample}

如果存在任何一个问题,请回答[否]
如果全部符合要求,请回答[是]
仅允许输出[是]或[否]"""

    try:
        print("开始校验...")
        print("原始文本:",original_text.replace('\n', ' '))
        print("处理后文本:",processed_text.replace('\n', ' '))
        response = client_local.chat.completions.create(
            model=VALIDATE_MODEL,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt.format(
                    original_sample=original_text[:300].replace('\n', ' '),
                    processed_sample=processed_text[:500].replace('\n', ' ')
                )}
            ],
            temperature=0,
            timeout=VALIDATE_TIMEOUT
        )
        # 严格清洗响应
        print("校验结果:",response.choices[0].message.content)
        clean_res = re.sub(r'<think>.*?</think>', '', response.choices[0].message.content, flags=re.DOTALL)
        clean_res = re.sub(r'[^是否]', '', clean_res)
        return '否' not in clean_res
    except Exception as e:
        logging.error(f"校验失败: {str(e)}")
        return False

数据清理

def clean_text_with_ai(text_chunk, model_name=CLEAN_MODEL):
    """基于上下文优化OCR文本的专业清理"""
    system_prompt = """你是一个专业的OCR文本清理专家。
你的任务是优化和清理OCR识别的文本,修正错误并提高文本质量。
直接输出清理后的纯文本,不要解释,不要道歉,不要表明你是AI。"""

    user_prompt = """请处理OCR识别文本:
    
【页面特征】
1. 可能存在:竖排文字、印章标记、装订线痕迹
2. 典型干扰:水渍斑点、页码标记、装订孔洞
3. 专业术语:命理学术语(如"正官格"、"伤官见官")

【处理规则】
1. 删除:
   - 页码标记(如"- 5 -", "Page 12")
   - 版权信息(ISBN/CIP/条形码)
   - 装订线痕迹/扫描伪影
   - 空白行和多余空格
2. 修正:
   - 排版错误(合并错误换行的句子)
   - 形近字错误(己→已,未→末)
   - 排版错误(合并被错误换行的句子)
3. 保留:
   - 专业术语原貌
   - 重要标点(。!?「」)
4. 根据上下文修复残缺文本

【完整上下文】▼
{full_context}

【待优化段落】▼
{target_text}"""

    try:
        response = client_sf.chat.completions.create(
            model=model_name,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt.format(
                    full_context=text_chunk['context'],
                    target_text=text_chunk['target']
                )}
            ],
            temperature=0.2,
            timeout=MODEL_TIMEOUT
        )
        # 后处理流程
        cleaned = response.choices[0].message.content.strip()
        cleaned = re.sub(r'<think>.*?</think>', '', cleaned, flags=re.DOTALL)  # 移除think标签
        cleaned = re.sub(r'[※★◆]+', '', cleaned)  # 去除装饰符号
        return re.sub(r'\n{3,}', '\n\n', cleaned)  # 标准化换行
        
    except Exception as e:
        logging.warning(f"清理请求失败: {str(e)}")
        return text_chunk['target']  # 失败时返回原始文本


原始图像

数据合并后效果

posted @ 2025-02-15 11:25  鹄鹄  阅读(665)  评论(0)    收藏  举报