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 文本清洗与噪声过滤
解析后的原始文本常包含大量非条款内容,必须经过系统性清洗。清洗流程通常按以下步骤执行:
- 页眉页脚去除:基于位置信息(页面顶部/底部固定区域)或正则匹配(如“第X页 / 共Y页”、“机密”等模式)删除。
- 特殊字符过滤:移除控制字符、不可见Unicode、多余空格与制表符。
- 水印与批注剥离:识别固定透明度文本或括号内批注(如“[甲方留存]”)并剔除。
- 表格结构还原:将OCR识别的表格文本按行列重组,避免单元格内容断裂。
- 编码统一:强制转换为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→清洗→分割)支持独立开关与参数调整。这样在切换合同类型(如从采购合同切换到保密协议)时,只需调整配置而非修改代码。