|
|
4 недель назад | |
|---|---|---|
| .. | ||
| README.md | 4 недель назад | |
| __init__.py | 4 недель назад | |
| __main__.py | 4 недель назад | |
| cli.py | 4 недель назад | |
| docx_writer.py | 4 недель назад | |
| extractor.py | 4 недель назад | |
| table_parser.py | 4 недель назад | |
| text_scorer.py | 4 недель назад | |
从文字型 PDF 中提取表格并生成 DOCX 文件,保留合并单元格、处理跨页表格、利用 NLP 模型优化文本合理性。
# 安装依赖
pip install pdfplumber python-docx onnxruntime tokenizers
# 单 DOCX 模式(所有表格依次排列)
python -m pdf_table_to_docx document.pdf
# 目录模式(JSON 元数据 + 每表独立 DOCX)
python -m pdf_table_to_docx document.pdf --dir
# 指定输出
python -m pdf_table_to_docx document.pdf --dir -o output_dir/
PDF 输入
│
├── 1. 逐页扫描 ─────────────────────────────────────────┐
│ │ pdfplumber.find_tables() │
│ ▼ │
│ 提取表格网格 grid[r][c] │
│ (每个单元格 = bbox 四元组 or None) │
│ │
├── 2. 合并单元格检测 ─────────────────────────────────────┤
│ │ │
│ ├─ 扫描连续的 None → 初测跨行/跨列跨度 │
│ ├─ bbox 验证 → 消除假阳性纵向合并 │
│ │ (单元格底部必须延伸到目标行底部) │
│ └─ T 形冲突解决 → 按 row-major 顺序认领区域 │
│ │
├── 3. 文本清洗 ───────────────────────────────────────────┤
│ │ │
│ └─ NLP 辅助换行符清理 │
│ 规则 + GPT-2 评分对比 → 去包装换行,留结构换行 │
│ │
├── 4. 跨页表格合并 ───────────────────────────────────────┤
│ │ │
│ ├─ 阶段一(严格匹配):列数+列宽+页边距+表头比对 │
│ └─ 阶段二(宽松匹配):列数+页边距+连续性信号 │
│ ├─ 空首列延续 │
│ ├─ NLP 文本延续(GPT-2 评分拼接提升) │
│ └─ 纵向合并语义断层修复 │
│ │
├── 5. DOCX 生成 ──────────────────────────────────────────┤
│ │ │
│ ├─ python-docx 重建表格网格 │
│ ├─ gridSpan / vMerge 写入 XML │
│ ├─ 列宽比例、字体、对齐、边框 │
│ └─ JSON 元数据(页号、跨页标记、行列数、预览) │
│ │
└── DOCX 输出 │
(单文件 or 每表独立) │
PDF 没有"合并单元格"的原生概念。pdfplumber 将表格解析为网格 grid[r][c],其中被合并覆盖的网格位置为 None。
三步检测法:
原始网格:
[0,0]=CellA [0,1]=None [0,2]=CellB [0,3]=None
[1,0]=None [1,1]=CellC [1,2]=None [1,3]=CellD
第一步:扫描连续的 None
CellA 在 [0,0]→ 右扫发现 [0,1]=None → col_span=2
→ 下扫发现 [1,0]=None → row_span=2
CellA 实际合并范围 = 2×2(覆盖 [0,0]~[1,1])
第二步:bbox 验证纵向合并
对 row_span>1 的候选,检查单元格底部是否 ≥ 目标行底部
防止「合计」行跨越导致的假阳性合并
第三步:T 形冲突处理
按 row-major 顺序认领区域,冲突时自动缩减
如纵向合并先认领了 [17,2],横向合并到 [17,0] 时
检测到冲突 → 缩减至 col_span=1
阶段一(严格匹配):
列数相同 + 列宽分布相似(|w1-w2|<10%)→ 肯定是跨页
阶段二(宽松匹配)—— 三个连续性信号:
信号 A(空首列延续):
表格 4 末行: ['分项服务方案', ..., '4']
表格 5 首行: ['' , '节能管理', ..., '4']
↑ 首列为空 → t4 的分类延续到 t5
信号 B(NLP 文本延续):
「资源投入」(0.013) + 「和管理」(0.005)
→ 拼接「资源投入和管理」(0.056) → 提升 4.3 倍 → 是延续
信号 C(全表空首列):
t5 前 5 行首列全部为空 → 续表模式
跨页合并后,纵向合并组在分页处断裂,导致上一页的分类标签无法覆盖下一页:
合并前:
行4-7: [分项服务方案, ...] ← page 23
行8-12: ['' , ...] ← page 24(空标签)
修复后:
行4-12: [分项服务方案, ...] ← 扩展合并覆盖
实现方式:合并后扫描各列,若 t1 末行有纵向合并且延伸到 t1 底部、t2 对应位置为空,则扩展 t1 的 row_span 覆盖到 t2 区域。
使用中文 GPT-2 模型(ONNX 格式,约 473MB)计算文本的自然度评分。
score(text) = 1 / (1 + PPL / 10)
其中 PPL = exp(mean(loss)),loss 为模型对每个 token 的交叉熵。
当列宽不匹配时,用 NLP 判断两段文本是否应该拼接:
| 文本片段 | 单独评分 | 拼接评分 | 提升 | 结论 |
|---|---|---|---|---|
| 「资源投入」 | 0.013 | 0.056 | 4.3× | 应拼接 |
| 「和管理」 | 0.005 | |||
| 「采购」 | 0.071 | 0.088 | 1.25× | 应拼接 |
| 「需求。」 | 0.003 |
PDF 表格中常因列宽不足产生包装换行。混合规则+NLP 判断:
规则系统 → 4 级判断
1. 单行 ≤1 字符 → 包装换行,删
2. 括号分裂 "(万\n元)" → 删
3. 分值模式 "文字\n数字" → 保留
4. NLP 评分提升 > 1.2× → 删(合并后更自然)
5. 默认 → 保留(保守策略)
示例结果:
| 原文 | 处理后 | 原因 |
|---|---|---|
| ★采购预算金额(万\n元) | ★采购预算金额(万元) | 括号分裂 |
| 内容\n部门 | 内容\n部门 | 两行标题,保留 |
| 绿化整洁\n20 | 绿化整洁\n20 | 分值模式,保留 |
| 被考核\n单位意\n见 | 被考核单位意见 | NLP 提升显著 |
| 会场清洁卫\n生\n20 | 会场清洁卫生20 | 碎片合并 |
模型位置:models/gpt2-chinese-cluecorpussmall-onnx/
如未下载模型,不影响表格提取和 DOCX 生成功能,仅跳过 NLP 相关的文本优化步骤。
pdf_table_to_docx/
├── __init__.py # 包入口
├── __main__.py # python -m 调用
├── cli.py # 命令行界面(单文件/目录模式)
├── table_parser.py # 表格解析 + 合并单元格检测 + 换行清理
├── docx_writer.py # DOCX 文档生成
├── extractor.py # 主协调器 + 跨页检测合并
└── text_scorer.py # NLP 文本评分(GPT-2 ONNX)
output_dir/
├── tables.json # 全局元数据
│ ├── pdf_file # 源文件名
│ ├── total_tables # 表格总数
│ └── tables[] # 每个表格的详细信息
│ ├── id # 序号
│ ├── file # 对应 DOCX 路径
│ ├── pages # 所在页码(1-indexed)
│ ├── rows, cols # 行列数
│ ├── cross_page # 是否跨页
│ └── preview # 首行前 5 列预览
└── tables/
├── table_001.docx
├── table_002.docx
└── ...
{
"pdf_file": "招标文件.pdf",
"total_tables": 36,
"tables": [
{
"id": 1,
"file": "tables/table_001.docx",
"pages": [5],
"rows": 3,
"cols": 3,
"cross_page": false,
"preview": ["包件号", "包件名称", "★采购预算金额(万元)"]
},
{
"id": 4,
"file": "tables/table_004.docx",
"pages": [23, 24],
"rows": 26,
"cols": 5,
"cross_page": true,
"preview": ["评审内容", "评审因素", "类型", "评审标准", "分值"]
}
]
}
python -m pdf_table_to_docx <pdf_path> [options]
选项:
-o, --output PATH 输出路径(单文件 .docx / 目录模式文件夹)
--dir, --directory 目录模式(JSON + 每表独立 DOCX)
-v, --verbose 详细日志
--page-width W 页面宽度 cm(默认 21.0 = A4)
--page-height H 页面高度 cm(默认 29.7 = A4)
--margin M 页边距 cm(默认 2.0)