企业文档处理绕不开 PDF 表格。参数表、报价单、检测报告、对账单,多数以 PDF 形式留存,而下游系统需要的是结构化字段。这篇文章把实际用过的三条路线摊开对比:规则/文本坐标法、PDF 库解析法、版面识别法,并给出选型判据与可运行的最小实现。
路线一:文本坐标法
思路最朴素:把每页文本连同坐标取出来,按 y 坐标聚类成行、按 x 坐标切列。
优点是可控、可解释、不依赖第三方模型;缺点是对合并单元格、跨页表格、无表线的三线表都不友好。适合格式高度固定的场景,比如自己系统生成的报表。
python
import fitz # PyMuPDF
def cells_by_position(page, y_tol=3.0, x_gap=8.0):
"""按坐标聚类:先按 y 分行,再按 x 间距切列"""
words = page.get_text("words") # (x0,y0,x1,y1,word,...)
words.sort(key=lambda w: (round(w[1]), w[0]))
rows, cur, cur_y = [], [], None
for w in words:
if cur_y is None or abs(w[1] - cur_y) <= y_tol:
cur.append(w)
cur_y = w[1] if cur_y is None else cur_y
else:
rows.append(cur)
cur, cur_y = [w], w[1]
if cur:
rows.append(cur)
table = []
for r in sorted(rows, key=lambda r: r[0][1]):
r = sorted(r, key=lambda w: w[0])
line, cell, last_x1 = [], "", None
for w in r:
if last_x1 is not None and w[0] - last_x1 > x_gap:
line.append(cell.strip())
cell = ""
cell += w[4]
last_x1 = w[2]
line.append(cell.strip())
table.append(line)
return table

> 阈值 y_tol、x_gap 必须按实际文档校准,这也是这条路线的成本所在:换一种模板就要重调一次。
路线二:PDF 库解析法
主流库各自封装了表格检测:
| 库 | 机制 | 适用 | 代价 |
|---|---|---|---|
| pdfplumber | 基于线条与文本对齐推测表格 | 有表线的规则表格 | 速度中等,依赖 camelot 系依赖较重 |
| PyMuPDF 的 find_tables() | 版面 + 线条联合判定 | 常见报表,速度快 | 复杂合并单元格易错 |
| tabula(Java) | 同类思路,规则驱动 | 批量离线处理 | 需 JVM |
python
import pdfplumber
def tables_pdfplumber(path, page_no=0):
with pdfplumber.open(path) as pdf:
page = pdf.pages[page_no]
out = []
for tb in page.extract_tables():
out.append([[ (c or "").replace("\n", " ").strip() for c in row] for row in tb])
return out

经验值:有框线的表格,这条路线的准确率通常在 90% 以上;无框线的三线表掉到 60% 附近,需要回退到路线一或三。
路线三:版面识别法
把页面渲染成图,用检测模型(表格结构识别)出框线与单元格,再做 OCR。这条路线的优势是无框线、扫描件、倾斜拍摄都能处理;代价是引入模型与算力,且需要后处理纠错。
落地时常见的折中方案是分层触发:先跑路线二,对识别结果做质量校验(行列数是否为空、单元格是否出现明显错位),不合格的页面再送路线三。这样在混合来源的文档集上,既控制了算力,又保住了召回。
关键的校验环节
无论走哪条路线,入库前都应过一遍这几项检查,它们比提高识别率更能减少下游事故:

-
行列一致性 :同一列的数值量级不应出现突跳(如 12、15、120000);
-
表头对齐 :识别到的表头字段是否与目标 schema 能一一映射;
-
单位与量纲 :抽出的数字必须带上单位字段,否则后续无法比较;
-
跨页拼接:检测页面底部是否存在延续表,按表头是否重复判断续页。
python
def sanity_check(rows, expected_cols):
issues = []
if not rows:
return ["空表"]
widths = {len(r) for r in rows}
if widths != {expected_cols}:
issues.append(f"列数不一致: {sorted(widths)}")
for i, r in enumerate(rows):
if all((c or "").strip() == "" for c in r):
issues.append(f"第 {i} 行为空")
return issues
选型判据
给一个简化的决策表:
· 文档由本系统生成、格式稳定 → 路线一,最省依赖;
· 文档来自外部、多数有框线 → 路线二,性价比最高;
· 存在扫描件、拍照件、无框线表格 → 路线三,或路线二 + 三分层;
· 只做一次、量很小 → 人工核对反而更快,别过度工程化。
小结
三条路线没有绝对优劣,只有与文档来源是否匹配。实践中收益最大的步骤,往往不是换更贵的模型,而是先对文档来源分层:哪些是自己生成的、哪些是外部来的、哪些是扫描的,然后各走各的通道,最后统一做质量校验。
顺带一提,抽取结果的可用性还取决于源文档的一致程度。同一份参数在正文写"≥800MPa"、在附件写"800MPa 级"时,无论识别多准,下游拿到的都是两个不同的值。识别技术解决"读得到",一致性问题只能靠源头的口径统一来解决。
(本文代码基于 Python 3.10、PyMuPDF 与 pdfplumber 实测,可直接运行。)