☰
PyPDF2实战:用Python搞定PDF合并、拆分、加密与文本提取
2026/10/5 3:43:46 网站建设 项目流程

处理PDF一直是我在文档自动化里最想吐槽的事:看起来就是一个文件,可合并、拆分、旋转、加密、提取文字这些需求,手动操作时没有一个不是坑。后来我把重复的PDF操作全部丢给Python,这次想详细聊聊PyPDF2,一个纯Python实现的PDF基础操作库。如果你刚学完Python的文件读写和循环,正愁没有实战项目练手,或者你每天都要和PDF打交道——财务对账单、合同归档、爬虫下载的电子书、网页打印件——那这篇里的内容基本都能用上。说它是文档处理的“屠龙刀”,不是因为它能一刀秒掉所有PDF问题,而是它覆盖面足够广、学起来成本低,应急场景下非常顶用。

1. 为什么说 PyPDF2 是文档处理的屠龙刀

1.1 先搞清楚它到底能做什么

PyPDF2能做的最核心的事情,我用一句话概括:在不打开任何PDF软件的前提下,用代码完成对PDF的增删改查。具体拆开看,它有这几大能力:

  • 读取PDF的页数、标题、作者、创建时间等元数据
  • 提取文本内容,也就是“PDF解析”中最基础的那种
  • 页面级操作:旋转、裁剪、添加水印
  • 文档级操作:拆分、合并、插入空白页
  • 安全操作:加密、解密、设置权限
  • 读取书签/大纲(outline),这个在整理长文档时很好用

这个功能列表对应到热搜词里那堆“pdf解析”“pdf文档”“pdf文件转换”“PDF编辑器”的需求,基本有一半是它可以直接干掉的。比如搜狗PDF编辑器那种商业工具能干的基础活,它都能在命令行里批量完成,而且不靠GUI,跑在服务器上也没问题。

但也要说清楚它的短板。PyPDF2是“纯Python”实现,不擅长做复杂排版、生成高颜值PDF报表,更不擅长做OCR识别。扫描版PDF那种“一张图一页”的文档,它提取文字的能力基本为零。它更像个工具刀,不是绣花针,也不是挖掘机。

1.2 Python PDF生态选型:为什么这个库值得先学

很多人在学习Python时,一听到“处理PDF”就会搜到一堆库,挑花了眼。我把Python生态里常见的库整理成一张表,这样定位更清楚:

库主打方向性能适合场景
PyPDF2 / pypdf合并、拆分、加密、提取文本中等通用办公自动化,轻量任务
pdfplumber文本、表格提取较慢从PDF里精确抠表格、抠字段
PyMuPDF (fitz)渲染、提取、转换,全功能很快大量PDF处理,追求速度
ReportLab从零生成PDF快报表、发票、标签打印
pdf2docxPDF转Word中等需要把PDF变成可编辑Word

我的建议是:第一个学的PDF库一定选PyPDF2。因为它的接口是围绕“文档对象”设计的,理解它的Reader、Writer这些概念之后,再上手别的库基本上无师自通。而且它安装零依赖,pip一下就能跑,对新手极其友好。虽然真到生产环境,很多高负载任务我会换成PyMuPDF,但日常七八成的问题,PyPDF2都扛得住。

2. 动手前的三个铺垫:安装、版本与核心对象

2.1 安装和版本:先记住那个最容易踩的坑

安装没什么好说的:

pip install PyPDF2

装完验证版本:

import PyPDF2 print(PyPDF2.__version__)

真正要提醒的是版本陷阱。现在网上大量教程还在用PdfFileReader、PdfFileWriter这组类名,那是PyPDF2 1.x时代的写法。2.x之后,类名改成了PdfReader、PdfWriter、PdfMerger。照着旧教程写代码,第一行就会报AttributeError: module 'PyPDF2' has no attribute 'PdfFileReader'。

另外有个背景知识:PyPDF2前面是从PyPDF继承过来的,后面社区又分出了一个更活跃的分支叫pypdf,接口基本和PyPDF2一致。如果你新建项目,直接用pip install pypdf也没问题,代码写法几乎不用改。我自己的习惯是旧项目继续用PyPDF2,新项目看团队情况二选一,反正遇到问题可以互相参考文档。

2.2 认识三个核心对象:Reader、Writer、Merger

PyPDF2的设计思路非常清晰,无非是三个角色:

  • PdfReader:负责把已有的PDF“读”进来
  • PdfWriter:负责把页面“写”出去,生成新的PDF
  • PdfMerger:专门做合并的“合成器”

页面对应的是Page对象,页面集合挂在reader.pages上。由于PDF页面本身是复杂对象,PyPDF2读取时是惰性的,不会一次性把所有页面内容都塞进内存,大多数操作是先拿页面引用、再按需处理。

先来一个最基础的开胃菜:读取一份PDF的基本信息。

from PyPDF2 import PdfReader reader = PdfReader("example.pdf") print(f"页数:{len(reader.pages)}") print(f"PDF版本:{reader.pdf_header}") meta = reader.metadata if meta: print(f"标题:{meta.title}") print(f"作者:{meta.author}") print(f"主题:{meta.subject}") page = reader.pages[0] print(f"首页尺寸:{page.mediabox}")

这里有几个新手容易懵的点:reader.metadata可能为空,要判空再取属性;reader.pages是列表,索引从0开始;page.mediabox返回的是页面坐标矩形,左下角和右上角的坐标值,单位是“磅”。A4纸大概是595×842,记住这个比例,后面做裁剪时会用到。

注意:提取元数据时,有些PDF生成工具根本就没写标题、作者,打印出来全是None,这不代表读取失败,是文件本身就没写。

3. 四个高频操作实战:合并、拆分、加密、页面调整

3.1 拆分PDF:从整份合同里抽出关键页

我处理过一个真实场景:同事发来一份几十页的合作协议,财务只需要中间三页的付款条款和最后一页的盖章页。手动用浏览器或PDF编辑器打开再另存,来回折腾不说,还容易在微信里传错版本。用PyPDF2解决就是几十行的事。

拆分逻辑很简单:新建一个PdfWriter,从reader.pages里挑出需要的页,加进去,最后写出新文件。

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("协议全文.pdf") writer = PdfWriter() # 提取第10-12页(索引9-11)和最后一页 for i in range(9, 12): writer.add_page(reader.pages[i]) writer.add_page(reader.pages[-1]) with open("协议关键页.pdf", "wb") as f: writer.write(f)

为什么range(9, 12)能取到第10、11、12页?因为range是左闭右开,索引从0开始,所以第10页对应索引9,第12页对应索引11,写到12正好是前开后闭的终点。这个“差一位”的细节,几乎每个用PyPDF2的人都踩过。

实操中还有个常见需求:把一个多章节的PDF按固定页数切成多个小文件,比如每10页切一个。用一个循环就能批量完成:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("大文件.pdf") total = len(reader.pages) chunk_size = 10 for start in range(0, total, chunk_size): writer = PdfWriter() for page in reader.pages[start:start + chunk_size]: writer.add_page(page) filename = f"切片_{start // chunk_size + 1}.pdf" with open(filename, "wb") as f: writer.write(f)

实操心得:大批量切片前,先打印一份总页数+分片数量的清单,确认切分逻辑无误再批量跑,避免几百个文件生成后发现页码区间理解反了。

3.2 合并PDF:把分散的附件合成一个文件

合并是另一个高频操作。比如把营业执照扫描件、身份证扫描件、盖章页拼成一个申请材料包。用PdfMerger最省事:

from PyPDF2 import PdfMerger merger = PdfMerger() merger.append("营业执照.pdf") merger.append("身份证.pdf") merger.append("盖章页.pdf") merger.write("申请材料包.pdf") merger.close()

PdfMerger.append()还有一个隐藏能力,可以只合并某个文件的指定页码范围:

merger.append("长报告.pdf", pages=(0, 3))

这行代码的意思是只取长报告的第1-4页(索引0到3)参与合并,非常适合“从多个文件里各抽一部分拼成新文件”这种需求。

如果用PdfWriter手动合并也可以,逻辑类似拆分,只是反过来:循环读取每个文件的所有页面,add_page进同一个writer。两种写法的区别在于PdfMerger封装了更多细节,比如会尝试保留书签结构,而手动写更可控,内存占用上自己心里有数。

注意:PdfMerger用完要close(),或者用with语法。不然文件句柄一直占着,Windows下会出现“文件被占用,无法删除”的灵异问题。

3.3 给PDF加密码:加密与解密

给文档加密也是常见的合规需求。把内部报告发给客户前,往往要先加一层密码;批量给历史合同归档时,也会统一加密存储。

用PdfWriter把原文件重新写一遍,加上密码:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("季度报告.pdf") writer = PdfWriter() for page in reader.pages: writer.add_page(page) writer.encrypt("abc123456") with open("季度报告_加密.pdf", "wb") as f: writer.write(f)

encrypt()默认只传一个密码时,会同时作为用户密码和所有者密码。如果想精细控制,可以传两个参数:第一个是用户打开文档要输入的密码,第二个是所有者密码,用来限制打印、修改等权限。

writer.encrypt("user123", "owner456")

对应到读取端,如果拿到了一个加密PDF,PdfReader在不知道密码的情况下是读不出页面的。处理方式是先判is_encrypted,再decrypt:

from PyPDF2 import PdfReader reader = PdfReader("季度报告_加密.pdf") print(reader.is_encrypted) # True reader.decrypt("abc123456") print(f"解密后页数:{len(reader.pages)}")

很多加密文档还分“有密码”和“无密码但禁编辑”两种状态。PyPDF2绕口令一样的地方在于:有些PDF设置的只是权限密码,没有任何打开密码,此时is_encrypted可能为True,但你什么都不用输,decrypt("")传个空字符串就能正常读。

注意:PDF的加密是“防君子不防小人”,有技术能力的人总能绕过。做安全合规可以,别拿它当绝对可靠的保密手段。

3.4 页面旋转、裁剪与添加水印

这三个都属于页面级整形操作。我遇到最多的是扫描件方向颠倒,手机拍的合同传上来就是横着的,用rotate一次搞定:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("扫描件.pdf") writer = PdfWriter() for page in reader.pages: page.rotate(90) # 顺时针旋转90度,负数就是逆时针 writer.add_page(page) with open("摆正后的扫描件.pdf", "wb") as f: writer.write(f)

裁剪稍微抽象一点。page.mediabox可以理解为页面的“画布边界”,修改它的坐标值,相当于把显示区域切掉一圈,内容不会缩放,而是直接消失。比如A4页面默认坐标是左下角(0, 0)、右上角(595, 842),如果需要去掉左右多余的空白边:

page = reader.pages[0] page.mediabox.lower_left = (40, 40) page.mediabox.upper_right = (555, 800)

坐标单位是磅,1磅约等于0.35毫米。改之前先打印原坐标值,再按比例算要裁掉多少,不然反复试错很浪费时间。

水印操作更特殊,它不是用文字画上去的,而是把另一个PDF页面叠加上来。所以要先准备一个水印PDF,比如用任何工具做一个写着“内部资料”的透明底PDF:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("报告.pdf") watermark = PdfReader("水印.pdf") watermark_page = watermark.pages[0] writer = PdfWriter() for page in reader.pages: page.merge_page(watermark_page) writer.add_page(page) with open("带水印_报告.pdf", "wb") as f: writer.write(f)

这个方法原理是“页面合并”,也就是把水印页的内容画到目标页上。水印PDF本身可以做成半透明、斜着放、带logo,只要能生成PDF就行。

4. 文本提取与信息解析:从PDF里“读”出内容

4.1 extract_text()用起来简单,但别抱太高期望

提取文本是“PDF解析”里最被高估的一步。PyPDF2提供了extract_text()这个看似简单的方法:

from PyPDF2 import PdfReader reader = PdfReader("说明书.pdf") page = reader.pages[0] print(page.extract_text())

但它的核心机制是解析PDF内部的字体编码和字符映射,只有文档本身带文字层时才能提取到内容。什么情况没有文字层?扫描件、拍照文件、打印机直接“扫描成PDF”生成的文件,它们本质上是图片,PyPDF2读出来就是个空字符串。

还有一类更隐蔽的情况:文档用特殊字体嵌入,但没有提供完善的字符映射表。这时提取出来的内容可能是乱码、缺字,或者顺序错乱。中文PDF尤其容易出现这种问题,因为中文字体的编码和映射比西文复杂得多。

4.2 批量解析几十个PDF的小脚本

哪怕extract_text()有局限,“读元数据+统计页数”这种轻量解析也够日常用。我经常做的一件事是:把下载目录里一堆来源不明的PDF文件,统一列出文件名、标题、页数,方便归档。

from pathlib import Path from PyPDF2 import PdfReader for pdf_path in Path(".").glob("*.pdf"): try: reader = PdfReader(pdf_path) meta = reader.metadata title = meta.title if meta and meta.title else pdf_path.stem print(f"{pdf_path.name} | 标题: {title} | {len(reader.pages)} 页") except Exception as e: print(f"{pdf_path.name} 处理失败: {e}")

这里我用try/except包住每个文件,是因为下载的PDF不一定都完整,偶尔会有破损文件。不包异常的话,一个坏文件就能让整个批量任务中断。

这个脚本的实际价值我自己深有体会。从网上下到的许多工具书、教程PDF,文件名可能是“新建文档(3).pdf”这种,用上面的方式跑一遍,能快速识别出真实的标题和页数,再决定怎么归类。之前帮朋友整理几百本电子书,就是用这个思路做的第一轮清洗。

实操心得:如果只是统计页数,不要重复创建PdfReader,一个文件创建一次就够了。否则在几百个文件的大目录下,性能差距会非常明显。

4.3 中文乱码和提取不全时怎么补救

当我们发现PyPDF2提取文本不理想时,不建议硬刚,换个工具可能十分钟就搞定。我常用的阶梯方案是这样:

  • 文档是扫描件 → PyPDF2天然无法处理,直接上OCR工具
  • 文档有文字层但提取乱码 → 试试pdfplumber或PyMuPDF
  • 提取内容顺序错乱 → 这个难度较高,需要按坐标重排,pdfplumber更灵活
  • 只是需要表格数据 → 不要用PyPDF2,用pdfplumber的extract_table()

表格式总结如下:

场景首选方案说明
合并、拆分、加密、水印PyPDF2接口最简单,够用
文字层正常的中英文提取PyPDF2 / pdfplumber先试PyPDF2,不满意换后者
复杂表格提取pdfplumber按单元格坐标提取更准
扫描件OCRPaddleOCR / RapidOCR需要图片化处理,CPU占用较高
高性能大批量处理PyMuPDF速度比PyPDF2快一个量级

OCR这块提一句:如果搜到了“python上利用rapidocr太吃cpu”这类问题,那是另一条技术路线,和PyPDF2不冲突。PyPDF2只负责处理已有文字层的PDF,OCR负责“看图识字”,两者常常搭配使用。

5. 常见报错与排查实录

5.1 AttributeError: module 'PyPDF2' has no attribute 'PdfFileReader'

这是版本升级带来的问题,几乎每个从旧教程学起的都会撞上。解决方案很简单:把所有PdfFileReader改成PdfReader,把PdfFileWriter改成PdfWriter。

我见过最迷惑的场景是代码里既有from PyPDF2 import PdfFileReader,又在同一个文件里用了PdfReader,两个类名混着用。统一改成新版API,全局搜一遍替换就好。

5.2 读加密PDF时卡住,decrypt解不开

decrypt()返回的结果在不同版本里表达方式不一样。与其纠结返回值是0还是1,不如直接看解密后能不能取到页数:

reader = PdfReader("加密文件.pdf") if reader.is_encrypted: reader.decrypt("密码") print(len(reader.pages)) # 能打印出页数就说明密码对了

如果密码明明正确,还是提示解不开,情况多半是:PDF使用了PyPDF2不支持的加密算法。这种文件我一般用qpdf命令行工具先转换一轮:

qpdf --password=xxx --decrypt input.pdf output.pdf

转成无加密版本后,再用PyPDF2处理就顺畅了。不要重复造轮子,命令行工具该用就用。

5.3 extract_text()返回空字符串,但阅读器里能看到字

这种情况要分清楚:你自己眼睛看到的“字”,是PDF图片里的像素,还是真正的文本层?用PDF阅读器是可以直接选中的文字,那就是文本层;如果只是“看起来像字”,一选就选中整块图片,那就没有文本层。

判断方法:打开PDF,尝试用鼠标从第一个字拉选中到最后,能选中的是文字版;只能框选出一个矩形区域的,是图片版。后者PyPDF2无能为力,直接上OCR。

如果是文字版但提取出来是空,可能和字体映射有关。可以换PyMuPDF看一眼结果:

import fitz doc = fitz.open("奇怪字体.pdf") page = doc[0] print(page.get_text())

如果PyMuPDF能提取,那就说明PyPDF2对这个文件的字体支持不好;如果两者都是空的,基本可以断定文件没有文字层。

5.4 合并几千页大文件时速度慢、内存高

PyPDF2是纯Python实现,性能天花板摆在那里。真要处理上千页的大文件,我不会用PyPDF2,而是直接换PyMuPDF。但如果你暂时不想换库,记住两个优化习惯:

  • 尽量用PdfMerger.append()而不是一页页add_page()
  • 不要在循环里重复创建Reader对象,能用变量复用就复用

如果文件本身太大,还有一个思路是缩小任务粒度:先按章节拆分,再分批处理,最后重新合并。这个“分而治之”的思路比单纯优化代码更管用。

6. 从热搜词看真实场景:怎么把 PyPDF2 放进工作流

6.1 爬虫和资料整理:把下载的PDF做成可检索的归档

搜“python爬虫”相关的热搜里,很多是“下载下来的PDF怎么处理”。爬虫把文件拉下来之后,往往还要做信息提取。PyPDF2在这个环节的价值是“第一道粗加工”:读标题、统计页数、抽取前言页文本、判断文件内容是否和预期匹配。

比如批量下载了一批教程PDF,文件名千篇一律是hash值,跑一轮元数据识别,就能把真实标题打回文件名:

from pathlib import Path from PyPDF2 import PdfReader for pdf_path in Path("downloads").glob("*.pdf"): reader = PdfReader(pdf_path) meta = reader.metadata if meta and meta.title: new_name = pdf_path.with_name(f"{meta.title}.pdf") pdf_path.rename(new_name)

这样处理后,再配合全文检索工具,整个资料库才算真正“能用”。

6.2 PDF转Word:为什么说这活儿不该交给PyPDF2

热搜词里“pdf转word”搜索量一直很高。PyPDF2本身不提供转Word功能,这叫“术业有专攻”。要做转换,一般用pdf2docx库,或者先用pdfplumber/PyMuPDF把内容抽出来,再通过python-docx重建文档。

实际转换效果取决于源PDF的复杂程度。纯文本型PDF转出来效果不错;带复杂表格、图片混排的,转完大概率版式错乱,需要人工调整。我的经验是:能转,但别期待100%还原。如果是扫描件PDF,那就更别想直接转Word了,得先OCR。

所以“PDF转Word”这个需求,正确的处理方式不是找一个万能库,而是先判断PDF类型,再分别选择不同工具链。

6.3 网页打印PDF的后期处理

热搜里还有“web页面pdf打印”。很多人不知道,浏览器打印成PDF之后,经常带出一堆废页:广告页、空白的背面页、多出来的页眉页脚。我有个小习惯:网页打印的PDF,先跑一遍PyPDF2检查每页的文本量和空白面积,再自动删除连续两页都没有文字的页面:

from PyPDF2 import PdfReader, PdfWriter reader = PdfReader("网页打印.pdf") writer = PdfWriter() for i, page in enumerate(reader.pages): text = page.extract_text() if text and text.strip(): writer.add_page(page) with open("清理后.pdf", "wb") as f: writer.write(f)

这个逻辑虽然粗糙,但对付大部分网页打印件足够了。

6.4 自动化流水线里的“文档归档”环节

看到热搜里那些“网络运维7天上岗pdf”“airflow实战pdf”“ros2机器人开发从入门到实践pdf”之类的词,我想到的是另一个场景:技术团队内部经常要共享资料,文档归档要统一格式。用PyPDF2写一个归档脚本,可以把散落的PDF统一加密、加水印、按时间戳重命名,然后扔进NAS或对象存储。

比如每天凌晨跑一个定时任务,把当天生成的报表PDF:加统一水印、加密、按日期归档到指定目录。整个脚本不过几十行,配合系统任务计划程序就能稳定运行。这种自动化流水线,才是文档处理真正落地的价值。

7. 最后分享几个我自己的使用习惯

我实际使用PyPDF2的频率非常高,至少每周都会跑几次批量处理。有几个习惯是踩坑踩出来的:

第一,生产脚本里一定要包try/except。PDF文件格式是出了名的不规范,同一个版本的阅读器能打开的文件,PyPDF2未必能顺利读。批量处理时不包异常,一个坏文件就能让整个任务功亏一篑。我更倾向于记录错误文件名,跳过继续,最后统一看失败清单。

第二,重要文件先备份再操作。PyPDF2处理PDF时是直接写新文件的,原文件不动。但如果你的逻辑里把原文件重命名或者覆盖,那就危险了。我见过同事写合并脚本时,输出文件名写成和输入相同,直接把源文件覆盖掉,最后只能从回收站找。

第三,处理前先抽样测试。不管脚本逻辑有多简单,我都建议用两三个有代表性的测试文件先跑一遍,确认输出符合预期,再对全部文件操作。尤其是页码区间、裁剪坐标这类“一步错、满盘皆输”的操作,抽样测试能省下大量返工时间。

最后再补充一个大多数人不知道的细节:PyPDF2的PdfWriter也支持添加空白页,add_blank_page(width, height),在需要插入分隔页、占位页的场景很实用。配合我前面讲的拆分合并思路,很多看似复杂的PDF需求,其实两三行代码就能解决。文档处理这件事,工具不在多,把一种工具的用法吃透,就足够应对日常工作中绝大多数PDF难题了。

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

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

立即咨询