尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

用Python PyPDF2一把梭:PDF拆分合并、文本提取与加密解密实战 今天这篇是文档自动化系列的第 44 天主题是 PDF。上午同事发来三十几份盖章扫描件要求按编号拆开、把第一页的金额字段抓出来汇总到表格手点肯定是不现实的我直接在 Python 里用 PyPDF2 一把梭搞定。标题里喊它文档处理的屠龙刀我觉得一点不夸张——日常能遇到的 PDF 大多数操作读页数、提取文本、合并、拆分、旋转、裁剪、上锁加密它都能处理而且 API 简单到小白也能直接上手。这篇笔记适合两类人一是 Python 基础学到函数、文件读写阶段、想找个真实练手场景的学习者二是被 PDF 工作流折磨的运营、财务、文档岗想用 Python 把自己从重复劳动里捞出来。下面我按从安装到实战的顺序把 PyPDF2 的用法、踩过的坑、以及它和其他 PDF 库的分工一次讲清楚。1. PDF 为什么难搞先搞懂它再上手 PyPDF21.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 PdfReaderpdf.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 块里或者先把文件内容读进内存再用 BytesIOimport 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, start1): text page.extract_text() print(f--- 第 {i} 页 ---) print(text)但我要泼一盆冷水这个提取是尽力而为。对排版规整、字体映射完整的 PDF效果很好对双栏排版、表格、扫描件结果可能乱成一团。文本顺序被打乱、段落间多出奇怪换行、中文变空白都属于常见现象。我的实操习惯是先只提取前两三页打印看看确认输出顺序能接受再跑全量。别上来就全量导出一堆数据最后发现顺序是乱的返工成本很高。如果你要做的是从特定区域取文本PyPDF2 做不到那得换 pdfplumber 按坐标切割。4. 合并与拆分 PDF最常用的两个操作4.1 合并多个 PDFPdfMerger 一行接一行合并是办公场景里的高频需求把多个合同附件合成一份、把扫描分段的文件拼起来、把日报汇总成月报。PyPDF2 提供了 PdfMerger写法非常直白from PyPDF2 import PdfMerger merger PdfMerger() for pdf in (contract1.pdf, contract2.pdf, contract3.pdf): merger.append(pdf, import_bookmarksFalse) with open(all-in-one.pdf, wb) as f: merger.write(f) merger.close()append 按你给的顺序追加整个文件。import_bookmarksFalse 是我习惯加的不然原 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(fpage_{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文本坐标、表格解析很强不适合修改页面结构抓表格、按坐标提取信息PyMuPDFfitz速度快、渲染、注释、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 PDFWindows或 pip list | grep -i pdfmacOS/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 44PDF 这块的日常需求基本就通透了下一步你可以把页面坐标和文本位置分析也学一下那样对文档的理解会更进一层。
返回列表