2. 合同文本预处理:PDF/Word/扫描件解析、OCR技术选型、文本清洗与段落分割

在采购合同条款抽取流程中,文本预处理是决定后续规则匹配与深度学习模型效果的基础环节。真实业务场景中,合同来源格式多样(PDF、Word、扫描件),且常包含页眉页脚、水印、表格嵌套等噪声。本节将系统讲解从原始文档到结构化文本的完整预处理链路。

2.1 多格式文档解析策略

不同格式的合同需要采用差异化的解析工具。下表对比了主流格式的解析方案与适用场景:

文档格式 推荐工具/库 解析方式 优势 局限性
PDF(文本型) PyMuPDF (fitz)、pdfplumber 直接提取文本流与位置信息 保留段落顺序,支持坐标定位 对复杂表格支持较弱
PDF(扫描件) OCR引擎(Tesseract / PaddleOCR) 图像转文字 可处理非电子签章合同 依赖图像质量,速度较慢
Word (.docx) python-docx 解析XML结构 保留样式、表格、修订标记 对.doc旧版支持需转换
Word (.doc) LibreOffice / antiword 命令行转换后读取 兼容旧版格式 需安装外部程序
实战建议:对于混合格式的合同库,建议先通过文件魔数(Magic Number)判断真实格式,再调用对应解析器。避免仅依赖扩展名判断,防止“.pdf”文件实为扫描件的情况。

2.2 OCR技术选型与对比

扫描件合同(如传真件、盖章扫描版)必须经过OCR才能进入后续流程。当前主流方案包括开源引擎与云服务,选型需平衡识别精度、速度与部署成本。

OCR引擎 识别精度(中文) 推理速度 部署方式 典型场景
Tesseract 5 + chi_sim 中等(约85-90%) 快(CPU可用) 本地离线 清晰印刷体、无复杂排版
PaddleOCR (PP-OCRv4) 高(约93-96%) 较快(GPU加速更佳) 本地/容器 多字体、倾斜、模糊文档
百度/阿里云OCR 极高(约97%+) 依赖网络 云端API 高精度要求、合规审计
选型原则:在采购合同抽取场景中,推荐以PaddleOCR作为主力引擎。其端到端训练管线对合同常见的手写批注、印章遮挡、表格线干扰有较好的鲁棒性,且支持自定义微调以适应特定合同模板。

2.3 文本清洗与噪声过滤

解析后的原始文本常包含大量非条款内容,必须经过系统性清洗。清洗流程通常按以下步骤执行:

  1. 页眉页脚去除:基于位置信息(页面顶部/底部固定区域)或正则匹配(如“第X页 / 共Y页”、“机密”等模式)删除。
  2. 特殊字符过滤:移除控制字符、不可见Unicode、多余空格与制表符。
  3. 水印与批注剥离:识别固定透明度文本或括号内批注(如“[甲方留存]”)并剔除。
  4. 表格结构还原:将OCR识别的表格文本按行列重组,避免单元格内容断裂。
  5. 编码统一:强制转换为UTF-8,修复常见乱码(如“é” → “é”)。
import re
import unicodedata

def clean_contract_text(raw_text: str) -> str:
    # 1. 移除控制字符(保留换行和制表符)
    text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', raw_text)
    # 2. 统一换行符
    text = text.replace('\r\n', '\n').replace('\r', '\n')
    # 3. 去除页眉页脚模式(示例:匹配“第X页”)
    text = re.sub(r'第\s*\d+\s*页\s*[//]\s*共\s*\d+\s*页', '', text)
    # 4. 合并多余空行
    text = re.sub(r'\n{3,}', '\n\n', text)
    # 5. Unicode规范化
    text = unicodedata.normalize('NFKC', text)
    return text.strip()

2.4 段落分割与语义边界识别

清洗后的文本需要分割为语义完整的段落,作为后续条款抽取的基本单元。单纯依赖空行分割在合同中往往失效,因为合同条款常以编号或缩进作为段落标记。

下图展示了基于规则与统计特征结合的段落分割流程:

段落分割混合策略流程图 清洗后文本 规则引擎 编号模式 / 缩进 / 关键词 统计特征 空行间距 / 字体变化 / 缩进量 候选分割点融合 投票 / 置信度加权 后处理:合并过短段落 / 拆分过长块 结构化段落列表
常见陷阱:仅依赖空行分割时,合同中的“甲方:___________________”与“乙方:___________________”之间的空行可能导致错误分段。建议结合编号正则(如“第[一二三四五六七八九十]+条”、“\d+\.\d+”)作为强分割信号。

2.5 段落分割代码示例

以下代码实现了基于编号模式与空行间距的混合分割策略:

import re

def segment_contract(text: str, min_para_len: int = 20) -> list:
    # 强分割模式:条款编号
    clause_pattern = r'(?m)^\s*(第[一二三四五六七八九十百千]+条|第\d+条|\d+[\.、])\s+'
    # 先按编号分割
    segments = re.split(clause_pattern, text)
    # 重组:将编号与正文合并
    merged = []
    for i in range(0, len(segments)-1, 2):
        merged.append(segments[i] + segments[i+1])
    if len(segments) % 2 == 1:
        merged.append(segments[-1])
    # 过滤过短段落(通常为页眉残留)
    merged = [seg.strip() for seg in merged if len(seg.strip()) >= min_para_len]
    return merged

# 示例用法
sample = """第一条 合同标的
甲方同意购买,乙方同意出售以下货物...
第二条 付款方式
买方应在收到发票后30日内支付...
第三条 交货期限
卖方应于2025年6月1日前完成交货..."""
paragraphs = segment_contract(sample)
for idx, para in enumerate(paragraphs, 1):
    print(f"段落{idx}: {para[:30]}...")

2.6 预处理质量评估指标

为量化预处理效果,建议在标注数据集上计算以下指标:

指标名称 计算方式 目标值 说明
段落召回率 正确分割段落数 / 人工标注段落总数 > 95% 避免遗漏条款
段落精确率 正确分割段落数 / 算法输出段落总数 > 90% 避免过度分割
字符错误率 (CER) 编辑距离 / 总字符数 < 5% OCR识别质量
噪声残留率 残留噪声字符数 / 总字符数 < 1% 清洗效果
工程要点:预处理管线应设计为可配置的DAG(有向无环图),每个处理步骤(解析→OCR→清洗→分割)支持独立开关与参数调整。这样在切换合同类型(如从采购合同切换到保密协议)时,只需调整配置而非修改代码。