想象你接手了一座塞满旧食谱的档案馆。里面既有印刷成册、纸张发黄的菜谱活页(PDF),也有用打字机敲出来的电子稿(Word)。你想把每道菜里的“用料清单”表格都抄进手机 App,做成可搜索的菜品数据库——可那些表格有的有框线、有的模糊不清、有的“调味料”三个字横跨三行。你要做的,就是系统地“抄表”:先看懂表格长什么样,再选对抄写工具,处理跨行跨列的怪格子,最后把抄好的内容整齐录入数据库。这套流程,正是 RAG 文档处理中的“表格提取”。本文将从识别到入库,带你完整走一遍这条抄表流水线。
掌握表格提取,你就能让 RAG 系统真正“读懂”文档里的表格信息,而不是跳过或乱序输出,问答质量会得到肉眼可见的提升。
可视化演示
展示PDF/Word文档表格识别、工具对比与合并单元格处理的完整流程,支持模式切换与参数交互。
本节配有交互动效(进入视口后自动加载,约 3KB)。
表格识别与提取流程 规则法基于线条和坐标对齐检测 CV法利用形态学操作定位表格 Accuracy = TP / (TP + FN) 深度学习法如TableNet实现端到端检测
表格识别基础
走进档案馆的第一步,是学会辨认“什么是菜谱表格”。旧食谱里,用料清单总是被印刷成规规矩矩的格子:最上面一行写着“食材、用量、备注”,下面每一行对应一种食材。在文档处理领域,这种由行和列围成的二维网格就叫表格,围出每个小空间的格子叫单元格,最上面那行叫表头,表头之下每一行都是代表一种食材的数据记录。RAG 系统要检索表格,得先让程序知道“哪里是表格”。
有了定义还不够,真正的难点在识别。就像有的菜谱用明显线条画格子(有线表格),有的只用空格和缩进暗示结构(无线表格),还有的老菜谱是整页扫描的图片(图片型表格)。PDF 里文字还分“活字印刷”和“照片成像”两种底层形式,识别策略完全不同。所以动手提取前,先做分类:表格是否有边框、是否是扫描件、表头是否多级——这些信息直接决定用哪类工具。
为了量化“抄得完不完整”,可以用提取完整度来度量:把文档里真实存在的单元格总数当作分母,把工具成功提取出的单元格数当作分子。分数越接近 1,说明抄录得越齐全。
回到档案馆的场景:如果连哪些页有表格都分不清,后面的抄写工具再利索也白搭。表格识别基础,就是把“哪些是表格、什么类型、难点在哪”先摸清楚,这一步做对了,后面按图索骥就轻松得多。
- N_ok成功提取的单元格数:程序准确读出来的格子个数,相当于完整抄录到笔记本上的那些用料行。
- N_real真实总单元格数:原文档里实际存在的所有格子,包括没被读出来的部分。
- C提取完整度:两者相除得到比例,C 越接近 1 说明抄录越完整,越接近 0 说明漏得越多。
PDF提取工具
印刷版的菜谱活页(PDF)是档案馆里最难抄的部分。PDF 天然只记录每个文字块的位置,不告诉你哪几条线围成了一个表格。好在有专门的“抄写工具”:PyMuPDF 像一把轻快的剪刀,擅长快速剪下文字及其坐标,适合表格线条清晰、结构简单的页面;pdfplumber 像一把细齿梳,会沿着文字行去推断哪些字符处于同一行同一列,对带边框的表格效果很好;Camelot 则像一位耐心的拓印工,先用计算机视觉算法找出页面上的直线,再根据虚拟线条重建出完整网格,复杂表格也拿得下,但安装依赖较重、速度偏慢。
工具一多就面临选型。实践里的经验法则是:先看页面有没有明显的直线边框。有框线的优先试 pdfplumber,它轻量且对文字型 PDF 友好;无框线或框线残缺的,用 Camelot 靠线条重建网格往往更稳;如果只是想快速拿到文本块坐标自己拼表,则选 PyMuPDF。选错了工具,就像用剪刀去拓印,事倍功半。
选型之后如何判断工具抄得好不好?可以把工具输出与人工标注的标准答案做字符相似度比较。两段文本共同字符数的两倍,除以两段文本长度之和,得到 0 到 1 的相似度分数。
就像抄完菜谱后拿原稿抽查几个字,相似度帮助我们判断这个“抄写工具”到底准不准。把 pdfplumber、Camelot 分别在测试页上跑一遍相似度对比,选分数最高的那只笔,再正式开工抄全部表格。
- A 与 B两份待比较文本:一份是工具提取出的单元格文字,另一份是人工对照原文档录入的标准文字。
- |A∩B|共同字符数:两段文字中相同字符的个数,字符重叠越多则相似度越高。
- |A|+|B|字符总数:两段文字长度之和,作为归一化的分母,避免文本越长分数越虚高。
- ×2 后整体相除因为共同字符被两边重复计算了两遍,所以乘 2 把分数校正到 0~1 区间。
Word提取方法
比起印刷的 PDF,档案馆里的 Word 电子稿就“乖”多了。Word 文档本身就是电子数据,表格结构被完整保存在内部文档模型里,不需要用视觉去猜,直接伸手到文件里就能把表格原样掏出来。在 Python 里,python-docx 就是那只手——它把 Word 文档解析成内存对象,其中 doc.tables 保存了文档中所有表格。
拿到表格对象后,遍历便是顺理成章的事:先遍历 doc.tables 得到每个表格,再遍历 table.rows 得到每一行,最后遍历 row.cells 取出每个单元格的文本。这就像把电子稿里的“用料清单”逐行复制粘贴到新文档里,不需要 OCR,也不需要图像识别,准确率天然就高。
Word 表格还保留着丰富的样式信息:单元格的合并状态、行高列宽、底纹颜色、字体加粗,都能被读取。这些信息在“还原表格”时非常有用——比如渲染回 Markdown 或 HTML 时,可以把表头单元格设为加粗、把合并结构保留下来。抄写电子稿菜谱,连排版风格都可以一并带走。不过在读取时也要注意:合并单元格会让 row.cells 返回重复的对象,需要结合合并信息去重。
在档案馆的电子稿区域,抄表几乎不受“印刷模糊”的困扰。遇到跨行跨列的怪格子时,只要记住“把起点值广播到覆盖区域”这条规则,Word 表格就能被整整齐齐地搬进结构化清单里。
- M[i][j]单元格矩阵:把表格看作一个二维矩阵,i 是行号,j 是列号,M[i][j] 就是该格的值。
- i0合并起始行:纵向合并是从第 i0 行开始的,该行存有真正的文字值。
- span合并跨度:合并总共占了几行,被覆盖的行号范围是 i0 到 i0+span-1。
- 赋值箭头把起点行的值复制到合并覆盖的所有行,于是这些行在矩阵中都有同样的值。
合并单元格
旧菜谱里常出现这样的排版:一栏“调味料”的竖排文字,横跨了三行食材——酱油、糖、醋下面共用这一个分类名。这种跨行跨列的格子在文档术语里叫合并单元格,是表格提取中最容易翻车的地方。如果不处理,三行数据中只会有第一行有“调味料”,另外两行全空,下游检索时就会丢掉两条记录。
第一步是合并识别。在 Word 的底层 XML 里,合并单元格会有 rowspan 或 colspan 属性;而在 PDF 里没有这么明确的标识,只能看“某个格子是否同时与多个格子共享一条边框”来推断。识别出合并区域后,第二步做行列映射:记录合并区域的起始行列和跨度,把“哪些行、哪些列共享同一个值”建模清楚。最后一步是数据填充:把起始格的值复制到合并覆盖的所有逻辑位置,让每个格子都有值可读。
一个合并区域本质上是一个矩形:从起点 (r0, c0) 横向延伸到 c0+colspan-1、纵向延伸到 r0+rowspan-1,覆盖范围内的所有格子在逻辑上都等于起点格的值。
处理合并单元格就像给旧食谱的跨行分类补上重复标注:虽然印刷原稿只写了一次,但你在抄录本上每行都要写一遍,这样检索时每行食材都能对应到正确的分类,不会因为缺值而被漏掉。
- T[i][j]逻辑表格位置:i 和 j 表示展开后的行列坐标,每个位置最终都要有值。
- T[r0][c0]合并起点值:合并区域左上角那个真正有文字的格子,是数据的唯一来源。
- region(r0,c0)覆盖矩形:以起点为中心、按跨度扩展开的连续行列范围。
- 等号连接覆盖范围内的每个位置都引用起点值,相当于把调味料的名字复制到每一行。
转结构化数据
表格抄完,最后一步是把抄录清单录入手机 App 的菜品数据库。从通用表格到可供 RAG 检索的数据,中间要过三道关。第一关是表头提取:默认表格第一行是字段名,把“食材、用量、备注”这些列名逐一摘出来,必要时还要处理多级表头。第二关是行转记录:每一行数据都对应菜谱里的一种食材,变成一个独立的记录对象。第三关是字段映射:把第一行的表头当作键,把当前行的单元格当作值,凑成一个“字段名:内容”的配对集合。
完成这三关后,所有记录组合起来就是一个 JSON 数组。JSON 是 RAG 系统非常喜欢的输入格式——它既是纯文本,又保持了结构,放进向量检索或关键词检索都清晰可解析。相比把整页表格塞成一段话,结构化之后的每一“行”都能单独成为一条可检索的知识单元,召回质量会有质的提升。
把这三关理清,可以用公式表达“每一行变一条记录”:
进入 App 的不仅是“菜名和正文”,更是每条整齐的记录。RAG 系统拿到 JSON 数组后,可以灵活地对任意字段做检索,就像在手机里按“食材”或“用量”筛选菜品一样自如。
- R_i第 i 条记录:表头之下第 i 行数据转化而成的一条结构化记录,对应菜谱中的一种食材。
- h_j第 j 个表头名:从表头行提取出的字段名,如“食材”“用量”。
- v_ij第 i 行第 j 列的值:当前行中与表头对应列的单元格内容,如“盐”“5克”。
- h_jv_ij 配对:冒号表示键值对,所有配对汇成一条 JSON 对象。
质量评估
抄完一整个档案馆的菜谱,不能直接交差,得随机抽查几页,对照原稿看看有没有抄漏、抄错。这就是表格提取的质量评估环节,也是 RAG 数据上线前的最后一道闸门。完整性检查负责统计:提取出的单元格数对比真实单元格数,差距过大说明有表格被整体跳过,或某些行列被漏读。准确性校验负责内容层面:抽样几十条记录,人工或规则化地比对原文,看看文字是否有串位、错位。
召回率评估则从检索视角回答“有没有漏掉表格本身”。假设文档里实际有 10 个表格,程序只提取到 8 个,那么表格级别的召回率就是 80%。在 RAG 场景里,漏掉一个表格往往意味着对应问题永远得不到正确答案,所以召回率比精确率更值得优先关注。
质量评估就像档案馆的终检员,拿着放大镜抽查几页菜谱,确认没有漏行漏列才允许入库。在 RAG 系统中,只有通过完整性、准确性和召回率三重检查的表格数据,才值得写进知识库,否则检索到的“结构化表格”里可能处处是坑。
- TP正确提取数:被成功提取且内容正确的表格或单元格,像抄写本上与原稿一致的那些行。
- FN漏提数:原稿中存在但程序没有提取出来的表格或单元格,像被翻页略过的菜谱。
- TP+FN真实总数:两部分相加等于文档中实际应有的总量。
- Recall召回率:正确提取数与真实总数之比,1 表示一个不漏,0.9 表示漏了一成。
AI 应用场景
金融研报财务表格抽取
阶段:数据准备与清洗
解决的问题:基金经理要把几十份 PDF 研报里的财务预测表(营收、利润、毛利率)整理成可比数据,传统做法是人工复制粘贴,一份报告要花半小时;AI 用 pdfplumber 解析加规则清理后,几分钟就能生成统一的 JSON 表,再进 RAG 向量库供投研问答检索。
具体案例:某金融科技的投研助手每天处理 200 份研报,提取出“2024 年营收预测”等表格后转换为 JSON 字段,分析师提问“哪些公司预期毛利率超过 30%”,系统直接检索结构化字段秒级给出答案。
合同条款信息结构化
阶段:文档解析与知识抽取
解决的问题:合同中的付款计划表、交付里程碑表,是法务高频查询的对象。AI 先用合并单元格处理补全横跨多行的“付款条件”,再用表头映射成“付款阶段/金额/截止日期”的记录,最终落入合同知识库。
具体案例:某企业法务系统接入表格提取管道后,上传 500 份 Word 合同时自动抽出所有付款计划表,法务提问“Q3 有哪些合同待付款”时,RAG 能准确定位到对应表格行并输出金额与日期。
Python 演示
"""从 PDF 与 Word 中提取表格并转为结构化 JSON 的演示"""import jsonfrom typing import List def extract_pdf_tables(pdf_path: str) -> List[List[List[str]]]: """使用 pdfplumber 从 PDF 页面中提取表格""" import pdfplumber tables = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: for table in page.extract_tables(): tables.append(table) return tables def extract_word_tables(docx_path: str) -> List[List[List[str]]]: """使用 python-docx 从 Word 中提取表格""" from docx import Document doc = Document(docx_path) tables = [] for table in doc.tables: rows = [] for row in table.rows: cells = [cell.text.strip() for cell in row.cells] rows.append(cells) tables.append(rows) return tables def fill_merged_cells(table: List[List[str]]) -> List[List[str]]: """把合并单元格的值广播到覆盖位置,避免缺值""" filled: List[List[str]] = [] for r, row in enumerate(table): new_row = [] for c, cell in enumerate(row): if cell: new_row.append(cell) elif r > 0: new_row.append(filled[r - 1][c]) else: new_row.append("") filled.append(new_row) return filled def tables_to_records(tables: List[List[List[str]]]) -> List[dict]: """表头映射为键,每一行转为一条 JSON 记录""" records = [] for raw in tables: table = fill_merged_cells(raw) if not table: continue header = [h.strip() for h in table[0]] for row in table[1:]: record = {} for i, cell in enumerate(row): key = header[i] if i < len(header) else f"col_{i}" record[key] = cell records.append(record) return records def main() -> None: pdf_tables = extract_pdf_tables("report.pdf") docx_tables = extract_word_tables("notes.docx") records = tables_to_records(pdf_tables + docx_tables) print(json.dumps(records[:5], ensure_ascii=False, indent=2)) if __name__ == "__main__": main()
page.extract_tables()用 pdfplumber 把一页 PDF 里所有表格以“行×列”的二维列表返回,是 PDF 表格提取的核心入口。doc.tablespython-docx 把 Word 文档中所有表格暴露成一个可遍历集合,直接拿到表格对象。cell.text.strip()取出单元格文字并去掉首尾空白,避免换行符和空格污染后续 JSON 字段。new_row.append(filled[r - 1][c])当前格为空时向上取上一行同列的值,完成纵向合并的复制填充。header[i] if i < len(header)把表头第 i 列当作键名,行单元格当作值,实现“表头→字段名”的映射。json.dumps(records[:5], ensure_ascii=False, indent=2)打印前 5 条记录,ensure_ascii=False让中文正常显示。
常见误区
误区我用了 pdfplumber 提取所有 PDF 的表格,遇到扫描件就提不出来,肯定是这个库不行,换个更强的库就好了。
正解扫描件里的表格本质是图片,任何基于文字定位的库都无效,必须先 OCR 把图片变成文字,再用有框线或无框线的解析策略。工具选型的本质是“按表格类型匹配工具”,而不是“找一个万能工具”。就像抄菜谱,活字印刷页用细齿梳,照片页得先拓印再抄写。
为何常见:初学者通常只接触过文字型 PDF,容易把工具能力绝对化,忽略输入格式对结果的决定性影响。
误区提取出的表格,合并单元格那几行是空的,直接转 JSON 存库里,反正检索时凑合能用。
正解空的合并区域会让记录缺字段,RAG 检索时该行无法被完整召回。正确做法是先识别 rowspan/colspan,做行列映射后把起点值复制到覆盖位置,保证每条记录字段齐全。这就像把“调味料”抄到每一行食材旁边,而不是只写一次。
为何常见:因为表格在视觉上“看起来”值只出现一次,新手误以为缺值是正常的,没有意识到这会让下游数据结构不完整。
误区表格提取脚本跑完没报错,输出 JSON 也生成了一堆数据,直接喂给 RAG 就好。
正解不报错不等于提取正确。必须做完整性检查(单元格数对比)、准确性校验(抽样比对原文)和召回率评估(漏表率),尤其要确认“文档里实际存在的表格”是否全部被提出来。终检员都没请,抄漏的菜谱自然悄悄进了数据库。
为何常见:报错导向的开发习惯让人只关注异常,忽略了静默错误——提取结果结构完整但内容错位,是表格提取最常见的失败模式。
误区每张表格都用第一行当表头做字段映射,结果有的表头是“名称/数量”,有的是“品名/规格/数量”,字段名对不上就报错。
正解真实文档里不同表格的表头往往不一致,需要做归一化:统一别名映射(“名称”→“品名”)、识别二级表头、无法匹配的列降级为 col_i 兜底名。表头是字段映射的钥匙,钥匙不对,门自然打不开。
为何常见:因为样例数据往往只有一张整齐的表格,容易忽略真实语料中表头多变的事实。
面试题
Q1. 在 RAG 文档处理中,为什么要对 PDF 和 Word 中的表格做专门提取?
- A.因为表格比普通文字更能提升页面美观度
- B.因为表格结构需要转为结构化数据才能高效检索← 正确答案
- C.因为表格里的文字都是加密格式无法直接读取
- D.因为 PDF 和 Word 本身不支持存储表格
答案:B。表格以行列结构承载信息,若整页转成文字会丢失结构,检索时无法按字段匹配。转为结构化数据后,RAG 才能对“行/列/字段”做精准召回。A 说的是视觉目标,C、D 与事实不符,选 B。
Q2. 下列哪个库可以从 Word 文档中直接读取表格对象?
- A.pdfplumber
- B.Camelot
- C.python-docx← 正确答案
- D.PyMuPDF
答案:C。python-docx 用于解析 Word 文档,doc.tables 直接暴露表格对象。pdfplumber、Camelot、PyMuPDF 都是处理 PDF 的工具,方向不同,故选 C。
Q3. 提取 PDF 表格时,发现页面完全没有可见边框线,最合理的做法是?
- A.放弃提取,改为人工录入
- B.用 PyMuPDF 按文本坐标自行拼行列← 正确答案
- C.使用 pdfplumber 只提取文字段落
- D.直接按整页文本送入向量检索
答案:B。无框线表格没有现成网格可循,适合用 PyMuPDF 拿到文字块坐标后按对齐关系自行拼表,或借助 Camelot 重建网格。A、C 放弃或退化为纯文本,D 丢失表格结构,故选 B。
Q4. 关于合并单元格,下列哪项描述正确?
- A.合并单元格在 PDF 和 Word 中都有明确属性标识
- B.提取时可以忽略合并单元格直接拼接文本
- C.合并单元格需要识别跨度并填充覆盖位置← 正确答案
- D.合并区域只影响显示样式不影响数据结构
答案:C。Word 有 rowspan/colspan 属性,PDF 通常要靠边框推断,A 错;忽略合并会导致缺值,B 错;合并既影响显示也影响数据结构,D 错。C 是标准处理流程的准确描述。
Q5. 表格转结构化数据时需要完成哪些环节?
- A.从首行提取表头字段名← 正确答案
- B.将每一行数据映射为一条记录← 正确答案
- C.使用 OCR 识别所有图片内容
- D.把表头作为键与单元格值配对← 正确答案
答案:ABD。结构化转换的核心是先提取表头、再行转记录、最后表头与值配对成键值对,三步缺一不可。OCR 只在扫描件场景才需要,不是通用必要环节,故选 ABD。
Q6. 在质量评估中,召回率主要回答什么问题?
- A.提取出的内容有多少是准确无误的
- B.文档中应提取的表格有多少被成功提取← 正确答案
- C.提取过程是否使用了正确的工具库
- D.结构化 JSON 的字段顺序是否合理
答案:B。召回率 = TP/(TP+FN),关注的是“该提取的有没有漏”。A 更接近准确率/精确率,C、D 与召回率无关,故选 B。
Q7. 一段工具输出文本 A 与标准答案 B 计算相似度,A=“盐糖醋”,B=“盐和糖醋”,共同字符为盐、糖、醋,则相似度约为多少?
- A.0.50
- B.0.67
- C.0.86← 正确答案
- D.1.00
答案:C。|A∩B|=3(盐、糖、醋),|A|=3,|B|=4,Sim=2×3/(3+4)=6/7≈0.86。A 是去掉乘 2 的结果,B 是分母取 6 的错误算法,D 要求完全相等。故选 C。
Q8. 关于 Camelot 与 pdfplumber 的差异,下列说法正确的是?
- A.Camelot 速度更快且依赖更少
- B.pdfplumber 擅长重建无框线表格
- C.Camelot 通过视觉线条重建表格网格← 正确答案
- D.pdfplumber 完全不支持表格提取
答案:C。Camelot 基于 OpenCV 找直线并重建网格,适合复杂表格但依赖重、速度慢,A 错;pdfplumber 对有线表格友好但对无框线表能力有限,B 错;pdfplumber 的核心能力之一就是表格提取,D 错。故选 C。
Q9. RAG 系统中表格提取的完整质量评估应包含哪些维度?
- A.完整性检查:对比真实单元格数与提取数← 正确答案
- B.准确性校验:抽样比对单元格文字是否一致← 正确答案
- C.召回率评估:统计漏掉的表格数量← 正确答案
- D.代码复杂度评估:统计解析脚本的行数
答案:ABC。完整性、准确性、召回率是表格提取质量评估的三个核心维度,分别回答“有没有漏格、有没有抄错、有没有漏表”。D 是工程维护指标,与提取质量无直接关系,故选 ABC。
Q10. 关于表格识别基础,以下说法正确的有哪些?
- A.表格是由行列构成的二维网格结构← 正确答案
- B.表格类型的差异会影响工具选型← 正确答案
- C.扫描件属于图片型表格需要特殊处理← 正确答案
- D.所有 PDF 表格结构都可以直接读取
答案:ABC。表格是行列网格,表头、行、列、单元格是其基本结构;有线/无线/图片型分类直接影响选型;扫描件本质是图片,需 OCR 预处理。D 与事实不符,文字型 PDF 可以直接读取,扫描型不能。故选 ABC。