我把一份 300 页的 PDF 拆成单页再原样合并回去,文件从 141,760 字节涨到 195,838 字节,涨了 38.1%,耗时 2884 毫秒。同一批文件抽文本只要 41 毫秒,换成扫描件再抽,返回 0 个字符。这两个数字是我把 PDF 处理工具链推倒重配的起点。
先给结论:别指望一个工具吃下四件事。按"改的是哪一层"分层——结构层用 qpdf,文本层用 pypdf,识别层用 OCRmyPDF 配 Tesseract,转格式与归档交给专职工具。下面是我换了三套方案才跑顺的过程。
大家通常怎么做
多数团队第一步都一样:找一个"PDF 全能工具箱"。拆分上一个在线站,合并换另一个,要 OCR 再开第三个。三步操作看着很轻——上传、点按钮、下载回来,跟站里另一篇讲监控工具的话术是一个模子:输入链接、设置间隔、点击监控。
我最早也是这套。前两周跑得好好的,直到接了个批量任务:三百多份合同要按章节拆开、给扫描件补可搜索文本、再按客户合并回去。在线站一个个点,点了两天;更麻烦的是有的站把文件留在了人家服务器上,法务直接叫停。
后来换成"一个库包打天下",pip install 一个 PDF 库,指望拆分合并 OCR 全在它里面。走得更远一点,但同样不通。
为什么这条路走不通
不是我选的库不行,是定位上就不成立。两个主力工具的官方 README 各自把话说得很明白:
qpdf 的 README 原文是"content-preserving transformations",并且直接声明它不渲染 PDF、不做文本抽取,是处理 PDF 文件结构的底层工具。pypdf 的 README 则把自己描述为一个纯 Python 库,能力清单是拆分、合并、裁剪、变换、加密码、取文本和元数据——没有 OCR。
也就是说,这两个最常用的工具,一个明说不抽文本,一个压根没列 OCR。指望任何一个包下四件事,从设计目标上就是反的。全能工具箱能做到,是靠把渲染、识别、转换全堆进去,代价是不可控的二次加工——你拿到的 PDF 已被重造过一遍,不再是最初那份。
还有个更隐蔽的坑:在线站和"重渲染"类工具本质是把页面栅格化再重建,原始矢量信息一旦没了就补不回来。有次拿转换后的文件核数字,小数位对不上,查了半天才发现重排时丢了个字符,返工两轮。
换个思路:按"改的是哪一层"分工具
把 PDF 想成四层,每层只让一个工具负责:
- 结构层(拆分、合并、旋转、加解密)——qpdf,命令行,Apache-2.0,改页面组织,不动内容本身。
- 文本层(抽文本、读元数据、按页拼装)——pypdf,纯 Python,写胶水最顺手。
- 识别层(扫描件变可搜索)——OCRmyPDF,底层调 Tesseract,官方 README 写的是 MPL-2.0,商用可以,改源码要公开那部分修改。
- 转格式 / 归档层——PDF/A 走 OCRmyPDF 的
--output-type,转图片、压缩另找专职工具。
判断标准一句话:能在结构层解决的,别下沉到像素层。拆分合并在结构层是"搬页",内容一个字节没动;一旦走截图或重渲染就变成"造页",体积和失真全部失控。前面那个 38.1% 的膨胀,就是没守住这条线的代价。
划边界比选工具重要,站里讲工具选型那篇也是这个思路:先定归谁,再谈用哪个。
具体做法
第一步:拆分与合并交给 qpdf
拆成单页:
qpdf --split-pages in.pdf out_%d.pdf 合并,还能顺手挑页:
qpdf --empty --pages a.pdf b.pdf 1-5 -- merged.pdf 有个必须写进脚本的细节:qpdf 官方手册给的退出码是 0 成功、2 出错、3 有警告但已恢复,并特别注明它永远不会返回 1——拿到 1 说明 shell 根本没找到这个程序。所以判失败要写"不等于 0",只判 == 2 会漏掉带警告那批。
跑完统一报一遍"哪些页失败、影响几页",把退出码透出去让上层能感知失败,批量处理的容错设计里也讲过,这条链同样适用。
第二步:先抽一次文本,再决定要不要 OCR
from pypdf import PdfReader
reader = PdfReader("scan.pdf")
text = reader.pages[0].extract_text() 实测文字页能抽出 48 个字符,纯图扫描页抽出 0 个字符。这个空值就是 OCR 开关——先抽一次,抽空了再送 OCR,别一上来就把整批文件喂给识别引擎。判断"该不该上 OCR"成本是毫秒级,判断错了成本是几十分钟。
踩过一个坑:带 AES 加密的 PDF,光装 pypdf 不够,要按官方 README 装 pip install pypdf[crypto],否则解密那步直接报错。
第三步:OCR 用 OCRmyPDF
ocrmypdf -l chi_sim+eng --jobs 4 --deskew --sidecar out.txt input.pdf output.pdf 官方口径的几个要点:默认输出 PDF/A 归档格式;语言包要单独装(Debian/Ubuntu 是 apt-get install tesseract-ocr-chi-sim),中文不装就只认英文;--sidecar 把识别文本单独落一份 txt 方便建检索;--deskew 先纠偏再识别。
依赖上,官方 README 明确写了除 Python 外必须另外安装 Ghostscript 和 Tesseract。Windows 原生支持,不用非走 Docker;PATH 里找不到 tesseract 时按注册表里的最高版本选。效果不够可换官方插件列表里的 OCRmyPDF-PaddleOCR,官方建议配 GPU。
第四步:用 pypdf 当胶水串起来
拆页 → 逐页抽文本 → 空页送 OCR → 按目录合并。每步都能单独重跑,失败了从当前节点续。
效果对比
本机实测:Windows / Python 3.13 / pypdf 6.20.0 / reportlab 5.0.1,样本是脚本生成的合成 PDF,不是真实业务文件,百分比只用于同批横向比较,不能直接套用。
| 页数 | 拆分耗时 | 合并耗时 | 合并后体积膨胀 |
|---|---|---|---|
| 30 页 | 118 ms | 458 ms | 32.7% |
| 100 页 | 315 ms | 1018 ms | 36.8% |
| 300 页 | 1095 ms | 2884 ms | 38.1% |
三个结论:
拆分比合并便宜得多。合并耗时是拆分的 3–4 倍,且随页数近似线性增长。批量流程要把拆分结果缓存下来,别每次重跑。
膨胀率稳定在 33%–38%,不是页数越多越离谱。这是每页资源字典被复制一份的固定开销。要压回去就在合并后单独跑压缩或优化,别指望合并顺手瘦身。
抽文本是整条链最划算的一步判断。41 毫秒就能定这页值不值得花几十秒去 OCR。我第一版不做判断、整批送识别,白烧了几个小时。
手上是大文件拆分,可以看 C# 大文件拆分的实现思路;要把扫描件里的表格还原成结构化数据,接着看 字段级抽取和整表还原怎么选;来源是网页截图的话,先读 为什么不建议一上来就 OCR 网页截图,能省掉一整轮返工。
常见问题
在线转换站到底能不能用?
能用,但只适合一次性、不涉密、页数少的活。三个硬伤:文件要上传到对方服务器;批量要排队;同一文件两次转换结果可能不一致,流程没法复现。进了自动化就得换成本地命令行工具。
合并之后文件变大了,正常吗?
正常。实测膨胀 33%–38%,来源是每页的资源字典被复制了一份。要压回去就在合并后单独跑压缩或图像优化,qpdf 本身提供结构检查与流压缩相关选项,具体参数以官方手册最新说明为准。
中文扫描件识别不出来怎么办?
先查语言包,OCRmyPDF 用 -l chi_sim,Tesseract 侧要装 tesseract-ocr-chi-sim。装了还是差,先别换引擎,开 --deskew 纠偏、提高扫描分辨率——预处理通常比换模型见效快。不行再考虑 OCRmyPDF-PaddleOCR。
采集来的 PDF 做批量处理有什么要注意的?
合规第一:只处理你有权处理的文档,遵守来源平台规则与相关法律法规,涉密或含个人信息的材料不要送任何在线服务。技术上要做进度可追踪、失败可重试、中间结果落盘,别让一个坏文件把整批跑空——这套容错思路在别的批量场景同样成立。
版本号会不会很快过时?
会。这三个工具都在持续更新,实测数据只代表本次环境。动手前请回各自官方仓库与文档站复核命令、参数与版本要求,以仓库最新说明为准。

