☰
Python处理PDF全攻略:从库选型到OCR与踩坑实战
2026/10/8 4:13:29 网站建设 项目流程

前阵子接手一个批量处理标书的活儿,几百份PDF要提取关键信息、合并盖章、转档分发,我第一反应是用Python快速搞定。结果一上手才发现,PDF这玩意儿看着人畜无害,实际处理起来坑多到让人怀疑人生:有的库提取中文乱码,有的表格一跨页就断,有的扫描件压根没字符层。折腾了几天,把主流的处理方案捋了个遍,也踩了不少雷。这篇就把我的实际经验摊开来讲,从库选型到具体操作,再到那些文档里从来不写的老坑,一次性说清楚。

1. 先选对库:PDF处理的工具矩阵与选型逻辑

处理PDF第一件事不是写代码,而是想清楚你到底要干嘛。PDF这个格式特殊在它是个"最终态"文档——设计目标就是无论什么设备打开都长得一模一样,所以它内部存的是精确的坐标、字体、图形对象,而不是像Word那样存段落和样式。这意味着你想"编辑"PDF,本质上是在改一堆绘制指令;想提取内容,就得从绘制指令里反推文本流和结构。不同库在这件事上的侧重点完全不同。

我用下来,主流方案基本分成四类:

库名定位强项短板
pdfplumber内容提取文本、表格提取精度高速度偏慢,不适合大型文件
pypdf / PyPDF2文档操作合并、拆分、加密、旋转等轻量操作文本/表格提取能力弱
PyMuPDF(fitz)全能选手极快,能渲染图片、提取文字、操作页面API风格偏底层,需要适应
ReportLab文档生成从零生成PDF,页面控制精细纯代码画图,中文处理要额外配置

你可以简单理解成:想从PDF里"拿东西"用pdfplumber,想对PDF做"手术"(合并拆分加密)用pypdf,追求速度和批量渲染就用PyMuPDF,要"造"一个全新PDF就用ReportLab。实际项目中经常是混合用。

环境准备也比较直接,直接用pip装就行:

pip install pdfplumber pypdf pymupdf reportlab pdf2docx

我建议把这几个都装上,PDF处理几乎没有只用单一库就能完美解决的问题。比如我最近一个项目就是pdfplumber提取表格,pypdf做拆分,PyMuPDF把扫描页渲染成图片再OCR,各干各的活,互不干扰。

2. 内容提取实战:文本、表格与扫描件识别

2.1 文本提取:pdfplumber的常规操作与乱码应对

提取文本是PDF处理里最基础也最常用的需求。用pdfplumber提取文本非常简单,核心就几行:

import pdfplumber with pdfplumber.open("demo.pdf") as pdf: for page in pdf.pages: text = page.extract_text() if text: print(text)

extract_text()会把当前页面上能识别出的文字对象按阅读顺序拼成字符串。但这里有个很多人第一次用就会困惑的点:返回的text可能是None。这不是BUG,而是这个页面根本没有文本层,比如是扫描件,或者是某种特殊编码,文字是用绘图指令画上去的曲线。遇到这种情况别死磕这一个库,直接换思路。

另一个高频坑是中文乱码。pdfplumber本身对中文PDF的提取能力其实不错,但有些PDF(尤其国产软件生成的,或者从WPS、方正排版系统导出的)字体编码不规范,提取出来全是乱码或空白。我的经验是用PyMuPDF兜底:

import fitz # PyMuPDF doc = fitz.open("demo.pdf") for page in doc: text = page.get_text() print(text)

这两个库提取文本走的是完全不同的技术路径:pdfplumber会去解析PDF的字符流和字体信息,再按坐标重组文字;PyMuPDF是直接读取内容流里的文本对象。所以一个提取失败时,另一个往往能救回来。实测中乱码PDF用PyMuPDF的成功率明显更高,缺点是有时会把同一段文字拆得零零碎碎,需要自己拼一下。

2.2 表格提取:从PDF里挖出结构化数据

表格提取是PDF处理里真正有技术含量的事。PDF本身没有"表格"概念,它只知道自己画了哪些线条、在哪放了哪些文字。pdfplumber之所以表格提取强,是因为它会利用页面上的线条和文字位置,重建网格结构,再按行列把文字塞进去。

import pdfplumber with pdfplumber.open("table.pdf") as pdf: page = pdf.pages[0] tables = page.extract_tables() for table in tables: for row in table: print(row)

返回的结构是一个嵌套列表,每个table代表一张表格,每行又是一个列表,单元格内容可能是字符串也可能是None(空白单元格)。拿到这个结构后,要存成Excel、CSV或者写入数据库都很容易:

import csv import pdfplumber with pdfplumber.open("table.pdf") as pdf: page = pdf.pages[0] table = page.extract_tables()[0] with open("output.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerows(table)

实际处理表格时最磨人的不是提取本身,而是表格样式不规整。比如没有完全封闭的框线、表头跨页、单元格里换行等。我的做法是先只提取一页做可视化验证——把提取出来的表格数量、行列数、是否有空行打出来看看,再决定要不要调extract_table()的table_settings参数。比如对付那些线不完整的表格,可以让它只依赖文字位置推断网格:

table = page.extract_table({ "vertical_strategy": "text", "horizontal_strategy": "text", })

意思是不用线定位,而是根据文本的列对齐关系来切分。对付特殊的表格,这个参数能救命。

2.3 扫描件使用OCR:PyMuPDF配合Tesseract

扫描版PDF本质上不是PDF,是一堆图片套了个PDF外壳。pdfplumber根本提取不出文字,因为压根没有文本层。这时候只能上OCR,我常用的组合是PyMuPDF渲染页面成图片,再用Tesseract识别。

import fitz import pytesseract from PIL import Image import io doc = fitz.open("scan.pdf") for page in doc: pix = page.get_pixmap(dpi=200) img_data = pix.tobytes("png") img = Image.open(io.BytesIO(img_data)) text = pytesseract.image_to_string(img, lang="chi_sim") print(text)

有几个值得注意的点。DPI建议至少200,太低了识别率明显下滑;太高了文件大耗时久,200-300是性价比区间。识别中文要装对应语言包,Tesseract默认只有英文,中文包需要单独下载,装好后lang="chi_sim"才能生效。另外,字迹清晰的正规扫描件识别率很高,但印章盖到文字上或者页面歪斜的,最好先做预处理,比如转灰度、去噪点,这些用PIL就能实现。

OCR这块我的建议是:不要追求完美识别每一个字,而是把目标定在"能把关键字搜出来"。很多场景下你不需要全文正确,只要能把合同编号、姓名、金额这类信息捞出来就够了。

3. 格式转换:PDF转Word与HTML生成PDF

3.1 PDF转Word:版式还原的可行性边界

PDF转Word可能是大家最常搜的功能。很多免费工具网站都能转,但批量处理、格式敏感的时候还是本地脚本更靠谱。Python这边我用得最多的是pdf2docx库,它内部是PyMuPDF解析页面布局,再用python-docx重建Word文档。

from pdf2docx import Converter cv = Converter("demo.pdf") cv.convert("demo.docx") cv.close()

几行就能完成转换,但转换质量完全取决于源PDF的复杂程度。纯文字型、单栏排版的PDF转换效果最好,能还原出段落和标题。让我吃过亏的是这些情况:

  • 多栏排版的论文PDF,转出来文本框移位频繁
  • 页眉页脚会被当作正文内容插入每段
  • 复杂表格转成Word后变成图片或乱排

说到底是因为PDF压根不记录"这是标题""这是正文段落"这类语义信息,转Word本质上是从几何坐标逆推文档结构,有天然上限。我的做法是:能用就用,批量转换前先抽查几页看看效果,没问题再跑全量。遇到排版精密的文件,转换完必须人工核对一遍,别直接拿转换结果交付。

3.2 HTML转PDF:让网页和报表直接落地成文档

反向的PDF生成需求也很常见,比如把报表页面存成PDF存档,或者把Markdown文档渲染成PDF发出去。这个方向我最喜欢用pdfkit,它是wkhtmltopdf的Python封装,可以直接把HTML字符串或URL转成PDF,CSS支持度还不错,页面样式基本能原样保留。

# 需要先安装 wkhtmltopdf sudo apt install wkhtmltopdf # Ubuntu brew install wkhtmltopdf # macOS
import pdfkit # HTML字符串转PDF html = "<h1>这是标题</h1><p>这是正文内容</p>" pdfkit.from_string(html, "output.pdf") # 网页URL转PDF pdfkit.from_url("https://example.com/report", "web.pdf") # 本地HTML文件转PDF pdfkit.from_file("index.html", "page.pdf")

这个方案最大的优势是排版能力。你想生成带样式的漂亮PDF,用HTML+CSS来写比用ReportLab一像素一像素调位置快得多,学成本和维护成本都低很多。我现在做给非技术同事看的自动化报表,基本都走这条链路。

一个实际经验:Python 3.10及以上版本有些环境直接pip装pdfkit会拉下来一个很老的版本,运行正常但功能不全。遇到问题可以先检查wkhtmltopdf是否在系统PATH里,很多时候"PDF转出来空白"的根源不是代码,而是这个外部工具没装好。

4. 文档编辑:合并、拆分、加密与加水印

4.1 合并与拆分:用pypdf处理批量文档

合并、拆分这种文档操作请求,技术含量不高,但架不住量大啊。几十份合同合并成一份提交,几百页的资料按页码拆出去,手工做能累死。pypdf写起来非常顺手。

批量合并:

from pypdf import PdfWriter, PdfReader merger = PdfWriter() for path in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]: merger.append(path) merger.write("merged.pdf")

append()可以直接传文件路径,也可以传PdfReader对象,后者可以做一些更细的控制,比如只append某个文件的一部分页面:

reader = PdfReader("big_file.pdf") merger.append(reader, pages=(0, 5)) # 只要第0到4页

按页拆分成独立文件:

from pypdf import PdfReader, PdfWriter reader = PdfReader("large.pdf") for index, page in enumerate(reader.pages): writer = PdfWriter() writer.add_page(page) with open(f"page_{index + 1}.pdf", "wb") as out_file: writer.write(out_file)

值得注意的一个细节:pypdf新版本(3.x+)建议用PdfReader和PdfWriter;老教程里的PdfFileReader已经废弃了,新代码就别再用了。另外writer.write()之后要把writer关掉或者复用前重置,不然多次写入同writer会累积页面。

如果你处理的PDF里有大量高清图片,合并后文件会膨胀得厉害。我处理过一批扫描合同,合并前总共200MB,合完变成1.2GB。后来先跑了一遍图片压缩再合并,体积控制在300MB以内。这个问题很容易被忽视,直到服务器磁盘告警才发现。

4.2 加密与解密:密码保护的常见处理方式

给PDF加密码是常见需求,尤其是发出去的资料不想让人随意改动。pypdf里加密很简单:

from pypdf import PdfReader, PdfWriter reader = PdfReader("original.pdf") writer = PdfWriter() writer.append_pages_from_reader(reader) writer.encrypt("my_password") with open("encrypted.pdf", "wb") as out_file: writer.write(out_file)

encrypt()支持两个密码参数,owner password和user password。传一个参数的话默认是owner password,控制能否修改文档权限;给用户看的只读密码可以再传个user_password。实际使用中,大多数场景一个密码就够,不用分太细。

解密同样简单:

from pypdf import PdfReader reader = PdfReader("encrypted.pdf") if reader.is_encrypted: reader.decrypt("my_password") for page in reader.pages: print(page.extract_text())

有个反直觉的坑:decrypt()成功后会返回一个枚举值,表示解密状态,但reader.is_encrypted在解密后仍然可能显示True,这是pypdf的设计,不代表没解密成功。很多人在这一步卡半天,以为是代码问题。

4.3 批量加水印:给页面盖个章

给PDF批量加水印(比如"内部资料""已审核"),原理其实是用另一个PDF当底图,叠加到目标页上。pypdf的页面合并功能就能实现:

from pypdf import PdfReader, PdfWriter # 水印PDF,通常是一张带透明度文字的页面 watermark = PdfReader("watermark.pdf").pages[0] reader = PdfReader("archive.pdf") writer = PdfWriter() for page in reader.pages: page.merge_page(watermark) writer.add_page(page) with open("watermarked.pdf", "wb") as out_file: writer.write(out_file)

水印PDF通常用ReportLab生成就行了,普通的文字打印出来就行。关键是水印文字要设成半透明效果,不然会盖住正文,影响阅读。这个可以预先在生成水印时控制透明度,如上图文字对象用setAlpha来调整。

用这个方案批量处理几百个文件也很快,基本是秒级完成。

5. 源码级踩坑记录:中文、性能与输出质量

5.1 中文PDF提取乱码:问题不总在提取层

前面提过中文PDF提取乱码的事,这里展开说说我一个印象深刻的案例。

有次处理某政务网站导出的公告PDF,pdfplumber提取出来全是空白,PyMuPDF提取出来是一堆乱七八糟的CJK字符。折腾半天发现,这个PDF的文本对象根本不存在——所有汉字都是直接调用字体的字形轮廓画上去的,本质上跟扫描件没有区别,只是不是位图,而是矢量图形。这种情况什么提取库都白搭,唯一的路就是像扫描件那样OCR。先把页面渲染成图片,再跑Tesseract。

所以遇到提取乱码,先别怀疑库不好,老老实实检查这个PDF是不是"真文本"。最简单的方法,用浏览器打开PDF选中文字拖一下,看能不能选中的一条连续的文本。如果能,说明有文本层,可以尝试提取;拖不动的,就是伪文本,直接走OCR路线。

5.2 ReportLab中文输出:字体注册这件小事

用ReportLab生成PDF时,新手第一坑就是中文。直接drawString写中文,出来全是方框,原因很简单:PDF的字体机制里,中文字体不是内置的,必须把字体文件嵌入文档。ReportLab默认字体只支持Latin字符集。

解决办法是注册一个中文字体:

from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 # 注册中文字体,SimSun是宋体 pdfmetrics.registerFont(TTFont("SimSun", "C:/Windows/Fonts/simsun.ttc")) c = canvas.Canvas("hello_cn.pdf", pagesize=A4) c.setFont("SimSun", 12) c.drawString(100, 700, "你好,PDF中文输出测试") c.save()

Linux上如果没有中文字体文件,可以先安装文泉驿等开源中文字体。注册好字体后还有个小坑:设置了字体之后,每段文字都要显式setFont,否则canvas会回退到默认字体导致中文再次变方块。

制作带中文的表格或复杂图形,直接用ReportLab会比较吃力。我的习惯是优先考虑HTML转PDF方案,HTML的表格和CSS排版能力强太多了,只有需要做精确到点的图形绘制(比如生成标签、计价单)时才动用ReportLab。

5.3 性能问题:几十万页PDF怎么扛

最后聊聊性能。PDF处理平时小打小闹感觉不到差异,但量一大就立刻见分晓。同样是提取文本,pdfplumber比PyMuPDF慢好几倍。我在一台普通笔记本上测试过,一个约500页、每页有一张图表的PDF,pdfplumber提取全部文本需要40多秒,PyMuPDF只要8秒左右。差一个数量级不止。

总结一下我现在的性能策略:

  • 批量提取、需要渲染图片的操作,优先用PyMuPDF
  • 表格提取精度要求高、文件页数不多时,才上pdfplumber
  • 合并拆分这种轻操作,pypdf本身效率不高,但对文件内容不敏感,复杂PDF也不会卡死
  • 超大批量任务,别一次性把整个文件都读进内存,逐页流式处理,或用for page in reader.pages这种懒加载方式

另外一个经常被忽略的点是文件句柄释放。用with pdfplumber.open(...)没问题,但如果手动PdfReader(...)打开了文件,处理完一定要确认文件能重新覆盖写入,不然Windows上经常遇到文件被占用删不掉的情况。我在写自动化脚本时通常会同时做异常捕获和finally关闭逻辑。

PDF处理这条路,入门容易做精深真的难。尤其是国内办公环境下的PDF产品五花八门,导出方式千奇百怪,你永远不知道下一份PDF会带什么编码、什么字体、什么奇葩结构。最关键的是别拿一把锤子敲所有的钉子——提取、转换、生成、编辑各用最合适的库,搭配着来,速度快又稳。上面这些代码我都跑通了,直接复制就能用,遇到问题欢迎回来交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询