如果你正在做 RAG,尤其是知识库的文档预处理,PDF 十有八九让你头疼。最近我把一个叫 pdf-inspector 的小工具接进了处理管道,专门用来给 PDF 做“体检”——判断哪些文件可以直接抽文本,哪些需要 OCR,哪些里面藏着大量表格,哪些压根就是扫描件。用了几周下来,很多以前靠肉眼和试错解决的问题,现在一条命令就能拿到答案。这篇文章聊的不只是工具用法,还有我在真实项目中怎么用它来优化 RAG 的全流程,包括动态分块、批量筛选,以及和 agentic RAG 的配合。适合正在做私有知识库、文档问答,或者刚接触 RAG 想少走弯路的同学。
1. 为什么 RAG 流程需要一份 pdf-inspector 体检报告
1.1 被 PDF 折磨过的 RAG 工程师都懂
如果你搭建过基于文档的 RAG 问答系统,一定遇过这种场景:用户上传了一批 PDF,你兴冲冲喂给解析器,第 1 个文件抽出来是整齐的文本,第 2 个文件抽出来全是乱码,第 3 个文件看起来很正常,但检索阶段怎么都命不中正确答案。排查半天,发现 PDF 里有多栏排版,文本被按阅读顺序完全拆乱了。这类问题非常普遍。
PDF 本身是“为打印而生的”,它记录的是每个字符、每条曲线的位置,而不是语义段落。所以你在屏幕上看到一个完好的段落,底层可能是无数个散碎的 text operator 拼出来的。更麻烦的是,扫描件 PDF 压根没有文本层,整页就是一张图片。还有的 PDF 里 70% 内容是表格,表格被解析成一行行碎片后,原本的列对应关系全部丢失。这些情况不统一解决,RAG 的检索命中率就是靠天吃饭。
pdf-inspector 要解决的,就是把“这个 PDF 能不能直接抽文本”“里面有没有图片”“有没有表格”“是不是扫描件”这些问题变成一份结构化报告。它不负责提取和解析,只负责在解析之前告诉你:这份文档是什么样子的。听起来很简单,但在流程里非常值得。
1.2 pdf-inspector 在管道里的定位
在常见的 RAG 管道里,文档进来后一般是:加载 -> 解析 -> 清洗 -> 分块 -> 向量化 -> 入库。大多数开源方案把“解析”这一步做成了各种解析器,比如 pypdf、pdfplumber、unstructured、OCR 工具。但每一步的适用场景完全不同,如果盲目选择,轻则浪费算力,重则污染知识库。
pdf-inspector 的定位正好卡在“加载”和“解析”之间。你可以把它理解成 PDF 的预检员,或者说“体检科”。它先扫一眼文档的结构,然后输出一个 JSON 报告:一共多少页、每页文字多不多、图片面积占多大、有没有表格线、有没有字体嵌入、文档是不是加密的、有没有文本层。根据这些指标,你再决定后续走哪条解析路线。
我实际用下来,最大的好处是流程变得可视、可追踪。以前同事问我为什么某个 PDF 解析效果这么差,我只能说“这个文件比较特殊”。现在我可以直接甩一份报告:第 3-6 页没有文本层,应该是扫描图;第 8 页表格占比超过 60%,建议单独走表格解析。整个团队的沟通成本降了不少。RAG 项目里最怕的就是黑盒处理,pdf-inspector 恰好把黑盒撬开了一条缝。
2. pdf-inspector 的核心理念:不解析,只体检
2.1 为什么叫 inspector,而不是 parser
刚看到这个名字时,我想的是“PDF 检视器”之类的东西。用下来才明白,“inspector”这个词特别准确。parser 的目标是把 PDF 转成结构化的纯文本或 HTML,而 pdf-inspector 的目标只是“看”,把你需要的元信息和统计特征拿出来。它不会尝试重组段落,不会输出清洗后的正文,更不会改掉原文件。这种克制的设计在工程上很明智。
现在的 PDF 解析生态已经足够丰富,pypdf、pymupdf、pdfplumber、camelot、unstructured、OCR 工具各有强项。pdf-inspector 如果自己也去写一套解析器,只会重复造轮子。它要做的是反客为主,帮你在这些解析器之前做决策。就好比你去看病,pdf-inspector 是医生开出的“检验报告”,真正动刀子还是得由各个专项工具来干。
这也决定了它非常轻量:不需要加载庞大的模型,不需要 GPU,纯粹靠规则和元数据分析 PDF 就能跑完。对一些只有十几个页面的 PDF,几十毫秒就能出报告,速度上完全能接受。
2.2 报告里的关键指标
我用的这个版本,报告会输出下面这些核心字段,我把它们和 RAG 的关系整理成了表格:
| 指标 | 说明 | 对 RAG 的影响 |
|---|---|---|
| page_count | 总页数 | 判断文档规模,决定是否抽页处理 |
| text_chars_per_page | 每页文本字符数 | 太少说明可能是扫描件或纯图文档 |
| image_area_ratio | 图片面积占比 | 高占比页面可能需要 OCR 或多模态模型 |
| table_bbox_count | 检测到的表格区域数量 | 有表格时建议走表格还原通道 |
| has_text_layer | 是否存在可提取文本层 | false 时必须 OCR |
| column_hint | 多栏版面的启发式标记 | 多栏文本需要版面还原或阅读顺序修复 |
| font_embedded | 是否嵌入了字体 | 影响个别字符能否正确解码 |
| encryption | 加密/权限状态 | 某些库无法读取加密 PDF,需要先解锁或允许文本提取 |
这些指标里,text_chars_per_page 是最直观的分水岭。一份正常的文字版 PDF,每页字符数通常在 800 到 2500 之间;如果是扫描件,这个数字基本是 0。image_area_ratio 则能告诉你页面上图片占了多大面积。如果一个 PDF 文字字符数不少,但图片面积也很大,那说明它可能是图文混排,或者是文字嵌在图片里,这些页面需要单独处理。
举个例子,我之前处理过一批产品说明书,报告显示 text_chars_per_page 是 400 到 600,不算低,但 image_area_ratio 超过 60%。后来抽查了一个页面才发现,正文其实是图片上的文字,那些“文本”其实是标题和页码。真正的产品参数全在图片里。如果只按文本分块,知识库会丢失大半信息。这份报告直接帮我避开了坑。
2.3 自定义检查规则
pdf-inspector 还支持用配置文件把某个指标作为“规则”来使用。比如你可以设定一个阈值:当某页的 text_chars_per_page 低于 100 且 image_area_ratio 大于 0.5 时,就把这一页标记为“疑似图片页”;当 has_text_layer 为 false 时,把整篇标记为“需要 OCR”。这样报告就不只是一堆数字,而是带上了业务标签,下游流程可以直接根据标签做路由。
这种设计让我想起了很多检测框架里的规则引擎。它没有硬编码地替你做决定,而是给你一组可编程的开关。你可以针对不同的文档类型写不同策略。比如合同类 PDF,表格检测权重更高;论文类 PDF,多栏检测更重要。把规则写在配置文件里,结合 CI 流程还能做回归测试,不至于今天调好一个文档,明天又坏了另一个。
当然,自定义规则要克制。我看到过有人在规则里写了几十条复杂条件,结果维护成本比解析器本身还高。我的建议是,按照“必做-选做-不做”三层来规划规则:必做的是扫描件判断、文本层判断;选做的是表格、多栏检测;不做的是需要语义理解的判断,例如判断该文档是否属于某个业务分类,那应该交给下游语言模型,而不是规则。
3. 实操过程:从安装到读懂一份报告
3.1 安装与基础用法
先聊最简单的安装。它是一个 Python 包,在 Python 3.9 以上的环境里可以直接 pip 安装:
pip install pdf-inspector装完后命令行就有了 pdf-inspector 命令。最简单的用法是直接扫一个文件:
pdf-inspector inspect docs/产品说明书.pdf默认会在终端打印一份人类可读的摘要,含总页数、每页的平均字符数、图片数量、是否有文本层、是否加密等信息。如果你像我一样想要拿到结构化结果,加一个 --format json 参数:
pdf-inspector inspect docs/产品说明书.pdf --format json --output report.json这样得到一份完整的 JSON 报告,后续无论是写 Python 脚本还是用 jq 做后处理都很方便。如果你有一堆文件要批量扫,它支持目录模式:
pdf-inspector inspect ./docs/ --recursive --format json --output batch_report.json它会递归遍历目录下所有 PDF,并把每个文件的报告合并到一个批次文件里。这个功能在我后文讲批量筛选时非常好用。
3.2 命令行输出体验
为了让你更有画面感,我举个例子。假设我现在有一个 12 页的扫描版 PDF,运行 pdf-inspector 命令后,终端大概会输出这样一段内容(具体字段可能有版本差异,思路一致):
File: docs/扫描版合同.pdf Pages: 12 Encryption: none Text layer: NOT_DETECTED (0 chars on page 1-12) Images: 24 detected, area ratio avg 0.93 Tables: 0 table regions detected Column hint: single column Verdict: 需要 OCR这个输出最关键的几个点是 Text layer 和 Verdict。如果文本层检测为 NOT_DETECTED,就说明 PDF 的底层是图片,直接抽文本只能得到空白;24 张图片和 0.93 的面积比进一步印证了这一点。下游接到这个报告,路由到 OCR 服务就是顺理成章的事。
同样,如果是一个正常的 Word 转 PDF,报告会显示 text_chars_per_page 在 1500 左右,has_text_layer 为 true,Verdict 是“可直接提取文本”。这种一目了然的结果,比你自己打开 PDF 翻一翻再猜测要可靠得多。
3.3 读懂报告里的每一项
很多第一次用 pdf-inspector 的朋友,容易忽略一些小字段。我这里挑几个容易误读的来说。
text_chars_per_page 是按页面字符总数统计的。注意,它统计的是能抽取出来的字符数,并不代表所有字符都是有效内容。有些 PDF 的页眉页脚、页码也会算进去,所以当你看到某页只有 50 个字符时,不需要过度紧张,可能该页就是过渡页或封面页。但如果连续几页都只有几十个字符,且图片面积很大,那基本就是在告诉你:正文在图片里。
table_bbox_count 检测的是基于线条和矩形边框的表格区域。对于有边框的表格,检测准确率很不错;对于无边框的表格(比如只有空格和大字段的表格),它可能会漏掉,因为没有任何几何线条可以作为依据。这并不代表工具不好,表格检测本身就是难题。我一般把 table_bbox_count 当成参考指标,需要结合业务情况判断。
font_embedded 和加密状态是另一个容易被忽略的细节。如果 PDF 没有嵌入字体,某些字符可能提取出来是乱码或空白。pdf-inspector 会标记出来,提醒你用支持字体子集化处理的解析器,或者干脆转成其他格式。加密状态则关系到解析器能不能打开文件,有些 PDF 设置了“仅允许打印”的权限,pypdf 等库在处理时可能抛出异常。
这些细节在第一次跑报告时可能用不上,但你不懂的话,遇到问题很容易误会是解析器的 bug。我在项目里就踩过这种坑。当时一个 PDF 用 pypdf 死活抽不出中文,我一度以为是编码问题,结果用 pdf-inspector 一看,font_embedded 是 false,缺少中文字形。后来换了一个能识别字形替代的引擎,问题就没了。
3.4 用报告确定处理策略
读懂报告之后,真正要做的是制定一个决策表。我自己的流程大致是这样:
| 报告特征 | 处理策略 |
|---|---|
| has_text_layer=true 且 text_chars_per_page>800 | 直接用常规文本解析器 |
| has_text_layer=false | 走 OCR 流程 |
| has_text_layer=true 但 column_hint=multiple | 先做版面分析,还原阅读顺序,再分块 |
| table_bbox_count>0 且表格占比高 | 单独用表格还原库提取表格 |
| image_area_ratio>0.6 且有大量自然图片 | 评估是否用多模态模型向量化图片内容 |
| encryption 标记 | 先解除限制或允许文本提取,再走正常流程 |
有了这张表,预处理流程就从一个黑盒变成了清晰的 if-else 路由。我甚至可以把这些规则写成一个小脚本,在进入正式解析之前,先用 pdf-inspector 跑一遍,根据报告的标签选择对应的处理器。
有一点要提醒:上面每个策略的阈值都要结合你自己的语料去标定,不要直接照抄我的数字。我处理产品说明书时,图片判断阈值是 0.6;但如果你在做个税发票,这个阈值可能会更高。最好先抽样 20 个不同来源的文件,跑一遍报告,再人工核对一遍结论,调好阈值后再全量铺开。
4. 常见问题与排查实录
4.1 扫描件误判为普通 PDF
我遇到最多的误判情况,是某些扫描件其实带了“隐形 OCR 层”。很多扫描设备会在底层图像上叠加一层不可见的文字,这样 PDF 可以被搜索引擎检索,但你直接看到的是一个干净扫描图。pdf-inspector 的 has_text_layer 会返回 true,但 text_chars_per_page 可能只有几十个字符。
这种文件很迷惑人,因为文本层存在,但内容稀疏且可能错位。如果直接抽文本,你拿到的是残缺的“影子文字”,检索效果很差。我的排查方法是:把 has_text_layer=true 且 text_chars_per_page 低于 300 的页面挑选出来,用可视化模式看一下页面上文本层的字符坐标,和明显扫描背景对比。如果字符零散在页面上,且没有连贯语句,就按“伪文本层”处理,强制走 OCR。
pdf-inspector 有几个内置参数可以用来辅助判断,比如 --strict 模式会把“文本层存在但覆盖率极低”的情况单独标出来。我建议不要只看布尔值,要结合文本字符数一起看。任何纯布尔判断,面对真实世界的 PDF 都会踩坑。
4.2 表格区域识别不准
有边框表格一般没问题,但遇到无边框表格,也就是用空格和缩进排列的所谓“表格”,pdf-inspector 会识别不到。这类表格在合同中很常见,比如“甲方:XXX,乙方:XXX”。它们的每行看起来是文本,但实际是多个字段。
处理这类问题,我现在会先运行 pdf-inspector,如果 table_bbox_count 为零,但文本字符数正常,我还是会再抽样几个页面看一眼。有的 PDF 页面虽然是纯文本,但实际逻辑上是一个个迷你表格,这种文件直接做纯文本分块,问“甲方是谁”时,答案很容易被拆开。
在这种情况下,我把规则表里加了一条:如果某页文本字符数相对稳定,但每行平均长度变化剧烈,就可能是无边框表格。这个判断没法由 pdf-inspector 直接给出,需要结合正则或者文本分布来补充。工具不可能替你解决所有版式问题,它帮你定位问题,剩下的还是要靠业务逻辑。
4.3 加密 PDF 带来的坑
加密 PDF 是另一个高频坑。我之前遇到过一份 PDF,pdf-inspector 报告显示 encryption 为 owner_password_set,意思是文档设置了“所有者密码”权限,限制了复制、打印等操作。大多数解析库遇到这种文件会直接报错,或者只能提取到受限的内容。
经验是先尝试用空密码打开,因为很多加密的 PDF 只是设置了权限加密,并没有真正的用户口令。python 里可以用 pypdf 的临时函数或 pdf-inspector 的 --open-password 参数直接传入密码。如果真的有密码,就没有什么魔法快捷方式了,需要让上传者解锁,或在流程里明确告知用户该文件需要手工处理。
另外,某些 PDF 虽然加密,但文本层仍然存在,直接看 has_text_layer 可能为 true。问题是多数解析库一上来就会发现加密,导致你还没碰到文本就中断了。所以 pdf-inspector 的检查步骤必须跑在解析器之前,否则你会在异常日志里翻半天。
4.4 与其他解析器配合时的顺序问题
pdf-inspector 不是万能的,它只是预检。我在实际项目里总结出一个相对稳定的接法:先跑 pdf-inspector,再根据报告选解析器,最后再做清洗分块。但要注意,解析器选型顺序也很讲究。同一个 PDF,pymupdf 和 pdfplumber 的提取结果可能差异很大,特别是在有复杂表格时。我一般先按报告中的页面类别,把不同类型的页面拆开,再分别交给适合的工具,而不是整篇文件只用一个工具处理到底。
简单画一个流程(这里直接写文字):
- 步骤一:pdf-inspector 生成报告;
- 步骤二:按报告把页面分为文本页、图片页、表格页、混合页;
- 步骤三:文本页用 pymupdf 提取纯文本;表格页用 pdfplumber 或 camelot 提取表格结构;图片页调用 OCR;混合页先做版面切分再走上面流程;
- 步骤四:合并所有结果,按报告中的阅读顺序拼接,再进入分块。
这样的好处是每类页面都能用上最适用的工具,代价是流程复杂度上去了。早期我们图简单,全用统一解析器,结果文档类型一多,质量马上下降。后来拆开后,检索命中率提升了不少。如果你刚开始接触 RAG,可以先忽略步骤二,等到你确定目标语料里有大量混合型 PDF,再上这套方案也不晚。
5. 真实项目里的应用经验
5.1 用报告驱动分块参数
RAG 里另一个让人头疼的问题是分块。块太大容易夹杂不相关内容,块太小容易丢失上下文。pdf-inspector 的报告能帮我在分块前从文档层面预判:如果一个 PDF 平均 text_chars_per_page 高,说明这份文档篇幅紧凑,我可以适当调大 chunk_size,比如 1000 字符;如果一个 PDF 里大量页面是图片占比高的图文页,说明文档信息密度分布不均,更适合用小块,或者把页面本身作为候选块。
我甚至在脚本里直接读取报告的 text_chars_per_page 平均值,动态计算 chunk_size 和 overlap。规则不复杂:
avg_chars = report["avg_text_chars_per_page"] if avg_chars > 1200: chunk_size = 1000 overlap = 200 elif avg_chars > 400: chunk_size = 700 overlap = 150 else: chunk_size = 500 overlap = 100这个动态策略明显改善了检索召回。原因不复杂:均匀的文档用大块更完整,非均匀的文档用小块能提高命中粒度。我建议你把自己手里的语料跑一遍,看这个均值分布,再决定用什么公式,不必照搬。
5.2 批量体检:自动化筛选知识库来源
批量处理是 pdf-inspector 非常实用的功能。你只需要一条命令,就能把一个目录里所有 PDF 的报告汇总成 JSON。配合 jq 或者 Python,你可以快速做很多清洗工作。比如,我可以直接筛出所有 has_text_layer=false 的扫描件,算一下占比;也可以按 image_area_ratio 排序,把图片最多的文件排在前面,优先人工审查。
我在一个内部知识库里做过一次统计,600 多份 PDF,其中 82 份是扫描件或带有大量图片,占比超过 13%。这在处理之前根本不知道。有了这个分数,我们让运营同事先手工挑选出 20 份最需要精细处理的文档,优先做 OCR 方案试点。剩下的正常文档直接进常规流程。整个上线周期缩短了很多。
在处理批量文档时,我建议把报告输出到一个固定的目录,按日期归档。这样哪天有人突然问“之前那批合同为什么解析效果差”时,你可以直接找到当时生成的报告,对照着解释,完全没有推诿空间。
5.3 与 agentic RAG、GraphRAG 的结合思路
最近社区里 agentic RAG、GraphRAG 概念很热。它们本质上都在解决传统 RAG 的“知识割裂”问题——知识被拆成互不关联的小块,检索时缺乏全局关系。pdf-inspector 在这种新架构里也能派上用场。
比如在做 agentic RAG 时,agent 需要先判断文档类型、决定调用哪些工具。如果 pdf-inspector 把 PDF 的体检报告作为文档元数据一并存入知识库,agent 在检索到文档时,可以直接看到“该文档包含 5 个表格”或“该文档有 12 页扫描件”这类上下文,从而决定是调用表格解析工具还是 OCR 工具。这就是用元数据驱动 agent 行为的一种很自然的做法。
GraphRAG 和 ontology RAG 就更需要结构信息了。你可以在构建知识图谱之前,先用 pdf-inspector 识别哪些页面包含实体关系密集的表格,把表格区域单独抽取出来交给图谱构建模块。否则,实体关系如果混杂在纯文本里,抽取效果会打折扣。
还有人问“RAG 知识库能存储图片嘛”。这个问题我的理解是:纯文本 RAG 肯定存不了,图片内容必须通过 OCR 变成文本,或通过多模态模型转成向量。pdf-inspector 的 image_area_ratio 指标能帮你发现哪些文档里“图片其实是关键内容”,从而决定是否引入多模态向量化。如果一份 PDF 图像占比极低,那老老实实用文本管道就够了;如果图像占了大半,那你就要考虑多模态方案。这个判断,真的不用靠肉眼逐页翻。
聊这么久,说点个人体会。我把 pdf-inspector 放进 RAG 流程后,最大的感受是:工具本身不复杂,复杂的是你也终于敢承认“解析 PDF 就是一份需要被认真对待的脏活”。过去我总觉得解析器只要选个好的就行,实际上根本没有十全十美的解析器,只有合适的场景。pdf-inspector 没法帮你解析出完美的文本,但它能让你在动手之前,看清楚每一份 PDF 的真实模样。对做 RAG 的人来说,这种“先检查,再决策”的思路,比堆一堆模型工具都管用。如果你正在为 PDF 解析头疼,建议也先给文档做个体检,也许思路一下就打开了。