# 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 <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）
```

## 依赖

- **pdfplumber** — PDF 文字提取和表格检测
- **python-docx** — DOCX 文档生成
- **onnxruntime** — GPT-2 模型推理（可选，用于 NLP 文本优化）
- **tokenizers** — HuggingFace 分词器（可选）
