前阵子帮朋友处理一批采购合同的自动化解析,几十份 Word 文档喂给模型之后,结果惨不忍睹:有的把表格里的金额读串位,有的把页眉的公司名当成正文反复引用,更离谱的是有一份带修订痕迹的文档,模型把删除线里的旧条款当成有效内容回答给了业务部门。排查到最后,问题根本不在模型,而在这批 .docx 文件里的排版结构太“有性格”。
那次之后我彻底想明白一件事:AI 读不懂 Word,通常不是模型笨,而是我们把“读文档”这件事想得太简单了。很多人以为把 Word 喂给 AI 就是把文字抠出来,其实 Word 的真正价值,以及所有解析难点的根源,恰恰藏在排版里。
这篇文章我想把这几年做文档解析踩过的坑、试过的路线、沉淀下来的方法论完整梳理一遍,给要接文档类 AI 应用的开发者、做知识库抽检的运营、还有被各种报告折磨的办公族一个可复用的参考。
1. Word 的核心是排版,而 AI 的默认值只看文字
1.1 一个真实场景引发的“血案”
先说开头那个合同项目的细节。朋友给的文档里有几种典型结构:封面、目录、正文多级标题、十几张供应商信息表、最后还有一页手写的签字扫描图。我当时直接用文本抽取工具把段落拼成一个大字符串,塞进模型的上下文窗口,想着反正模型能理解自然语言,抽出来的字串得越全越好。
结果就翻车了。文本流里表格被逐行拍扁之后,金额和数量列全部串到一堆,模型分不清“总价 12,500”到底是甲条款还是乙条款;目录页的前言页码又和正文数字混在一起产生幻觉;更麻烦的是因分页符被拆断的段落,模型经常把一个合同条款误判成两句独立的陈述。
这个案例其实是典型症状:把 Word 当成“带格式的 TXT”,而不是当成“有结构的排版容器”。只要视角错了,后续所有步骤都会跟着错。
1.2 .docx 的底细:一个装着 XML 的压缩包
很多人不知道,.docx 本质上是一个 Zip 压缩包,里面装的是若干 XML 文件。随便解压一个 .docx,你会看到这样的结构:
word/document.xml——正文内容和排版的骨架word/styles.xml——样式表,定义标题、正文、列表等外观word/media/——嵌入文档里的图片word/footnotes.xml“脚注、尾注word/header1.xml「页眉、页脚word/numbering.xml——编号规则,多级列表的秘密都在这
换句话说,同一个“第二章”可能用五笔、楷体、加粗、斜体、红色、居中、缩进、编号八种属性去描述,而这些属性全部堆在 XML 标签里,真正的字符串反而只占文件体积的一小部分。AI 要读懂 Word,先要穿越这一整层排版迷雾。
也正因如此,任何只按纯文本处理 Word 的方案,天然丢失掉至少一半信息量——结构信息、表格坐标、层级关系、样式继承全部没戏。
1.3 为什么“纯文本”永远是副产物
Word 的设计哲学是“所见即所得”,用户看到的是排版后的效果,文字只是排版的素材之一。排版元素包括:
- 分页符、分节符,会切断句子
- 首行缩进、悬挂缩进,属于段落样式
- 多级编号列表,各级数字靠
numbering.xml里的规则生成 - 表格行列合并、嵌套表格,是严格的二维结构
- 图片、文本框、形状,内容不在
document.xml的文本流里
所以当你把 Word 转成纯文本时,这些信息被一并丢掉。你会得到一个连续的字符串,但字符串中可能混着页码、页眉、脚注,表格结构也荡然无存。模型再聪明,也猜不出你丢掉了什么。
注意:做文档类 AI 应用,第一步就要确立“结构优先于文字”的原则。文字只是排版的投影,排版才是文档的本体。
2. 解析 Word 的路线选择,先搞清楚你要付出什么代价
2.1 python-docx:干净场景的首选,但能力边界明显
python-docx是最常用的库,对结构清晰的文档很舒服,能直接访问段落、表格、样式名,代码写起来也短。比如最简单的段落提取:
from docx import Document doc = Document("example.docx") for para in doc.paragraphs: if para.text.strip(): print(para.style.name, "|", para.text)这条命令能把每个段落和它绑定的样式名一起拿出来,对论文、制度文件这种规范文章基本够用。
但它有几个硬伤:一是对复杂布局力不从心,文本框、形状里的文字读不到;二是表格嵌套不支持直接便利,需要自己递归;三是修订记录不会自动合并,旧文本和新文本可能同时出现。所以 python-docx 适合“文档质量可控”的预处理链路,不能当万能解析器。
2.2 直接拆包读 XML:一撸到底,最灵活也最繁琐
遇到 python-docx 搞不定的文档,我一般会直接打开 Zip 包,用 XML 方式解析。能做的事一下子就多了:
import zipfile from lxml import etree with zipfile.ZipFile("example.docx") as z: with z.open("word/document.xml") as f: tree = etree.parse(f) for elem in tree.xpath("//w:p"): text = "".join(t.text or "" for t in elem.xpath(".//w:t")) print(text)这等于绕过了所有封装,直接从 XML 标签里拿文本和属性,页面上下文、样式继承、多级编号全都能读到。代价是代码量大、兼容性环节多,需要在命名空间上花不少功夫。适合做自定义解析器和特殊排版规则处理。
2.3 转 PDF 再提取:用一次格式转换换取内容稳定
这个路线听起来绕,但实战很常用:先把 .docx 转成 PDF,再用 PDF 解析工具把文字和位置抓出来。转换后文本不再依赖 Word 内部的 XML 谜局,页面的坐标是确定的,版面结构相对稳定。
为什么需要这样做?因为原生的 Word 排版里缩进、对齐、悬浮元素都靠属性运算,AI 很难从 XML 直接还原“这一栏属于哪一列”。转成 PDF 就是把排版运算结果“拍扁”成一个位面,再用坐标反推布局,反而好理解。
代价也明显:交互控件、动态域、批注这些信息会丢,表格如果被分页截断,还得按坐标做重建,工作量不小。我一般只在文档来源五花八门、格式严重损坏时才用这招。
2.4 万能备选:pandoc 转 Markdown
还有一个被埋没的利器:pandoc 可以把 Word 转成 Markdown,顺带保留标题层级、表格结构、列表嵌套、链接和图片引用。
pandoc input.docx -t markdown -o output.md转出来的 Markdown 干净、可读,还能直接塞进模型上下文。表格会变成管道符分隔的 MD 表格,嵌套列表会用缩进体现。它的短处是样式细节丢得比较多,比如字体颜色、缩进的具体数值,但这类信息对 AI 理解语义本来就不关键。
我的经验是:如果你的目标是“让 AI 读内容,而不是复刻版式”,pandoc 这步往往能节约一半工作量。
3. 实操:把排版结构翻译成 AI 能理解的文本
3.1 场景一:干净报告/论文的段落与表格抽取
先处理最常规的文档形态——标题、正文、表格齐全但没有宏和文本框。这个场景我用 python-docx 做基础抽取,同时保留标题层级和表格对象。
from docx import Document def iter_block_items(parent): from docx.document import Document as _Doc from docx.table import Table from docx.text.paragraph import Paragraph from docx.oxml.ns import qn if isinstance(parent, _Doc): parent_elm = parent.element.body else: parent_elm = parent._element for child in parent_elm.iterchildren(): if child.tag == qn("w:p"): yield Paragraph(child, parent) elif child.tag == qn("w:tbl"): yield Table(child, parent) doc = Document("report.docx") for block in iter_block_items(doc): if block.__class__.__name__ == "Paragraph": text = block.text.strip() if text: print(f"[{block.style.name}] {text}") elif block.__class__.__name__ == "Table": for row in block.rows: cells = [cell.text.strip() for cell in row.cells] print(" | ".join(cells))这段代码的价值在于“按文档流顺序”同时拿段落和表格,避免先用doc.paragraphs拿到所有段落、再用doc.tables拿到所有表格,结果先后顺序对不上的尴尬。
有的工具库会把表格里的文字重复出现,因为合并单元格会产生重复引用,我通常会在 join 前去重。实测下来,提取干净文档的结构,这套流程最稳。
3.2 场景二:多级标题+嵌套表格的长文档分块
长文档最大的坑是“一股脑塞给模型”导致上下文爆掉,分块策略就成了核心问题。我的做法是根据标题层级切块,而不是按页切。
思路是:先找到所有一级标题和二级标题,把文档切成若干个逻辑块,每个逻辑块内的正文、表格、图片说明打包成一个单元。这样模型收到的每个块是完整语义段落,而不是被页码生生切开的碎片。
from docx import Document doc = Document("long.docx") blocks = [] current_heading = "前言" current_texts = [] for para in doc.paragraphs: if para.style.name.startswith("Heading"): if current_texts: blocks.append({"title": current_heading, "content": "\n".join(current_texts)}) current_heading = para.text.strip() current_texts = [] else: if para.text.strip(): current_texts.append(para.text.strip()) if current_texts: blocks.append({"title": current_heading, "content": "\n".join(current_texts)})分块长度需要根据后续模型的窗口上限做控制。如果大块太长,再按段落粒度二次拆小,同时在块之间保留一点重叠,避免语义断裂。我习惯在每块开头附上“本文档标题:某某 + 当前二级标题:某某”这样两行元信息,模型在读正文时就知道自己是谁、在哪个章节。
3.3 场景三:老式 .doc 文件的迁移策略
碰到旧版二进制 .doc 是很头疼的事。python-docx 和 zipfile 在 .doc 面前完全失灵,因为 .doc 是二进制格式。我的处理顺序通常是这样:
- 第一步,检查能不能用 LibreOffice 的命令行批量转换。这是很多公司没意识到的好工具,它能用脚本把一批 .doc 统一转成 .docx 或 PDF。
- 第二步,转换后新文档再走 python-docx 或 pandoc 管线。
- 第三步,如果转换失败或格式损坏,退回到“按打印视图导出截图 + OCR”的方案,用视觉模型读取图片内容。
会里真切的一幕就是碰到几十份十年以上的老合同,字体集乱、域代码失效,LibreOffice 转换后仍有错页。最后我用 PDF 渲染然后 OCR,反而最省事。
3.4 场景四:图片里的信息怎么办
Word 文档里的图片分几种:作为插图、作为扫描件、作为表格截图、还有装在文本框里的图形文字。前两种可以直接忽略或按“图注说明”处理,后面两类必须认真对待。
一个很常见的错误是把图片丢给传统 OCR,结果表格截图被识别成零散文字,行列关系全错。我的办法是先识别表格线,按坐标切分单元格,再对每个单元格做 OCR,最后重新组装成表格文本。这个流程成本高,但效果立竿见影——模型终于能看懂图片里的“数量、单价、金额”了。
如果扫描件不是特别老,现在很多多模态模型可以直接把图片本身作为输入传给模型,省了中间步骤。但在批量场景里,还是建议先做一次 OCR 预清洗,因为模型的视觉通道成本和延迟都高很多。
4. 常见故障与排查实录
4.1 文本乱码、错位和全角符号问题
Word 解析出现乱码,十次里有八次是编码或字符宽度问题。比如全角逗号、全角空格、换行符混在文本里,会让模型对字符串的判断出错。
另一个经典场景是大段的“自动编号”,Word 其实不在 document.xml 里写编号数字,而是在 numbering.xml 里生成规则。直接用文本流抽取,你会看到“1.1”凭空消失,只剩下“引言”“方法”干巴巴地挤在一起。排查时要去读w:numPr节点,或者用 python-docx 的 numbering 逻辑手工“算”出编号。
提示:凡是看到“列表里的数字丢了”“目录里的页码全是 1”这类问题,先查编号规则,别急着怀疑解析库坏了。
4.2 批注、修订、页眉页脚悄悄混进来
前面说的合同案中,修订痕迹就是典型。python-docx 默认会返回原始文本,不会合并修订符号里的“删除”“插入”。吞噬文本后,模型会认真地把“本条无效”当成有效条款。
我的排查方法是先从word/settings.xml里看修订状态,如果有w:ins或w:del标记,就要决定是接受修订还是拒绝修订。实操中我用一个比较粗暴的方式:
- 检测到
w:del的文本片段,删除; - 检测到
w:ins的文本片段,保留; - 批注内容单独提取,不让它进入正文流。
页眉页脚另一个坑。很多文档的页眉里印着公司名、Logo、保密等级,这些内容会重复出现在每一页的文本里。如果不加过滤,模型很容易把它们当成正文的高频词,进而产生错误推理。
4.3 合并单元格与嵌套表格导致的行列错乱
经常遇到医院报告、考核表、报价单这种复杂表格:同行跨列合并、文字换行、单元格里塞图片。直接用 table.rows 遍历,会得到大量重复或空值,因为合并单元格的引用会覆盖多个 grid 位置。
我的建议是直接用 XML 层级遍历表格结构:
cell_map = {} for row_idx, row in enumerate(table.rows): for col_idx, cell in enumerate(row.cells): if col_idx in cell_map: continue cell_map[col_idx] = cell.text.strip()但更稳妥的做法是读取每个单元格的w:gridSpan和w:vMerge属性,把跨度信息显式记下来,生成伪表格坐标,再用坐标输出。这个过程比较繁琐,却是正确理解合并单元格的唯一途径。
嵌套表格就更难受了,常常外面是一个大格子,里面又嵌了一个表格。python-docx 拿内层表格需要递归查询,直接平面遍历只能拿到外层文字。遇到这种文档,我的建议是优先考虑 pandoc 转换,DTP 工具内部的表格嵌套处理往往比手写递归更可靠。
4.4 分页符和节符把一句话腰斩
Word 文档经常因为分页符、分节符把一句话断开。从 XML 文本流看,这是两个文本段,但从语义看应该连在一起。这个问题的排查思路得反过来看:不把分页符当分隔,要结合上下文判断。
如果文本流里发现“上一条末尾没有句号,下一条开头不是大写或序号”,我会把这相邻两段合并。但实际业务里,更常见的做法是“保留分页信息而不是假装它不存在”,比如把“第 3 页”作为分隔标记注入文本,这样模型至少知道页面的边界在哪,不会把两个章节的碎片强行缝合。
5. 从“能读”到“读得好”:让 AI 用上结构的进阶玩法
5.1 先转 Markdown,再给模型做上下文注入
文本抽取做得差不多了,接下来就要考虑怎么把文本送给模型。裸文本的准确率上限有限,换 Markdown 之后效果会明显提升——因为 Markdown 保留了标题、表格、列表、引用等结构标记,模型能识别语义边界。
我常在预处理流程里加一步:
import subprocess subprocess.run([ "pandoc", "input.docx", "-t", "markdown", "-o", "output.md" ], check=True)然后直接把 Markdown 内容读进来,再按##级别切块。切完之后给每块加一个“来源文件 + 章节名”的前缀,让模型理解长文档的章节归属关系。
对比实测:同样一份带三级标题和五个表格的文档,纯文本抽出后模型回答关于“第二张表第三行数据”的问题时频频出错,Markdown 结构输入后,模型能稳定引用正确的表格坐标。不要小看这种“符号级”的改进,它决定模型对结构化内容的敏感度。
5.2 按逻辑块切片,而不是按页数切
分块是一个容易被低估的环节。很多团队偷懒按固定 token 长度切块,比如每 2000 字符一刀,切完不管。遇到表格被腰斩、标题和正文分离、一句话横跨两个块,忠实程度惨不忍睹。
我现在的分块逻辑是“按结构优先,token 长度兜底”。具体来说:
- 以标题为一二三级块的第一层边界;
- 表格整体作为独立块,不跟上下段落混切;
- 如果块长度超过窗口上限,再在段落之间切开,而不是在句子中间切;
- 相邻两个块之间保留 100~200 字符的重叠,把被边界截断的语义修补回来。
这样的切法比固定长度切片多花 15% 的预处理时间,但对模型回答质量的提升是立竿见影的。做知识库问答的团队值得投入这点成本。
5.3 输出 JSON 化结构,把表格、列表、标题变成显式字段
最后提一个偏进阶的玩法:与其让模型读排版后的文字,不如先把 Word 解析成结构化 JSON,再把 JSON 喂给模型。JSON 里包含标题层级、段落列表、表格行列、图片说明等字段。这样模型不再需要从文本流里还原结构,直接读 JSON 就能知道“表格的第二行列标题”是什么。
{ "title": "供应商资格评审表", "headings": [ {"level": 1, "text": "一、报价要求"}, {"level": 2, "text": "1.1 价格组成"} ], "tables": [ { "name": "报价明细表", "rows": 6, "cols": 4, "header": ["序号", "项目", "单位", "金额"], "body": [["1", "调试服务", "项", "3500"]] } ] }这个方案对多模态模型尤其友好,因为表格的结构信息被建模成数组,不会被空格宽度、缩进、边框线干扰。代价是解析代码量翻倍,但换来的是后续 prompt 设计简单、准确率上限更高。如果你的项目对文档信息精度要求极高,比如审计、合规、合同审查,我建议直接走这条路线。
这几年处理文档类 AI 项目的经验让我有个很深的体会:排版不是 AI 理解文档的障碍,而是被很多人忽略的隐藏信息层。当你开始尊重标题层级、表格坐标、样式继承、修订痕迹这些“格式暗号”,模型的幻觉率和答非所问率会大幅下降。
最后分享一个小技巧,也是我现在每个文档项目都会做的一步:接文档之前,先让业务部门提供三份“代表性样本”——一份带复杂表格,一份带修订记录,一份含扫描图片。用这三份样本跑通全流程后再大批量处理,能省掉后面至少七八成的返工。文档解析这活,越早暴露格式的复杂性,后面越顺利。