想象一位图书管家。他被聘来管理你新书房里堆积如山的纸箱——其中有装订好的项目报告、散落的会议纪要,还有排版精致的财务报表。你想问他“去年的预算批复是多少”,他不会把每张纸都从头读一遍,而是走到书架对应位置,抽出几本“相关”的资料快速翻阅。可问题是:这些资料还躺在箱子里,管家连箱子里有什么都不知道。你要做的第一件事,就是把纸箱打开、把内容分类、把知识整齐摆上书架。这个过程在 AI 领域叫作“文档解析与分块”,它是 RAG(检索增强生成)系统的第一公里。本文将带你用“搬家整理”的视角,学会如何处理 PDF 和 Word 文档——从工具选型、版面恢复到分块策略,让“图书管家”真正有书可查。
文档解析质量直接决定 RAG 问答效果的上限。掌握这项“搬箱术”,是从“能跑通 Demo”到“能扛住生产环境”的分水岭,也是 RAG 应用工程师最硬核的基本功。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
文档解析流水线可视化:展示从原始文档到RAG就绪的分块处理流程
文档解析概述:先把纸箱搬出来再说
你换了新房子,书房的角落堆着十二个纸箱:有打印的项目报告、手写的会议纪要,还有排版精致的财务报表。管家想做一本“知识总目录”,但他连箱子里有什么都不知道。你的第一项工作,就是开箱、清点、分门别类——把散乱的信息变成管家认得的东西。这一步如果做得敷衍,之后无论管家记性多好(模型多强)、翻书多快(检索多快),都找不到该找的资料。
在 RAG 系统里,“图书管家”由三部分组成:向量索引(目录)、检索器(找书人)和生成器(阅读并作答的人)。用户提问时,检索器先拿着问题去目录里找候选书架,生成器再基于候选内容回答。而文档解析,就是给这套系统“造册”的前置工序。它把 PDF、Word 这类“非结构化文档”——没有统一字段、人和机器都难以直接读取的文件——改造成结构清晰、能切分、能向量化的文本。
一条典型的文档处理管线长这样:加载(load)→ 解析(parse)→ 清洗(normalize)→ 分块(chunk)→ 向量化(embed)→ 建索引(index)。其中“解析”决定源头水量。业内常用“解析覆盖率”衡量这一步做得够不够:抽出的有效字符数占文档应有字符数的比例。
搬家师傅干活前会先清点纸箱数量、登记编号。解析覆盖率就是你的“清点单”:它不保证每本书都放对书架,但能告诉你到底有多少书、丢了多少。接下来我们逐个房间查看——先看最棘手的 PDF 箱子。
- 分子 \(E_{\text{extracted}}\)(提取字符数)解析器真正从文件里捞出来的有效字符总量。PDF 中一个字符可能藏在字体编码表里,捞不出来就算丢失。
- 分母 \(E_{\text{total}}\)(应有字符数)文档真实包含的字符总数,可用人工抽查或两套工具交叉统计来估计。
- 比例与百分比两者相除再乘 100,得到 0 到 100 的百分比,便于跨文档横向比较。
PDF解析工具:挑对开箱的家伙事儿
PDF 这个纸箱有点特殊:它出厂时就被“塑封”了。你在屏幕上能清清楚楚看到文字,但那是因为阅读器替你“翻译”了页面图画。PDF 本质上是一份“打印格式”——每个字、每条线都是按坐标画在页面画布上的图形。它没有段落、没有标题、没有表格的概念。想把它变成纯文本,得像用美工刀割开塑封一样,用专用工具解析。
最常用的两把“刀”:PyMuPDF(fitz)——锋利的美工刀,解析速度飞快,适合以文字为主的 PDF,一句 page.get_text("text") 就能把整页文字捞出来,还附带每个字符的坐标;pdfplumber——精密镊子,慢但细腻,特别擅长顺着表格线把单元格一个个夹出来,适合财务报表、登记表这类重表格文档。
怎么选型?三条经验:纯文本 PDF(论文、报告正文)用 PyMuPDF,性价比最高;表格密集或版式复杂用 pdfplumber;另外还有 pypdf、PDFMiner、unstructured 等助手工具——unstructured 适合快速试水,但对生产环境可控性较差。记住:解析覆盖率是试金石,同样的 PDF 换把“刀”,结果可能天差地别。
表格相册和纯文本书本,用的开箱工具可不一样。开箱工具选对了,后面才不伤“物件”——PDF 没有“万能开箱器”,按内容选工具,是搬箱术的第一条纪律。
- 字符 \(w_i\)PDF 内容流里记录的最小文本单元,一个字母或一个汉字。
- 映射符号 \(\mapsto\)表示“对应到”,把字符本身与它在页面上的位置绑定。
- 四个坐标值\(x_0, y_0\) 是字符矩形左下角坐标,\(x_1, y_1\) 是右上角坐标,两点确定一个矩形;PDF 坐标系原点在页面左下角,y 轴向上。
Word解析要点:开一箱自带标签的宝贝
如果说 PDF 是塑封印刷品,Word 就是“自带标签的收纳盒”。原作者写文档时已经用标题样式把章节分好了:一级标题、二级标题、正文、表格,各有各的“标签贴纸”。开这箱既不费刀也不费镊子——只需一只会“读标签”的手,也就是 python-docx。
为什么 Word 好解析?docx 文件本质上是一个 ZIP 压缩包,里面躺着 document.xml 等结构化 XML 文件。XML 用标签清清楚楚写着:这里是一个段落,那里是一个表格。python-docx 把这层结构直接暴露给开发者:doc.paragraphs 拿到所有段落,paragraph.text 得到文本。
真正的宝贝是“样式”。paragraph.style.name 会告诉你这段文字是 Heading 1 还是 Normal——这恰好是后续分块最需要的“章节分隔信号”!把标题样式保留下来,分块时就能按章节自然切分,而不是盲人摸象式地数长度。Word 里的表格也能通过 doc.tables 逐行逐列读取,原生结构比 PDF 干净得多。
于是搬这箱的效率高得多:标签就是标题,盒子就是段落。把标签抄进“总目录”,管家以后按章节查书毫不费力——别忘了,Word 里那些“标签”(样式)别扔,它们是后续分块的地图。
- 段落 \(P_i\)Word 正文按段落存储,\(P_i\) 表示第 \(i\) 个段落对象,来源是
doc.paragraphs。 - Runs 集合一个段落可能有多个 run,每个 run 携带独立的字体、字号、加粗等格式。
- 求和拼接按顺序把各 run 的文本拼接起来,得到完整的段落文本。
表格图片抽取:易碎品请单独装箱
有些“物品”特别娇贵:相册(表格)和奖状(图片)不能跟书混在一起压。如果拿解析纯文本的办法对付表格,等于把照片从相框中扯出来——照片还在,玻璃碎了,排版彻底散架。表格要整整齐齐地搬:行列关系就是它的“玻璃框”。
表格解析的正确姿势是“坐标网格还原”。pdfplumber 通过检测表格线交叉点,把单元格还原成二维网格,再输出成 Markdown 或 HTML——这两种格式保留了“行列”语义,大模型读起来能理解“哪一列是金额、哪一行是差旅费”。Word 表格更省事:python-docx 直接按行和列读取原生结构。
图片和扫描件是另一类“易碎品”。PDF 里嵌入的图片用 PyMuPDF 的 page.get_images() 提取坐标并导出;扫描版 PDF 的“文字”其实是像素,必须先做 OCR(把像素里的文字识别成文本的技术,如 PaddleOCR、Tesseract)再进解析管线。多模态大模型也能直接吃“页面截图”,但成本和延迟更高,通常只做兜底。
易碎品单独打包、贴上标签——表格转 Markdown、图片单独导出,目的就在于此。等管家日后检索到这段内容,它既能看懂“数字”,也能看懂“数字属于哪一列”。
- 单元格 \(C(i, j)\)第 \(i\) 行、第 \(j\) 列的单元格,行号和列号一起唯一定位它在网格中的位置。
- 四个坐标left/top 是左上角,right/bottom 是右下角,共同围出单元格的矩形区域。
- 网格组合所有单元格共享行线、列线,拼起来就是完整表格,再转成 Markdown 输出。
版面恢复:把颠散的相册按页码拼回去
搬家路上,车厢一颠,相册散了,照片在地上混成一堆。PDF 解析出来的文本块就是“散落的照片”:它们按文件内部的存储顺序输出,而不是人眼阅读的顺序。直接拿这份乱序文本去分块,等于让管家读一本页码装订错乱的书。
版面恢复的常规动作有三步。第一,按坐标排序:每个文本块都有 y(纵向)和 x(横向)坐标,单栏文档按“先 y 后 x”升序排列,就还原了从左到右、从上到下的阅读顺序。第二,双栏检测:如果页面是左右两栏,先把 x 坐标聚类成栏,再对每一栏按 y 排序,最后左栏整栏接右栏——论文 PDF 十有八九是这种情况。第三,清理页眉页脚:页眉页脚每页都在固定位置反复出现,用“位置 + 词频”就能识别并剔除。
一个常见坑:PDF 的一行文字可能被拆成多个小文本块(尤其是不连续字体的标题),需要在排序前先按 y 坐标聚类、把同一行的碎片合并成行,否则排序后仍会串行。
相册按页码拼好,管家才能顺着页码读故事。版面恢复就是给 PDF 文本块重新编页码——这步做好了,“预算批复”和“落款日期”才不会在向量空间里错位相逢。
- 块集合 \(\mathcal{B}\)解析器输出的文本块集合,每个块都带坐标。
- 排序键 key选每个块的“顶边 y”和“左边 x”作为排序依据。
- 升序排列先比较 y,y 相同再比较 x,得到的排列就是阅读顺序。
分块策略:书架隔层的艺术
书已经开箱归好类,接下来要考虑怎么摆上书架。摆法有讲究:每层放多厚?(块大小)层与层之间要不要留叠放区?(重叠)是按书名首字母摆还是按主题摆?(分隔符/语义)。这直接决定管家找书时,一眼扫过去能不能看到完整信息。
向量模型一次能处理的文本有限,且嵌入对语义敏感。块太大,几百个字被平均成一个向量,细节被“冲淡”,检索召回一堆“好像相关”的废话;块太小,一句话没有上下文,检索到了也回答不了“这个数字指的是哪年的”。经验值:中文场景块大小 200~500 字,重叠 10%~20%。
切法优先级:最好按自然边界切——遇到空行、标题、章节号下刀,语义最完整;没有自然边界才用定长滑动窗口。进阶方案:语义分块(用句子向量相似度决定在哪切)、父子分块(父块提供上下文、子块用于检索)。块数可以由公式估算:
书架隔层定好了,管家“一翻一个准”。分块不是越细越好,也不是越粗越好,而是“足够完整 + 足够聚焦”——这和你给好友荐书时会挑一章而不是一句话,是同一个道理。
- 窗口步长 \(s - o\)每切完一块,窗口前进的长度是“块大小减去重叠”。
- 可切长度 \(L - o\)最后一块吃到文档末尾即可,所以分子只需覆盖 \(L - o\) 的跨度。
- 向上取整切不到整数块时,末尾补一块较短的;\(\lceil \cdot \rceil\) 保证不丢内容。
工程实战:搬家队的标准作业单
正规搬家公司从不清场式搬运气——他们有一张标准作业单:清点、包装、装车、运输、验收。文档解析进生产环境同样需要一张“作业单”:把零散 Python 脚本变成可监控、可回滚、可评估的解析管线。
标准管线串联:加载(load)→ 解析(parse)→ 清洗(normalize)→ 分块(chunk)→ 向量化(embed)→ 建索引(index)。主流的 LangChain、LlamaIndex 都有现成 Loader,但底层仍是我们前面讲的解析工具。工程上建议把解析结果落盘成中间态(如 Markdown 或 parquet),一次解析、多次复用,省去重复解析的算力。
异常处理要兜底:加密 PDF 需提前检测并提示;扫描件按页 OCR,保留页码信息;损坏文件用 try/except 捕获并写入失败清单,而不是让整个任务崩溃。性能上用多进程按页并行,配合缓存。最后是关键一步——质量评估:抽样人工校对解析文本、统计覆盖率、做端到端召回对比,形成“解析→索引→检索→反馈→调参”的闭环。
作业单的最后一步是“客户验收”。当解析覆盖率、召回率都进了监控面板,搬箱术才算真正落地——管家从此有书可查,你也能在凌晨三点安心睡觉,而不是被“为什么这个问题答不上来”的告警吵醒。
- 分子(命中的相关块)测试问题在向量库中检索,返回结果里真正与问题相关的块数。
- 分母(所有相关块)人工标注的、应该被检索到的相关块总数。
- 比值命中相关块占全部相关块的比例,越接近 1 说明“从解析到分块再到检索”整条链路越健康。
AI 应用场景
企业制度文档检索问答
阶段:文档加载与解析(RAG 前置处理)
解决的问题:HR、行政每天被“年假怎么休”“报销流程是什么”等重复问题淹没。传统方案是人肉翻 PDF 制度文件,慢且版本混乱;AI 方案先把数百份制度 PDF 批量解析成干净文本,再按制度章节分块入向量库,问答系统直接检索作答。
具体案例:某互联网公司用 PyMuPDF 批量解析 300 份制度文件,用“标题样式 + 章节号”切块,叠加 10% 重叠。上线后 HR 重复问询下降 70%,回答附带的引用来源可追溯到具体页码。
券商研报行业知识库
阶段:版面恢复 + 表格抽取
解决的问题:研报 PDF 普遍双栏排版、图表密集,直接文本抽取会左右栏串行,表格内容变成乱码,分析师检索“某行业毛利率”时找不到对应数字。
具体案例:解析管线先用 pdfplumber 抽取表格转成 Markdown,再做双栏版面恢复(左栏读完接右栏),最后按“行业-公司-指标”做语义分块。上线后检索命中率从 45% 提升至 82%。
Python 演示
# -*- coding: utf-8 -*-"""RAG 文档解析演示:PDF/Word 文本抽取 + 重叠分块"""import re class SmartChunker(object): """带重叠窗口的分块器:先按自然段合并,超长段再做滑窗""" def __init__(self, chunk_size=240, overlap=40): self.chunk_size = chunk_size self.overlap = overlap if overlap >= chunk_size: raise ValueError("overlap 必须小于 chunk_size") def split(self, text): items = [p.strip() for p in re.split(r"\n+", text) if p.strip()] chunks, buffer, buf_len = [], [], 0 for item in items: if buf_len + len(item) + 1 > self.chunk_size and buffer: chunks.append("\n".join(buffer)) # 重叠:保留最后一段带回下一块 buffer = buffer[-1:] buf_len = len(buffer[0]) buffer.append(item) buf_len += len(item) + 1 if buffer: chunks.append("\n".join(buffer)) result = [] for chunk in chunks: if len(chunk) <= self.chunk_size: result.append(chunk) continue start = 0 while start < len(chunk): end = min(start + self.chunk_size, len(chunk)) result.append(chunk[start:end]) if end >= len(chunk): break start = end - self.overlap return result class DocumentParser(object): """文档解析门面:按文件类型分发到 PDF / Word 解析器""" def parse_pdf(self, path): """用 PyMuPDF 抽取文本,生产环境可换 pdfplumber 处理表格""" try: import fitz except ImportError: return "未安装 PyMuPDF,请执行: pip install pymupdf" pages = [] with fitz.open(path) as doc: for page in doc: pages.append(page.get_text("text")) return "\n".join(pages) def parse_docx(self, path): """用 python-docx 读取段落标题样式与表格""" try: import docx as docx_lib except ImportError: return "未安装 python-docx,请执行: pip install python-docx" document = docx_lib.Document(path) parts = [] for para in document.paragraphs: if para.text.strip(): parts.append(para.text) for table in document.tables: for row in table.rows: parts.append(" | ".join(cell.text.strip() for cell in row.cells)) return "\n".join(parts) def main(): sample = ("第一章 引言\n公司年度报告摘要。\n" + "本季度收入稳步增长,环比提升明显。" * 30) chunker = SmartChunker(chunk_size=100, overlap=20) chunks = chunker.split(sample) print("原始字符数:", len(sample)) print("分块数量:", len(chunks)) for index, chunk in enumerate(chunks[:3], 1): print("块 %d: %s ... (%d 字符)" % (index, chunk[:24], len(chunk))) if __name__ == "__main__": main()
class SmartChunker(object):定义带重叠的分块器,封装正文讲的“块大小 + 重叠”两个核心参数。if overlap >= chunk_size:参数合法性校验:重叠等于块大时窗口永远不前进,直接抛错。if buf_len + len(item) + 1 > self.chunk_size and buffer:当前缓冲装不下这段时,把缓冲先输出为一块。buffer = buffer[-1:]输出前把最后一段留回缓冲,实现段落级重叠。start = end - self.overlap滑窗前进时扣掉重叠长度,实现字符级重叠。pages.append(page.get_text("text"))PyMuPDF 的核心抽取:把整页文字按阅读顺序捞出来。for para in document.paragraphs:python-docx 遍历 Word 所有段落,对应“自带标签的收纳盒”。parts.append(" | ".join(cell.text.strip() for cell in row.cells))表格一行单元格用竖线拼接,保留行列语义。if __name__ == "__main__":可执行入口,直接运行即可看到分块效果。
常见误区
误区PDF 看起来有文字,就直接用通用文本读取器读;读不出来再换工具,反正“只要能打开文件就能抽文本”。
正解PDF 是“打印格式”,字符是按坐标画在页面上的图形,与印刷品没有本质区别。它没有段落、标题、表格结构,必须用解析工具处理;且不同工具抽取质量差异很大。就像相册不能拿扫描枪直接扫出文字,得先把玻璃擦干净、按顺序摆好。
为何常见:因为人在屏幕上“看到”了文字,误以为文件里存在一个可读的“文字层”,忽略了 PDF 的渲染本质。
误区为了让检索“精准”,把文本切成几十个字的短块,认为小颗粒一定能命中更准。
正解块太小会丢失上下文,向量像把一句话从语境中抽走,召回了也答非所问;块太大又会让语义被稀释。经验值是 200~500 字并叠加 10%~20% 重叠。就像把一本章回小说撕成单字,反而连不成故事。
为何常见:直觉上“小 = 精准”,忽略了语义完整性与向量表征的“平均化”效应。
误区表格不做结构化处理,直接把所有单元格文字拼成一堆文本,觉得“字都在就行”。
正解表格的行列关系承载语义:纵向是字段,横向是记录。纯文本拼接会让“金额”和“日期”丢失对应关系,大模型无法还原。应还原为 Markdown/HTML 或结构化记录,就像把相册的玻璃擦干净再运输,而不是把照片抽出来揉成一团。
为何常见:人在视觉上“看得懂”表格,误以为机器也拥有同样的视觉先验。
误区把 PDF 内容流的存储顺序当成阅读顺序,解析完直接分块,结果双栏论文左右串行。
正解PDF 内部对象顺序和阅读顺序无关,双栏论文会左右串行,页眉页脚会混进正文。必须按坐标做版面恢复:双栏先左后右,过滤每页重复的页眉页脚。否则向量块里全是乱序文字,检索语义严重受损。
为何常见:打印出来的文档是通顺的,误以为文件内部的数据顺序也是通顺的。
误区找一个宣称“什么文档都能解析”的库,希望一套代码吃遍所有 PDF、Word。
正解没有万能工具。文本 PDF、表格 PDF、扫描件、Word、复杂版式各自的最优解不同,且都需要质量抽检闭环。工业级管线是“多工具按需路由 + 异常兜底 + 人工抽检”,就像搬家公司对不同的箱子用不同的工具,并全程记录破损率。
为何常见:工具库的宣传话术加上初期小样本“看起来都能读”,让开发者低估了真实文档的复杂度。
面试题
Q1. RAG 系统中,文档解析环节的直接产出是什么?
- A.向量数据库的索引条目
- B.大模型的最终回答文本
- C.切分好的文本片段← 正确答案
- D.PDF 的原始字节流
答案:C。文档解析把非结构化文档改造成可供下一步分块与向量化的文本片段。索引条目是后续向量化与建索引的产物,最终回答是生成器的工作,原始字节流是输入而非产出。
Q2. 关于 PDF 格式本质,下列说法正确的是?
- A.PDF 内部就是纯文本流
- B.PDF 是带坐标的打印格式← 正确答案
- C.PDF 与 Word 完全等价
- D.PDF 自带段落大纲结构
答案:B。PDF 的字符是“画”在页面坐标系上的图形,没有段落与大纲语义,所以必须解析。它不像 Word 那样有结构化的 XML 标签,这也是 PDF 解析更费劲的原因。
Q3. 从表格密集的报表 PDF 中抽取结构化数据,首选哪种方案?
- A.直接读取原始字节流
- B.使用 pdfplumber 解析← 正确答案
- C.使用 JSON 解析器处理
- D.使用正则匹配全部文本
答案:B。pdfplumber 擅长检测表格线与单元格边界,能还原行列结构。原始字节流是压缩数据,JSON 解析器与 PDF 无关,正则只能处理纯文本、无法还原表格网格。
Q4. 用 python-docx 解析 Word 时,保留“标题样式”信息的作用是什么?
- A.让控制台打印更美观
- B.识别层级辅助语义分块← 正确答案
- C.让文档文件体积减小
- D.自动给文档生成目录
答案:B。Heading 1/2/3 是天然的章节分隔信号,分块时按标题下刀能保持语义完整。字体美观、文件体积和目录生成都不是解析阶段的核心目标。
Q5. 对双栏排版的论文 PDF,恢复版面读取顺序的正确做法包括哪些?
- A.按 x 坐标聚类识别栏← 正确答案
- B.直接按对象流顺序读取
- C.每栏内部按 y 坐标排序← 正确答案
- D.过滤每页重复的页眉页脚← 正确答案
答案:ACD。双栏恢复要先按 x 聚类分栏,再对每栏按 y 排序,最后左栏接右栏;同时要剔除固定位置的页眉页脚。直接按对象流顺序读会左右串行,是错误的。
Q6. 分块时保留重叠窗口 "overlap" 的主要目的是什么?
- A.减少向量库的总块数
- B.让切缝两侧语义不断裂← 正确答案
- C.加速文档解析的速度
- D.去掉页眉页脚噪声
答案:B。切块的缝隙可能切断一句话或一个论点的上下文,重叠让相邻块共享一段文字,保证语义衔接。它不会减少块数和加速解析,与页眉页脚过滤也无关。
Q7. 处理扫描版(图片型)PDF 的合理方案包括哪些?
- A.直接按文本流抽取方式
- B.先 OCR 再进解析管线← 正确答案
- C.抽取页面图像供多模态模型← 正确答案
- D.对整册文档直接跳过处理
答案:BC。扫描件的“文字”在像素里,必须先 OCR 还原文本,或抽取页面图像交给多模态模型理解。直接按文本流抽取只能得到空字符串,整册跳过会丢掉信息。
Q8. 文档解析质量评估可以采用的量化手段包括哪些?
- A.抽样后人工校对抽取文本← 正确答案
- B.统计文本覆盖率指标← 正确答案
- C.端到端检索召回率对比← 正确答案
- D.比较 PDF 文件的大小
答案:ABC。人工抽校看阅读质量,覆盖率衡量字符遗失比例,端到端召回率衡量解析对最终检索的影响,三者构成闭环。文件大小与解析质量没有直接关系。
Q9. 已知文档长度 L、块大小 s、重叠 o,若增大 o,则分块数 N 会怎样变化?
- A.随着重叠变大而变大← 正确答案
- B.随着重叠变大而变小
- C.与重叠大小完全无关
- D.先变小再恢复正常
答案:A。由公式 N = ceil((L-o)/(s-o)),增大 o 会让分母 s-o 变小,分子 L-o 变化相对较小,总体块数通常增加。重叠越多,同一内容被更多块覆盖。
Q10. 文档解析在 RAG 全流程中的位置是?
- A.生成回答之后汇总阶段
- B.向量检索之前的地基← 正确答案
- C.嵌入计算完成后的尾处理
- D.与向量检索完全无关
答案:B。文档解析位于检索之前,负责把原始文档变成可分块、可向量化的文本;它决定“目录里有什么书”。生成回答和嵌入计算都在它之后进行。