# pdf_table_to_docx 从文字型 PDF 中提取表格并生成 DOCX 文件,保留合并单元格、处理跨页表格、利用 NLP 模型优化文本合理性。 ## 快速开始 ```bash # 安装依赖 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 每表独立) │ ``` ## 关键步骤详解 ### 1. 合并单元格检测 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 ``` ### 2. 跨页表格检测与合并 #### 两阶段判断 ``` 阶段一(严格匹配): 列数相同 + 列宽分布相似(|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 区域。 ### 3. 文字合理性(NLP 辅助清洗) 使用中文 GPT-2 模型(ONNX 格式,约 473MB)计算文本的**自然度评分**。 #### 评分原理 ```python 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/` - 模型来源:[uer/gpt2-chinese-cluecorpussmall](https://huggingface.co/uer/gpt2-chinese-cluecorpussmall) - 导出格式:ONNX(CPU 推理,单次约 21ms) - 评分特性:BERT WordPiece 分词器,会对空白字符做了标准化处理 如未下载模型,不影响表格提取和 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 └── ... ``` ### JSON 元数据示例 ```json { "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": ["评审内容", "评审因素", "类型", "评审标准", "分值"] } ] } ``` ## CLI 参考 ``` python -m pdf_table_to_docx [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) ``` ## 依赖 - **pdfplumber** — PDF 文字提取和表格检测 - **python-docx** — DOCX 文档生成 - **onnxruntime** — GPT-2 模型推理(可选,用于 NLP 文本优化) - **tokenizers** — HuggingFace 分词器(可选)