Python处理PDF全指南:从文本提取到OCR识别的实战技巧
2026/9/9 5:54:46 网站建设 项目流程

先说个很多人容易踩的误区:提到Python处理PDF,很多人第一反应是“直接用PDF编辑器不就行了?”但真正到了批量操作、内容提取、数据清洗这一步,手动处理基本就是灾难。比如你手上有一百份格式统一的扫描件合同,要抽出关键字段做台账;比如你要把几十个PDF按目录拆分成独立章节;再比如你想把一堆表格PDF里的数据汇总到Excel。这种场景下,Python的价值不在于“编辑一个PDF”,而在于把PDF当做一个数据源、一条流水线来处理。

这篇文章会围绕Python处理PDF的几类核心操作展开:文本提取、图片提取、表格解析、格式转换、合并拆分、加密解密、水印添加、OCR识别。每个模块我都会给出可直接运行的代码、选型思路,以及实际项目中容易翻车的细节。适合两类读者:一是被PDF重复操作折磨的办公自动化需求方,二是想系统掌握PDF处理技术栈的开发者。

1. 动手之前先选对库:Python处理PDF的几大主力工具对比

PDF处理的库非常多,但选错库会把简单事情搞复杂。我在早期项目里就吃过亏——用某个人气很高的库去做表格提取,结果面对带合并单元格的复杂表格直接崩掉,最后花了一整晚重构。先把主力工具分分类,大家按场景选型。

1.1 高配核心:pypdf 与 PyMuPDF 的定位差异

处理PDF绕不开两个名字:pypdf和PyMuPDF。先说历史沿革,早期有一个库叫PyPDF2,后来维护者重构之后把项目改名为pypdf,如果你的老代码还在用from PyPDF2 import PdfReader,现在装pypdf也能正常运行,因为兼容了旧导入路径,但新代码建议直接写from pypdf import PdfReader

pypdf是纯Python实现,优点是跨平台、无外部依赖、安装简单,适合做PDF的“结构性操作”——合并、拆分、旋转、裁剪、加密解密、读取元数据和书签。缺点是它对PDF内容流的解析能力一般,别指望它做精细的文本定位。

PyMuPDF是需要重点关注的库,虽然安装包名是PyMuPDF,但导入名是fitz,这里曾经坑过不少人。它底层绑定的是MuPDF这个C语言渲染引擎,性能极强。它最拿手的是两件事:一是把PDF页面渲染成图片,二是按坐标块提取文本和图像。配合正则表达式做定向抓取,比pypdf的extract_text()输出可控得多。

对比维度pypdfPyMuPDF
底层实现纯PythonC扩展(MuPDF)
结构操作(合并/拆分/加密)一般
文本提取精度较弱,常混排较强,支持坐标块提取
页面渲染为图片不支持
安装依赖需要编译好的wheel
学习曲线

结构操作优先用pypdf,内容提取和渲染优先用PyMuPDF。这不是“选一个”的问题,而是两个都要会。

1.2 内容提取特长生:pdfplumber 与 pdfminer.six

如果核心需求是从PDF中抽表格,pdfplumber是最成熟的选择。它的底层解析器用的是pdfminer.six,但pdfplumber在pdfminer基础上做了大量优化,直接暴露了extract_table()extract_tables()这两个接口,还能控制表格线的识别策略。pdfminer.six本身定位是低层工具,适合高级用户自定义解析流程,如果你只是想快速抽表,直接上pdfplumber。

pdfplumber提取表格的原理并不神秘:它先解析页面上的线条(包括水平线和垂直线),把线条交叉形成的矩形网格填上文本内容,最终拼成二维表格。这也就解释了它的局限性——如果表格没有画线,或者线条是图片形式的扫描件,pdfplumber就失灵了。这种情况要落到OCR方案,我在后面专门讲。

1.3 生成场景的梯队:reportlab、fpdf2 各自擅长什么

说完读取再说生成。从零创建PDF,最常见的是reportlab,老牌、功能全、支持复杂的绝对定位和图形绘制,缺点是API设计偏古老,写起来不够直观。如果你要生成的是简单报表、发票样式的PDF,用reportlab非常稳。另一个轻量方案是fpdf2,API更现代化、文档也友好,但复杂布局能力不如reportlab。我的建议很直接:要做动态版式、大批量出证,直接学reportlab,它帮你避免很多底层坑。

1.4 安装与版本踩坑记录

安装命令如下,建议用虚拟环境:

pip install pypdf pdfplumber PyMuPDF reportlab pdf2docx

这里有三个常见的坑:

第一,PyMuPDF导入名不是PyMuPDF,而是import fitz,看到ModuleNotFoundError: No module named 'fitz'不用慌。

第二,新版pypdf对旧版PyPDF2的API做了调整,比如PdfFileReader这种类名在新版中变成了PdfReader,网上很多老教程混着用会报错。

第三,pdfplumber依赖pdfminer.six,而pdfminer.six更新频繁,偶尔会遇到pdfplumber对某个pdfminer版本不兼容,症状是调用extract_text()时抛TypeError。解决办法很简单,把pdfminer.six固定到pdfplumber要求的版本即可,看它的setup.py为准。

2. 从PDF里“薅”内容:文本、图片、表格三种提取实战

2.1 文本提取:先跑通最简单的,再处理“难啃”的

先看最简单场景——数字化生成的PDF,即文本可选的PDF,不是扫描件。用pypdf提取全文只需要几行:

from pypdf import PdfReader reader = PdfReader("demo.pdf") for page in reader.pages: text = page.extract_text() or "" print(text)

这个方案对简单的单栏文本有效,但对双栏排版、页眉页脚混排的效果就不太理想,因为extract_text()是整页一股脑输出的,阅读顺序会乱。这时候换成PyMuPDF的按块提取方式:

import fitz doc = fitz.open("demo.pdf") for page in doc: blocks = page.get_text("blocks") # 每个块是(x0, y0, x1, y1, text, block_no, block_type) for b in blocks: if b[6] != 0: # 跳过图片块 continue print(f"坐标({b[0]:.1f}, {b[1]:.1f}) -> {b[4]}")

拿到每个文本块的坐标后,你可以按列排序、按区域过滤,比如只提取页面左上角的标题,或者只取某一坐标范围内的正文内容。这在处理复杂版面的纸质扫描转换PDF时非常实用。

2.2 图片提取:别再截图了,直接拉原图

PDF里嵌的图片,很多人是打开PDF软件放大后截图,分辨率一塌糊涂。用PyMuPDF可以精准取回原始嵌入图片。

import fitz doc = fitz.open("with_images.pdf") for page_num in range(len(doc)): page = doc[page_num] images = page.get_images(full=True) for img_index, img in enumerate(images): xref = img[0] base_image = doc.extract_image(xref) image_bytes = base_image["image"] ext = base_image["ext"] with open(f"page{page_num+1}_img{img_index+1}.{ext}", "wb") as f: f.write(image_bytes)

extract_image()拿到的就是嵌在PDF里的原图字节流。不过需要注意:有些PDF设计者会对图片套一层裁剪遮罩(mask),单独提取出来整图会带黑色背景或透明通道异常。遇到这种情况,建议先渲染整个页面区域,再用像素处理的方式裁剪,后续会聊到。

2.3 表格提取:把“铁板一块”拆成行和列

表格提取是PDF操作里最能体现“开箱即用与真实阵痛”的部分。拿pdfplumber举例:

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

看着简单,但真实项目中会遇到几个问题。第一,pdfplumber默认用页面上的所有线条生成表格,如果页面有线框用于装饰,会被误判成表格,解决方案是传入vertical_strategyhorizontal_strategy参数,改成"text"策略,让系统根据文本位置推断表格边界。第二,合并单元格提取后会出现重复文本或None值,需要写清理逻辑。

我自己的经验是,处理复杂表格时绝不只依赖一次extract_tables(),而是结合page.linespage.rects先画一个页面网格可视化,再做针对性参数调整。必要时用page.crop()把表格区域切出来单独解析,准确率会明显上升。

2.4 提取后数据清洗的必要性

提取出文本或表格之后,别急着入库。PDF中的空格可能是为了对齐而填充的,换行符可能出现在句子中间。我通常会在提取后做一次统一清洗流程:合并断行、去空格、处理全半角符号、剔除页眉页脚特征文本。用正则表达式处理时注意PDF文本常含有特殊连字符和不可见字符,优先用Unicode规范化。

3. PDF转Word、转图片:多种转换思路解析

3.1 很多人想要的PDF转Word:怎么做才不像“奥利给”

PDF转Word是搜索热词里的大户,但说实话,不存在完美转换。文字版PDF转Word还能做到格式基本还原,扫描版PDF不做OCR是不可能直接转成可编辑Word的,所谓“识别”其实是OCR加版面重排。

工具层面,我推荐用pdf2docx,它基于PyMuPDF和python-docx实现,对文本型PDF的还原度相当高,能保留基本样式、表格和图片。

pip install pdf2docx
from pdf2docx import Converter pdf_file = "demo.pdf" docx_file = "demo.docx" cv = Converter(pdf_file) cv.convert(docx_file, start=0, end=None) cv.close()

一个合理的预期管理:pdf2docx对单栏文本、标准表格的PDF效果很好,但对复杂排版、艺术字体、特殊符号的处理会出现偏位。如果你接触的PDF大多来自Word或WPS导出,这个工具的性价比极高。

如果你的转换需求不是整篇转换,而是从PDF里挑几页转出来,Converter的startend参数可以直接指定页码范围,不用先把PDF裁开。

3.2 PDF转图片:渲染成PNG/JPG到底有什么用

PDF转图片有两大典型场景:做OCR预处理,以及防止代码被直接复制走的内容分发。PyMuPDF把页面渲染成图片非常简单:

import fitz doc = fitz.open("demo.pdf") zoom_x = 2.0 # 2倍分辨率 zoom_y = 2.0 mat = fitz.Matrix(zoom_x, zoom_y) for page in doc: pix = page.get_pixmap(matrix=mat) pix.save(f"page_{page.number+1}.png")

这里的zoom参数直接控制输出DPI,2.0对应约144 DPI,3.0约216 DPI。如果只是预览,1.5足够;如果后续要送OCR,建议至少2.0以上。别小看这个参数,我曾试过用1.0的zoom导出图片OCR,小字号中文识别率惨不忍睹,调高到2.5后准确率明显改善。

3.3 批量转换与文件整理

批量处理是Python处理PDF的最大优势。一个常见的自动化场景:把某个目录下所有PDF按文件名转换成同名的Word或图片,并归档到指定目录。

from pathlib import Path from pdf2docx import Converter src_dir = Path("./pdfs") out_dir = Path("./converted") out_dir.mkdir(exist_ok=True) for pdf_path in src_dir.glob("*.pdf"): out_path = out_dir / (pdf_path.stem + ".docx") cv = Converter(str(pdf_path)) cv.convert(str(out_path)) cv.close() print(f"已转换: {pdf_path.name}")

如果是成百上千个文件,加一个ThreadPoolExecutor做多线程也能跑,但要注意pdf2docx的转换过程比较吃内存,建议分批处理,一次控制在20个文件以内。

4. 合并、拆分、旋转、加密:不换格式也能完成的日常操作

4.1 合并多个PDF:顺序与书签问题

合并PDF是最基础也最高频的操作。一个典型的业务场景:把多份扫描件合并成一个完整合同。pypdf的实现方式如下:

from pypdf import PdfWriter, PdfReader writer = PdfWriter() pdf_files = ["a.pdf", "b.pdf", "c.pdf"] for file in pdf_files: reader = PdfReader(file) for page in reader.pages: writer.add_page(page) with open("merged.pdf", "wb") as out: writer.write(out)

需要重点处理的是书签(目录)问题。直接用上面的代码合并,生成的文件没有书签面板,读者翻页很不方便。如果你在做一个几十页的合并PDF,建议给每个文件的开头页面添加书签:

from pypdf import PdfWriter, PdfReader writer = PdfWriter() pdf_files = ["a.pdf", "b.pdf", "c.pdf"] for index, file in enumerate(pdf_files): reader = PdfReader(file) start_page = len(writer.pages) for page in reader.pages: writer.add_page(page) writer.add_outline_item(f"Part {index+1}: {file}", start_page) with open("merged_with_bookmarks.pdf", "wb") as out: writer.write(out)

还有个细节容易被忽视:合并PDF时,如果原始文件的页面尺寸不一致(比如第一部分是A4,第二部分是A3扫描件),合并后查看器会自动缩放显示,打印时会出现页面大小错乱。此时可以在添加页面前用page.scale_to(width, height)统一页面尺寸。

4.2 拆分指定页:灵活抽取子文件

拆分PDF常见有两种需求:按页码范围切割,以及按页数规律抽选。看代码:

from pypdf import PdfReader, PdfWriter reader = PdfReader("source.pdf") total_pages = len(reader.pages) # 提取第2页到第5页(注意页码从0开始) writer = PdfWriter() for page_num in range(1, 5): writer.add_page(reader.pages[page_num]) with open("extracted.pdf", "wb") as out: writer.write(out)

从财务对账场景来看,经常需要“抽奇数页”,因为很多扫描合同是正反面分别扫描的,正面是盖章页,背面是条款页,全处理完后就要按奇偶拆成两组文件。控制循环步长即可实现。

4.3 页面旋转、裁剪与方向修正

扫描件方向错乱的问题非常常见,特别是手机扫描的文档,经常出现某些页面旋转了90度或180度。pypdf的旋转操作:

from pypdf import PdfReader, PdfWriter reader = PdfReader("rotated_source.pdf") writer = PdfWriter() for page in reader.pages: page.rotate(90) # 顺时针旋转90度。也可用rotate_clockwise writer.add_page(page) with open("rotated_fixed.pdf", "wb") as out: writer.write(out)

判断页面是否旋转、应该旋转多少度,现实中不能靠猜。推荐先提取每页的前几个文本块的坐标,分析文本方向后再决定是否旋转。PyMuPDF可以拿到页面原始旋转属性:

page.rotation # 返回0, 90, 180, 270

如果页面自带的rotation信息不对,或者压根没写rotation字段是0,但内容实际是横着排的,那你可能需要先渲染成图片,用视觉方向判断。自动判断文本方向是个偏复杂的CV问题,简单的场景可以直接肉眼预览几个页面再写死旋转逻辑。

4.4 加密、解密与权限控制

加密PDF分为两种:打开密码(User Password)和权限密码(Owner Password)。用pypdf设置密码和权限:

from pypdf import PdfWriter, PdfReader reader = PdfReader("source.pdf") writer = PdfWriter() for page in reader.pages: writer.add_page(page) writer.encrypt( user_password="user123", owner_password="owner456", permissions_flag=0b1100, # 禁止打印、禁止修改 ) with open("encrypted.pdf", "wb") as out: writer.write(out)

解密有打开密码的文件:

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

需要提醒一句:你只能解自己有权限处理的文件。很多打印店和公司内部文件的加密权限是自己设置的,解密逻辑是做自动化归档用的,别拿来做突破访问控制的事。从技术上讲,某些非标准加密的PDF(比如部分国内的电子发票PDF)pypdf是解不开的,会抛FileNotDecryptedError,这种属于供应商自研加密方案,需要使用专用SDK处理。

5. 水印、OCR识别和扫描件处理:PDF的进阶玩法

5.1 给PDF批量加文字水印和图片水印

给PDF加水印通常有两种实现路线。路线一:用reportlab先生成一个透明背景的水印PDF,再用pypdf把它叠加到目标页面上。路线二:直接使用PyMuPDF在每页的指定坐标写入文字。

这里说路线一,因为它的通用性更强,不依赖PyMuPDF的渲染坐标。先弄一个水印源:

from reportlab.pdfgen import canvas from reportlab.lib.units import cm c = canvas.Canvas("watermark.pdf", pagesize=(595, 842)) # A4 c.setFont("Helvetica", 40) c.setFillColorRGB(0.5, 0.5, 0.5, 0.3) # 半透明灰色 c.translate(297, 420) # 移动到页面中心 c.rotate(45) c.drawCentredString(0, 0, "CONFIDENTIAL") c.save()

再把这个水印PDF和原始PDF叠加:

from pypdf import PdfReader, PdfWriter reader = PdfReader("target.pdf") watermark = PdfReader("watermark.pdf") wm_page = watermark.pages[0] writer = PdfWriter() for page in reader.pages: page.merge_page(wm_page) writer.add_page(page) with open("watermarked.pdf", "wb") as out: writer.write(out)

这里merge_page()会把水印层的透明效果保留下来,适用于合同、设计稿、内部文件的防泄露场景。

5.2 扫描件PDF的OCR识别:把“死”文件变“活”

扫描版PDF本身没有文本层,任何直接提取文本的方法都返回空字符串。要让它可搜索、可提取,唯一的途径是OCR。两个主力方案:pytesseract(Google Tesseract)和PaddleOCR

Tesseract是老牌方案,支持100多种语言,中文识别效果中规中矩,安装时除了pip还要额外装系统级程序(Windows下要安装tesseract.exe并配置环境变量)。PaddleOCR是百度开源的,中文识别效果更佳,pip安装即可,但模型加载会更重量级。

我的典型处理流程是:先用PyMuPDF把PDF页面渲染成高分辨率图片,再用OCR引擎识别文本,最后把识别结果连同位置信息写入一个新的可搜索PDF,或者直接生成TXT/JSON数据。

以PaddleOCR为例,识别并输出JSON:

import fitz import json from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch") # 支持中英文 doc = fitz.open("scan.pdf") result_all = [] for page_num in range(len(doc)): page = doc[page_num] pix = page.get_pixmap(matrix=fitz.Matrix(2.5, 2.5)) img_path = f"temp_page_{page_num+1}.png" pix.save(img_path) result = ocr.ocr(img_path, cls=True) for line in result: if line is None: continue for item in line: box = item[0] text = item[1][0] conf = item[1][1] result_all.append({ "page": page_num + 1, "text": text, "bbox": box, "confidence": conf }) with open("ocr_result.json", "w", encoding="utf-8") as f: json.dump(result_all, f, ensure_ascii=False, indent=2)

OCR结果的置信度过滤很关键。低于0.7的文本块基本可以丢掉了,或者在导出前标记为“疑似识别错误”。实际项目中,扫描件的清晰度参差不齐,识别结果总会有误差,算法模型再好也救不了一张拍歪拍糊的原件。前期做图像预处理(转灰度、锐化、二值化)往往比调模型参数更有效。

5.3 模板化批量提取特定字段

掌握了文本提取和OCR之后,最实用的进阶玩法是做模板化字段提取。比如我从几十张格式统一的报销单PDF里提取日期、金额、报销人三个字段。

思路是:先用pdfplumber打开一份样本,定位目标字段的坐标区域,然后批量处理所有文件时,只对相同坐标区域调用extract_text()extract_words()。这就是“坐标模板”方案,只要版式不变,准确率非常高。

import pdfplumber regions = { "date": (50, 100, 200, 130), # (x0, y0, x1, y1) "amount": (300, 100, 500, 130), "name": (50, 140, 200, 170), } with pdfplumber.open("batch_01.pdf") as pdf: page = pdf.pages[0] for field, (x0, y0, x1, y1) in regions.items(): crop = page.crop((x0, y0, x1, y1)) text = crop.extract_text() print(f"{field}: {text}")

这套方案的关键在于拿到准确的坐标。第一次的坐标往往不准,建议先用page.to_image(resolution=150)渲染页面并手动标注,然后再写模板。版式只要错位1毫米,提取结果就可能串字段。

6. 踩坑实录:编码、字体、坐标和页面尺寸那些问题

6.1 提取出来全是乱码怎么办

PDF文本提取乱码的典型原因有两个。第一是字体子集化加自定义编码,PDF标准允许字体内部用非Unicode的CID映射,很多国内厂商的PDF生成工具喜欢这么干。提取工具读不到ToUnicode映射表时,extract_text()就会吐出类似乱码或者空文本。第二个原因是字体缺失,尤其是比较偏的字体,系统没有对应字体库时,PyMuPDF对部分文本块的解析会异常。

最优解法是转路线:如果必须取文字,试试pdfplumber的extract_words(),它输出的粒度更细,偶尔能救回来。不行就上OCR。OCR对这种情况几乎是降维打击,因为不管底层字体编码多怪,渲染成图片后文字特征还在。

6.2 PDF坐标体系与常规图像坐标的差异

PDF的坐标系统与常规图像不同:原点是左下角(0,0),x轴向右增长,y轴向上增长。很多初学者用PyMuPDF的get_text("blocks")拿到坐标后,直接去别的图像库画框,结果文本框全部上下颠倒,因为图像库的坐标原点是左上角,y轴向下增长。

转换公式很简单:

image_y = page_height - pdf_y

用PyMuPDF渲染页面时,page.rect.height就是页面高度,换算一下即可。这个坑我在做“根据PDF文本定位信息在截图里画高亮框”时踩过,折腾了半小时才发现是坐标系问题。

6.3 表格提取时线条缺失、合并单元格错位

用pdfplumber提取复杂表时,经常遇到“表格线有一根没识别出来”导致整列错位。此时把策略改成显式配置:

table = page.extract_table({ "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, # 线条吸附容差,单位pt })

如果表格线是花色的、浅色的或者虚线,lines策略可能识别不到,改用explicit_vertical_linesexplicit_horizontal_lines手动传入线坐标。合并单元格的处理没有银弹,我的经验是解析后写一个后处理函数,把跨行跨列的单元格按第一出现的行列对齐,并把空值填成上一行的值。

6.4 页面尺寸不一致导致的奇怪问题

合并或截取PDF页面后,如果页面尺寸各不相同,某些PDF阅读器显示时会出现“页面很大但内容很小”的怪象。最好的方案是在页面写入前统一尺寸:

from pypdf import PdfReader, PdfWriter reader = PdfReader("source.pdf") writer = PdfWriter() for page in reader.pages: if page.mediabox.width > page.mediabox.height: page.rotate(90) # 统一为竖向 writer.add_page(page) with open("normalized.pdf", "wb") as out: writer.write(out)

mediabox是PDF页面的物理尺寸,单位为pt,A4大约是595x842,letter大约是612x792。不同来源的文件混在一起时,先标准化尺寸和方向,能避免后面打印、合并时的一堆刁钻问题。

6.5 嵌入式字体与文件体积膨胀

给PDF加水印、合并页面后,文件体积可能会异常膨胀。这是因为merge_page()有时会把水印页的字体资源整体带入目标文档,多次合并后字体资源被重复嵌入。解决方法是处理完所有操作后,用PyMuPDF的垃圾回收机制做一次文档瘦身:

import fitz doc = fitz.open("merged_raw.pdf") doc.subset_fonts() # 尝试压缩字体子集 doc.garbage_collect() doc.save("merged_compressed.pdf", garbage=4, deflate=True)

garbage=4是最激进的重写策略,会用重新序列化整个文档的方式清理无用对象。但要注意,对加密的PDF先解密再压缩,否则可能报错。

7. 把脚本封装成顺手的小工具:经验与建议

走到这里,基础的PDF处理能力你已经全部掌握了。最后一个建议,别每次都在命令行里敲交互式代码,要把这些能力封装成函数或脚本,形成自己的工具库。比如我会在本地维护一个pdf_toolkit.py,里面放十来个常用函数:extract_text(path, pages)extract_images(path, output_dir)split_pdf(path, ranges)merge_pdfs(file_list)add_watermark(src, wm, output)

团队协作的时候,把这套工具包做成命令行CLI或者简单的图形界面,能极大减少重复沟通成本。我见过很多运营人员对着一百多份PDF手工操作,教她们用脚本之后,日常处理时间从小时级降到分钟级。这就是技术解决实际问题的直接价值。

一个小技巧是给所有函数加异常兜底和日志输出,PDF文件来源五花八门,有些文件损坏、有些权限受限、有些非标准实现,处理批量任务时一个坏文件可能会中断整个流程。我的习惯是用try...except跳过并记录失败文件,等批量结束后统一查看错误报告。

最后再分享一个关于页面预览的调试经验。处理PDF时如果你不确定页面当前的样子,不要反复猜测参数,直接渲染页面缩略图看一眼最有说服力。写代码的过程就是和PDF格式斗智斗勇的过程,保持这个习惯,能帮你省掉大量的猜测时间。

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

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

立即咨询