news 2026/10/5 7:58:38

用Python PyPDF2一把梭:PDF拆分合并、文本提取与加密解密实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python PyPDF2一把梭:PDF拆分合并、文本提取与加密解密实战

今天这篇是文档自动化系列的第 44 天,主题是 PDF。上午同事发来三十几份盖章扫描件,要求按编号拆开、把第一页的金额字段抓出来汇总到表格,手点肯定是不现实的,我直接在 Python 里用 PyPDF2 一把梭搞定。标题里喊它"文档处理的屠龙刀",我觉得一点不夸张——日常能遇到的 PDF 大多数操作,读页数、提取文本、合并、拆分、旋转、裁剪、上锁加密,它都能处理,而且 API 简单到小白也能直接上手。

这篇笔记适合两类人:一是 Python 基础学到函数、文件读写阶段、想找个真实练手场景的学习者;二是被 PDF 工作流折磨的运营、财务、文档岗,想用 Python 把自己从重复劳动里捞出来。下面我按从安装到实战的顺序,把 PyPDF2 的用法、踩过的坑、以及它和其他 PDF 库的分工一次讲清楚。

1. PDF 为什么难搞?先搞懂它再上手 PyPDF2

1.1 PDF 的底层逻辑:电子打印纸

先说为什么 PDF 处理在 Python 生态里地位这么特殊。你有没有想过:一个 PDF 文档在编辑器里能改字,用代码提取却不完美?合并、拆分、转换的库一大堆,为什么还经常翻车?问题出在 PDF 的底层设计上。

PDF 本质上是"电子打印纸"。它保存的不是"第几行是什么文字",而是一套精确到点的绘制指令:在什么坐标、用哪份字体子集、绘制哪段内容流。你看到的每个页面,在 PDF 内部只是一个页面对象加一串内容流,外加资源字典里的字体、图像、颜色对象。拿 Word 对比就很好理解:Word 是"菜谱",PDF 是"做好的菜的照片"。你能从照片里看出放了盐,但没法直接抽出一行"盐 3 克"。同理,PDF 里能看见文字,不代表代码能直接按段落读出来。

这个设计带来的直接后果:想从 PDF 里提取文本,前提是 PDF 里的字体对象提供了 Unicode 映射(专业术语叫 ToUnicode CMap)。现实中大量扫描件、老式排版工具生成的 PDF 根本没有这个映射,所以直接抽文字,经常得到空白或乱码。这不是 PyPDF2 的锅,是 PDF 格式天生如此,搞清楚这一点,后面遇到问题你就不会冤枉库了。

1.2 PyPDF2 的价值:页面级手术刀

在这样的生态里,PyPDF2 的位置是"页面级手术刀"。它的核心 API 围绕 page 和 writer 展开,让你把 PDF 当成"一叠纸"去处理:添加页面、删除页面、调整顺序、拆分、合并、旋转、裁剪。这些操作都在动页面树和文档结构,而不是去管文字长什么样。

打个比方:PDF 像一座房子,PyPDF2 管的是墙体、门窗的拆装和房间分割,它不管房子里沙发怎么摆。沙发摆放(文本布局、表格坐标)是 pdfplumber 的活,敲墙拆房才是它的主场。学习的时候把这条界限记住,能少走很多弯路——不要指望 PyPDF2 把表格数据完美读出来,那是另一个库的擅长领域。

2. 装库和选版本,别让 API 坑了你

2.1 安装方案与 PyPDF2 / pypdf 的关系

安装本身没什么难度,一个 pip 命令的事:

pip install PyPDF2

但这里有个特别容易踩的坑:PyPDF2 的维护已经转移了。老仓库发布到 2.12.x 后就进入只读维护,官方把核心代码合并到了 pypdf 项目,以 3.x 版本继续迭代。所以你在网上搜教程,可能看到两种写法混着出现,实际是同一套代码的"新老马甲"。

我给的建议很简单:新项目直接用 pypdf,老项目继续用 PyPDF2 也没问题,关键是别混。如果你刚开始学,PyPDF2 的资料足够丰富,代码能跑起来,学习阶段完全可以用。要是做生产环境的新项目,建议直接依赖 pypdf,后续维护更省心。

2.2 版本差异对照,写新旧代码不再糊涂

网上很多教程代码是 2.0 之前的老 API,当时类名叫 PdfFileReader、PdfFileWriter,方法叫 getNumPages、extractText。现在的 PyPDF2 2.x 和 pypdf 3.x 都把这些改了。拿一份对照表,你排查报错更快:

旧写法(别用)新写法(推荐)
from PyPDF2 import PdfFileReaderfrom PyPDF2 import PdfReader
pdf.getNumPages()len(reader.pages)
page.extractText()page.extract_text()
writer.write("路径.pdf")writer.write(文件对象)

最坑的是最后一行:老写法 write 接收的是文件路径字符串,新写法要求传文件对象。用老写法直接传字符串,大概率报错。我看到太多初学者卡在这一步,其实就是 API 迁移导致的。代码报错的时候,先扫一眼是不是这几个名字的差别,往往一秒钟定位问题。

3. 读取 PDF 信息:页数、元数据、文本提取

3.1 打开 PDF 与惰性读取的陷阱

先来个最基础的打开和读信息:

from PyPDF2 import PdfReader reader = PdfReader("sample.pdf") print("页数:", len(reader.pages)) meta = reader.metadata if meta: print("标题:", meta.title) print("作者:", meta.author)

reader.pages 返回的是页面对象列表,len 一下就知道总页数。metadata 是文档自带的信息,但有些 PDF 生成器不写元数据,它的值可能是 None,所以用 if 判断一下再访问属性。

这里要郑重提醒一个惰性读取的坑。PdfReader 打开文件后,不会立刻把所有内容加载到内存,页面对象在内部还持有对原始文件流的引用。如果你图省事这么写:

# 这样不行,文件句柄被提前关了 with open("sample.pdf", "rb") as f: reader = PdfReader(f) page = reader.pages[0] # 报错!页面对象拿不到底层流了

正确做法是把读取和后续操作放在同一个 with 块里,或者先把文件内容读进内存再用 BytesIO:

import io from PyPDF2 import PdfReader with open("sample.pdf", "rb") as f: data = f.read() file_like = io.BytesIO(data) reader = PdfReader(file_like) print(len(reader.pages))

这个坑相当隐蔽,因为报错信息不那么直白。我自己就栽过两次,之后统一用 BytesIO 方式,省心多了。

3.2 批量遍历页面提取文本的实操写法

提取文本是 PyPDF2 最高频的需求之一。语法很简单:

for i, page in enumerate(reader.pages, start=1): text = page.extract_text() print(f"--- 第 {i} 页 ---") print(text)

但我要泼一盆冷水:这个提取是"尽力而为"。对排版规整、字体映射完整的 PDF,效果很好;对双栏排版、表格、扫描件,结果可能乱成一团。文本顺序被打乱、段落间多出奇怪换行、中文变空白,都属于常见现象。

我的实操习惯是:先只提取前两三页打印看看,确认输出顺序能接受,再跑全量。别上来就全量导出一堆数据,最后发现顺序是乱的,返工成本很高。如果你要做的是从特定区域取文本,PyPDF2 做不到,那得换 pdfplumber 按坐标切割。

4. 合并与拆分 PDF:最常用的两个操作

4.1 合并多个 PDF:PdfMerger 一行接一行

合并是办公场景里的高频需求:把多个合同附件合成一份、把扫描分段的文件拼起来、把日报汇总成月报。PyPDF2 提供了 PdfMerger,写法非常直白:

from PyPDF2 import PdfMerger merger = PdfMerger() for pdf in ("contract1.pdf", "contract2.pdf", "contract3.pdf"): merger.append(pdf, import_bookmarks=False) with open("all-in-one.pdf", "wb") as f: merger.write(f) merger.close()

append 按你给的顺序追加整个文件。import_bookmarks=False 是我习惯加的,不然原 PDF 里的书签目录会被带进来,有些场景不需要。文件列表用 sorted 排好序再循环,能避免"1、10、2"这种数字顺序错乱问题。

这里说个经验:PdfMerger 操作大文件时内存占用偏高,因为它要把所有文件的结构临时挂在内存里维护。合并 100MB 以上的文件,容易卡顿甚至报递归错误(后面排查部分细说)。如果文件很大,我建议每次只合并两三个,分几轮合并,或者干脆用别的库。

4.2 按页拆分:切片思路写清楚

拆分跟合并正好反过来,核心是"按索引挑页面、放进新 Writer、另存文件":

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("source.pdf") writer = PdfWriter() for page in reader.pages[2:5]: writer.add_page(page) with open("part.pdf", "wb") as f: writer.write(f)

pages[2:5] 是左闭右开区间,实际取出第 3、4、5 页,总共 3 页。想拆单页,循环用页号一层一层来即可:

pages = [1, 5, 9] for n in pages: writer = PdfWriter() writer.add_page(reader.pages[n - 1]) with open(f"page_{n}.pdf", "wb") as f: writer.write(f)

这里要注意索引从 0 开始,所以第 1 页对应 reader.pages[0]。这种小细节看着不起眼,但循环批量拆的时候错了,文件名和内容对不上,真要命。建议先拆一页做校验,核对页数再跑全量。

5. 旋转与裁剪:把页面折腾成你要的样子

5.1 旋转角度与方向心法

扫描件方向不对是常见问题。竖着排版的合同被扫成横的,转 90 度;有些扫描仪逆时针转了 90 度,导致内容整个倒过来。PyPDF2 处理很简单:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("tilted.pdf") page = reader.pages[0] page.rotate_clockwise(90) with open("rotated.pdf", "wb") as f: writer = PdfWriter() writer.add_page(page) writer.write(f)

反向旋转用 page.rotate_counter_clockwise(90) 即可。但注意两个细节:

第一,PyPDF2 的旋转只认 90 的倍数。PDF 页面内部有个 /Rotate 字段,只存方向不存任意角度,你传 45 度它也不会真正旋转。想转任意角度,PyPDF2 做不到,得用 PyMuPDF。

第二,旋转是叠加的。如果页面本来已经被旋转过 90 度,再 rotate_clockwise(90) 就变成 180 度。想要"绝对方向复位",先看页面自带的 rotation 属性,再决定转几次。我在代码里经常这样判断:

current_rot = page.rotation or 0 if current_rot == 90: page.rotate_counter_clockwise(90)

这样能精确把倒转的页面转回来,而不是越转越乱。

5.2 裁剪坐标:别让留白吃掉内容

裁剪的需求也很常见:扫描件四周有大黑边、PDF 里混入了页眉页脚、某张截图想只保留中间区域。PyPDF2 裁剪的本质是修改页面的裁剪框(cropbox),相当于在纸张上画一个矩形框,只保留框内内容。

PDF 坐标系统的原点在页面左下角,单位是点(1 英寸 = 72 点),A4 纸张大约是 595 x 842 点。裁剪框用四个坐标来描述:左下角 (x1, y1) 和右上角 (x2, y2)。看代码:

page.cropbox.lower_left = (50, 50) page.cropbox.upper_right = (542, 742)

这样就从四周各裁掉 50 点,把内容往里收了一圈。也可以整体赋值:

from PyPDF2.generic import RectangleObject page.cropbox = RectangleObject([50, 50, 542, 742])

必须提醒的是:裁剪只是声明"显示这个区域",PDF 里被裁掉区域的数据(尤其是嵌入的图像)并没有被真正删除,所以文件体积经常不减反增。想彻底瘦身,需要重新压缩图像或做内容流清理,这不是 PyPDF2 的强项。

还有一个顺序问题:先旋转还是先裁剪?我的经验是先旋转、后裁剪。因为旋转会改变页面内容的方向感知,如果先精确裁剪再旋转,旋转后你心里算的坐标就全反了;反过来先转好方向,再以当前页面坐标系去设置裁剪框,符合直觉,基本不会翻车。

6. 给 PDF 加密与解密

6.1 设置打开密码和权限密码

办公场景里,给 PDF 加打开密码特别常见:工资单、报价单、签字版合同,这些敏感文档适合上锁后再分发。PyPDF2 的 encrypt 方法一行搞定:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("source.pdf") writer = PdfWriter() for page in reader.pages: writer.add_page(page) writer.encrypt("user-password") with open("locked.pdf", "wb") as f: writer.write(f)

如果只传一个密码,它作为打开文档的用户口令。PyPDF2 还支持第二个参数 owner password,用来限制打印、复制、修改权限。例如:

writer.encrypt("user-password", "owner-password")

这里的逻辑是:打开文档只需要 user 口令,但 PDF 软件里看到"文档受保护,不可编辑不可打印"的提示,是因为 owner 口令没匹配。如果两个参数都不传,不会输出加密 PDF;只传 user 参数时,权限不受限。

另外注意,encrypt 传空字符串有特殊含义:表示"不需要打开密码,但限制某些权限"。新手容易把空串当"没加密",实际输出文件还是带了限制标记。

6.2 验证密码、处理加密文件时的注意事项

读取加密 PDF 时,必须先解密再访问页面,否则直接 reader.pages 会抛异常。标准流程:

reader = PdfReader("locked.pdf") if reader.is_encrypted: result = reader.decrypt("user-password") # result 是 PasswordType 枚举: # USER_PASSWORD 表示用用户口令解开了 # OWNER_PASSWORD 表示用权限口令解开了 # NOT_DECRYPTED 表示密码不对 print(len(reader.pages))

我建议写完 decrypt 后检查一下返回值,别偷懒。因为密码错误时不会抛异常,只是返回 NOT_DECRYPTED,而后续访问 pages 时才报错,报错信息还容易让人误会成"文件损坏"。提前判断返回值,就能写出更友好的提示。

PyPDF2 对老式 RC4 加密支持不错,但对某些新版 AES 加密的 PDF 可能无能为力,遇到打不开的加密文档,别死磕这一个库,先确认加密算法,必要时换 pypdf 新版本或 PyMuPDF。另外切记:PDF 加密是硬加密,密码忘了基本没有后门可用。唯一合理的建议是,处理自己遗忘密码的文档时,多试试可能的常用密码,再不行就用源文件重新生成。谨慎建议生产脚本里不要把密码硬编码,从环境变量或配置文件读,避免密码随代码泄露。

7. 选型:PyPDF2 的边界,以及和其他库的分工

7.1 三库对比:各管一摊

谈到 PDF 处理的 Python 方案,很多人在 PyPDF2、pdfplumber、PyMuPDF 之间纠结。我直接给一张对比表:

库强项弱项适合场景
PyPDF2 / pypdf页面拆分合并、裁剪旋转、加密解密,API 直观文本提取不强、表格几乎不可用办公自动化、PDF 批量加工
pdfplumber文本坐标、表格解析很强不适合修改页面结构抓表格、按坐标提取信息
PyMuPDF(fitz)速度快、渲染、注释、OCR 配合全面安装包较大、API 更复杂大规模内容抽取、文档引擎

我的选库决策路径很简单:改页面结构(拆、合、转、裁、加密、解密)选 PyPDF2 系;读内容、抽表格选 pdfplumber;既要快又要复杂处理,选 PyMuPDF。它仨不是竞争关系,而是互补关系。

7.2 什么时候别用 PyPDF2

有三类场景我强烈建议别选 PyPDF2,免得浪费时间:

第一是提取复杂表格数据。PyPDF2 的 extract_text 对表格几乎无能为力,它拿到的只是文字流,没有"行"和"列"的概念。这种需求直接上 pdfplumber,它有 row、cell 的概念,按页面坐标还原表格。

第二是把 PDF 渲染成图片。PyPDF2 没有渲染引擎,不能把某页转成 PNG。无论是做缩略图还是 OCR 预处理,都用 PyMuPDF 的 get_pixmap。

第三是矢量图解析。PDF 里的线条、形状、矢量路径,PyPDF2 看不到,能看到的只有文字和图像对象。要对 PDF 里的图形做处理,也得换 PyMuPDF。

记住判断原则:先问自己"我是要动页面,还是要读内容"。动页面找 PyPDF2,读内容找解析库,两者都需要的,就组合使用。

8. 常见问题排查:这是我踩过的坑

8.1 大文件合并直接崩溃:RecursionError 解决方案

用 PyPDF2 合并几十份大文件时,我遇到过明显的 RecursionError,看报错以为是代码写错了,后来才发现是库内部在解析对象树时递归深度超过 Python 默认限制。临时解法简单:

import sys sys.setrecursionlimit(10000)

但治本的方法有两个:一是每批只合并少量文件,分批汇总;二是如果有条件,换 PyMuPDF 处理超大文件的合并。真实场景里,我处理 30 份报表合并时,调高递归限制后顺利通过,但文件到了 200MB 级别,还是明显变慢。这个经验仅供参考,不是无限调大就万能的,内存占用也要考虑。

8.2 页面对象读取时报错:文件句柄被提前关闭

前面在惰性读取的部分提过,我再把报错现场描述一下:你明明打开了文件,代码跑得也正常,但一旦出了 with 块,再去取页面属性,就提示找不到文件。新手的直觉是"这库有 bug",实际上是作用域问题。PyPDF2 的 Reader 不会把文件内容全部加载完,页面对象靠原始流"边走边读"。

修正方案就是那两种:把读取和操作放同一个上下文里,或者用 BytesIO 把数据装进内存。我平时写工具函数,都会把"读文件"这一步统一封装成 BytesIO 加载,这样返回的 reader 可以安全传递给任何函数使用,不会被上下文关闭问题坑到。

8.3 中文提取乱码:字体映射与 OCR 兜底

中文文档用 extract_text 得到一堆乱码或者空字符串,这是 PyPDF2 用户最常见的痛点。原因就是我在第 1 节讲的 ToUnicode CMap 映射缺失。有些 PDF 生成工具(尤其是某些扫描一体机、老式排版软件)写入字体时,没把字符编码和 Unicode 的对应关系写进文件。

我的判断流程是:先提取一两页看输出。如果乱码,马上换 OCR 思路,不要继续在 PyPDF2 里耗时间。常见做法是把 PDF 页面渲染成图片(用 PyMuPDF),再对接 RapidOCR、PaddleOCR 这类离线识别引擎。这样虽然多几步,但成功率远高于硬抠文本。记住这是 PDF 格式本身的设计问题,不是哪个库坏了。

8.4 API 张冠李戴:PyPDF2 与 pypdf 混用

这个问题相当普遍。你自己可能只装了一个,但从网上 copy 的代码 import 的是另一个。更隐蔽的是,有些环境里两个库都被间接装上了,代码用的 import 和 pip 里实际依赖的版本对不上,导致明明装了库却导入报错。

排查办法:先在终端跑 pip list | findstr PDF(Windows)或 pip list | grep -i pdf(macOS/Linux),看环境里有哪些相关库。再检查代码头部的 import 一致。统一用 PyPDF2 就全部 from PyPDF2,统一用 pypdf 就全部 from pypdf。最怕修一处漏一处,报错信息还指向另一个方法名,让人一头雾水。

8.5 排查思路速查表

我把日常高频问题整理成一张速查表,可以直接贴在笔记里:

现象可能原因第一步处理
导入报错PyPDF2 和 pypdf 混用检查 pip 列表,统一 import
访问 pages 报加密错误PDF 有口令保护先 decrypt 再操作
大文件合并卡死递归深度超限调高 recursionlimit 并分批处理
中文乱码 / 提取空白字体缺少 Unicode 映射改用 OCR 方案兜底
合并后顺序混乱文件路径排序错误用 sorted 按版本号排序
write 写入报类型错误旧 API 写了路径字符串改用文件对象

这张表我打印出来贴在工位旁边,遇到问题先对着表检查,大概率能省半小时排查时间。

我个人实际用下来的体会是:PyPDF2 不一定是最强的 PDF 库,但作为文档处理的第一把刀,它的"页面级操作"思路是最容易上手的。如果你接下来要做 PDF 自动化,我建议把解密、提取、合并、拆分这四个动作写成四个小函数,放进自己的工具模块里,以后遇到什么批量需求,直接组合调用就行。真碰到 PyPDF2 干不动的,再切 PyMuPDF 也不迟。这个系列走到 Day 44,PDF 这块的日常需求基本就通透了,下一步你可以把页面坐标和文本位置分析也学一下,那样对文档的理解会更进一层。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:58:34

车载司机疲劳检测系统:轻量级三指标融合方案

简介:本资源是一套基于Python与卷积神经网络实现的驾驶员疲劳检测与预警系统完整项目,面向计算机、人工智能及相关专业本科生开展毕业设计或课程大作业使用,聚焦真实交通场景下的实时人脸关键点定位、闭眼/打哈欠行为识别与声光预警功能。包内…

作者头像 李华
网站建设 2026/10/5 7:56:22

shadcn/ui 开源项目,专业打造专业UI

大家好,我是Java1234_小锋老师。 一篇轻松读懂 shadcn/ui 是什么、为什么火、以及怎么上手的小文。 一、先说结论:它到底是什么? 如果你去 GitHub 搜前端 UI 相关的开源项目,shadcn/ui 几乎一定会出现在热门列表里。 一句话概括…

作者头像 李华
网站建设 2026/10/5 7:56:22

C++ QT魔塔项目:内存管理与事件驱动的工程实践指南

简介:这是一份基于Qt与C开发的完整魔塔游戏源码工程,面向计算机专业本科生及初学者,适用于毕业设计、课程设计与小型桌面应用开发实践。项目采用模块化架构,包含角色控制(hero.h/cpp)、地图管理&#xff08…

作者头像 李华
网站建设 2026/10/5 7:55:30

从sqlite3到APSW:真正掌控SQLite底层能力的Python接口

最近在折腾SQLite的底层能力时,用Deep Seek把APSW和SQLite的关系捋了一遍。说实话,AI总结概念的能力确实强,把两者之间的层级关系、设计哲学讲得头头是道,但真正到了写代码、调接口、跑数据的时候,光靠那些概念总结远远…

作者头像 李华
网站建设 2026/10/5 7:55:30

OpenShell完整指南:Windows 11经典开始菜单自定义与避坑

如果你最近把主力机升级到 Windows 11,或者还在被 Windows 10 的磁贴开始菜单折磨,那你大概率听过 OpenShell 这个名字。它是已停更的 Classic Shell 的社区续作,目标很朴素:把经典的、高效的传统开始菜单重新带回到新系统上。我在…

作者头像 李华
网站建设 2026/10/5 7:55:24

基于代价的连接条件下推:多表连接SQL性能优化的关键

数据库优化这件事,做久了你会发现一个规律:80%的慢SQL不是死在单表查询上,而是死在多表连接上。尤其是那种六七张表join的大查询,哪怕每张表都建了索引,整体执行时间还是几十上百秒,换个参数换个数据量&…

作者头像 李华