1. 项目概述与准备工作
说到用Python处理PDF,很多人的第一反应是“装个库调函数就完事了”。真上手做几个实际项目之后你会发现,PDF这个东西远没有想象中那么规矩——有的PDF是文字流,有的是扫描图片,有的带密码,有的排版乱七八糟,光是搞清楚“你到底要处理哪种PDF”就能省下一半时间。
这篇文章围绕“Python处理PDF文件”展开,覆盖文本提取、拆分合并、旋转裁剪、加密解密、格式转换、PDF生成等核心操作。适合三类人阅读:一是工作中经常跟PDF打交道、想把重复操作脚本化的办公人员,二是需要批量处理电子文档的初级开发者,三是想系统掌握Python PDF处理技术栈、准备做自动化工具的学习者。
开始之前先把环境准备好。我推荐用Python 3.9以上版本,不是必须,但新版本的PyPDF2(现在叫pypdf)和老版本API差异较大,网上很多教程用的还是旧调用方式,直接复制会报错。如果你还没有Python环境,去官网下载安装包,安装时勾选“Add Python to PATH”,省得后面命令行找不到命令。
1.1 依赖库安装清单
处理PDF的Python库不少,功能各有侧重,我建议一次装齐常用的一套:
pip install pypdf pdfplumber reportlab pdf2image python-docx Pillow逐个说下它们各自干什么的:
pypdf:负责PDF的拆分、合并、旋转、裁剪、加密解密,纯Python实现,轻量。pdfplumber:文本提取和表格识别,比对PyPDF2要强很多,适合做内容解析。reportlab:PDF生成工具,从零创建PDF、画表格、写报告都靠它。pdf2image:把PDF页面转成图片,底层依赖poppler,后面会专门讲怎么装。python-docx:严格说它处理的是Word文档,但PDF转Word这一类的任务离不开它。Pillow:图片处理库,PDF转图片之后如果需要裁剪、加水印、调分辨率,用它。
这里说一个容易踩的坑:不要把PyPDF2和pypdf混着装。两个库的历史有点渊源,但接口不兼容,网上旧教程里的PyPDF2.PdfFileReader()在新版本里已经不能用了。我的建议是统一用pypdf,它还在持续维护。
1.2 pdfplumber的安装与基础验证
装好之后先跑个简单的验证,确保环境没问题。随便找一个PDF文件,放在项目目录下命名为sample.pdf,执行:
import pdfplumber with pdfplumber.open("sample.pdf") as pdf: print(f"页数: {len(pdf.pages)}") first_page = pdf.pages[0] text = first_page.extract_text() print(text[:500])如果能看到页数和文本内容,说明环境就绪。这里有个小细节,extract_text()返回的是字符串,如果页面是扫描图片(也就是没有文字层),返回的结果是None或空字符串,这不是代码的问题,而是PDF本身的类型所限,后面会讲怎么处理。
2. PDF文本提取——从文本流到可用数据
文本提取是整个PDF处理里最常用的需求。你会发现,并不是所有PDF都能直接提取出文字。这背后涉及PDF文件的两种构造方式:文本型PDF和扫描型PDF。
文本型PDF内部存的是字符编码和位置信息,好比一个排版好的Word文档导出,每个字都有对应的Unicode编码,所以提取起来直接、准确。扫描型PDF本质上是图片,里面没有文字层,你看到的字其实是图像上的像素点,提取文本需要先做OCR识别。
pdfplumber在这个领域表现很稳,我重点讲它的用法。
2.1 提取纯文本内容
import pdfplumber with pdfplumber.open("report.pdf") as pdf: full_text = [] for page in pdf.pages: page_text = page.extract_text() if page_text: full_text.append(page_text) result = "\n".join(full_text) with open("output.txt", "w", encoding="utf-8") as f: f.write(result)如果只是简单提取,这段就够了。但实际项目中你会发现几个问题:
- 页眉页脚混入正文,比如“第X页 共Y页”这种噪音字符。
- 表格数据被拆得七零八落,某些行的数据拼错位。
- 多栏排版的文档,提取顺序是乱的,左边一栏还没读完就跳到右边。
我的处理方式分两步。第一步是用page.crop()把页眉页脚区域裁掉,只保留正文区域。第二步是用page.extract_tables()单独处理表格,不要和extract_text()混在一起。
2.2 处理报表型的PDF:用extract_tables提取表格
pdfplumber对表格的提取能力在开源库里属于头部水平。它的原理基于坐标分析——把页面上文字的几何位置和直线的交叉点进行匹配,找出表格的边界和单元格内容。
import pdfplumber with pdfplumber.open("financial_report.pdf") as pdf: page = pdf.pages[0] tables = page.extract_tables() for table in tables: for row in table: cleaned_row = [cell.replace("\n", " ") if cell else "" for cell in row] print(cleaned_row)这里我踩过一个坑:表格单元格里的换行符会导致数据错位,所以在打印或写入CSV之前,最好对每个单元格做一次replace("\n", " ")处理。另外extract_tables()返回的是列表的列表,最外层是多个表格,如果页面里只有一个表格,也要记得索引是tables[0]。
2.3 提取指定区域的内容
有些场景不需要整页文本,只需要页面上某个固定区域的数据。比如从银行对账单里提取交易明细,每笔交易的位置都是固定的。这时候用crop按坐标裁剪,效率远高于整页提取再正则匹配。
import pdfplumber with pdfplumber.open("bank_statement.pdf") as pdf: page = pdf.pages[0] # 裁剪区域:左边界120,上边界150,右边界500,下边界720 cropped = page.crop((120, 150, 500, 720)) text = cropped.extract_text() print(text)坐标单位是PDF文档的单位(通常是point,1 point = 1/72英寸),不是你屏幕上的像素。这个参数需要反复试,建议先在PDF阅读器里用截图工具量一下大概位置,再微调数值。
3. PDF文档操作:拆分、合并、旋转、加密
这一部分是pypdf的主场。它的设计思路很轻巧,把PDF文件看作一个对象容器,页面可以自由增删排序。
3.1 拆分PDF:提取指定页面并另存
比如把一本手册的第3到第7页拆出来单独发给用户:
from pypdf import PdfReader, PdfWriter reader = PdfReader("manual.pdf") writer = PdfWriter() for page_num in range(2, 7): # 第3到第7页,索引从0开始 writer.add_page(reader.pages[page_num]) with open("extracted_pages.pdf", "wb") as f: writer.write(f)注意range(2, 7)是左闭右开区间,对应的是第3页到第7页。这个错误特别隐蔽,我曾经把页码写错了,发给客户之后才发现内容少了一页。
3.2 合并PDF:把多个文件拼成一个
from pypdf import PdfReader, PdfWriter file_list = ["chapter1.pdf", "chapter2.pdf", "chapter3.pdf"] writer = PdfWriter() for file_name in file_list: reader = PdfReader(file_name) for page in reader.pages: writer.add_page(page) with open("merged.pdf", "wb") as f: writer.write(f)这里有个值得说的细节:PdfWriter可以理解为内存中的“草稿本”,所有页面先放进它里面,最后统一落盘。如果你的PDF页数很多(比如几十个文件合并成几百页),内存占用会明显上升,但普通办公场景不用太担心。
合并时容易忽略的是文件大小。如果每个子文件都有很多大尺寸图片,合并后的文件体积可能翻倍。我在实践中发现,可以先对子文件做压缩或优化再合并,效果更可控。
3.3 旋转与裁剪页面
from pypdf import PdfReader, PdfWriter reader = PdfReader("scan.pdf") writer = PdfWriter() for page in reader.pages: # 顺时针旋转90度 page.rotate(90) writer.add_page(page) with open("rotated.pdf", "wb") as f: writer.write(f)这个功能常用于纠正扫描件的方向。有一种情况是扫描仪自动进纸时把偶数字页扫成倒的,处理逻辑就是判断页码奇偶,奇数页转0度,偶数页转180度,这个脚本跑起来比手动一页页旋转舒服得多。
裁剪和旋转类似,用page.cropbox属性设置,也不难。
3.4 加密与解密PDF
from pypdf import PdfReader, PdfWriter reader = PdfReader("important.pdf") writer = PdfWriter() for page in reader.pages: writer.add_page(page) # 设置用户密码和所有者密码 writer.encrypt("user_password", "owner_password") with open("encrypted.pdf", "wb") as f: f.write(writer)user_password是打开文档需要输入的密码,owner_password是用来执行权限操作(比如打印、复制)的密码。解密则简单得多:
reader = PdfReader("encrypted.pdf") if reader.is_encrypted: result = reader.decrypt("user_password") print("解密结果:", result) with open("decrypted.pdf", "wb") as f: writer = PdfWriter() for page in reader.pages: writer.add_page(page) writer.write(f)需要注意,有些PDF虽然加密了,但密码为空。decrypt("")在某些版本下返回PasswordType.NOT_DECRYPTED,但页面仍然可以读取,这类边界情况建议在实际处理前先做个探测。
4. PDF格式转换:转Word、转图片
格式转换是很多人最先接触PDF处理的原因:老板发了份PDF让你改成Word,或者要把PDF页面转成长图发给别人。这部分我把两套链路讲清楚。
4.1 PDF转Word:原理与实现
先说透一个概念:PDF转Word不是“打开另存为”,而是重新排版。PDF的文件结构是一个个带坐标的文字块,Word是基于流式排版,文字像水流一样自适应换行。所以转换的过程中必然要经历“从坐标定位到自然阅读顺序”的重组。
我的实现思路分三步:用pdfplumber提取文字内容和图片;用python-docx创建Word文档;把文字按段落写入,把图片插入到对应位置。
import pdfplumber from docx import Document from docx.shared import Inches pdf = pdfplumber.open("input.pdf") doc = Document() for page in pdf.pages: text = page.extract_text() if text: for line in text.split("\n"): doc.add_paragraph(line) for img in page.images: # 提取图片到本地 bbox = (img["x0"], img["top"], img["x1"], img["bottom"]) cropped = page.crop(bbox).to_image(resolution=150) img_path = f"img_{page.page_number}_{img['name']}.png" cropped.save(img_path) doc.add_picture(img_path, width=Inches(4)) pdf.close() doc.save("output.docx")说实话,这个方案对单栏、纯文本类的PDF效果不错,但对复杂版式会有排版错位。如果对保真度要求很高,建议用Adobe或WPS这类商业方案的SDK,Python方案更适合“格式化程度不高但内容要能用”的场景。
4.2 PDF转图片:pdf2image使用全解
把PDF段落转成PNG图片,常用于分享预览、生成封面图、或者做OCR前预处理。
from pdf2image import convert_from_path images = convert_from_path("input.pdf", dpi=150) for i, image in enumerate(images): image.save(f"page_{i+1}.png", "PNG")关键的坑在安装环节:pdf2image底层调用的是poppler工具集,不装它的话会报PDFInfoNotInstalledError。
Windows下的解决方案是:下载poppler的Windows编译版本,解压得到bin目录,然后把路径传给convert_from_path:
images = convert_from_path("input.pdf", dpi=150, poppler_path=r"C:\poppler\bin")这步不处理好会卡很久,网上搜到的方案五花八门,很多都是老版本的教程。
4.3 网页打印PDF的思路扩展
从热搜词来看,很多人关心“web页面pdf打印”,这个和Python关系不大,更多是前端领域的工作——浏览器打印、分页CSS、甚至是直接调用浏览器的window.print()。但如果要做服务端自动生成网页内容的PDF,常见方案是Playwright或Selenium无头浏览器把HTML渲染成PDF,这个思路和本文的库不冲突,可以作为后续进阶学习方向。
5. 使用reportlab生成PDF文档
说完读取和转换,再来看看如何“从零造一个PDF”。reportlab是Python里最成熟的PDF生成库,底层直接写PDF语法,运行速度快,生成的文件也稳定。
5.1 第一个PDF:文字和段落
from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas c = canvas.Canvas("first.pdf", pagesize=A4) width, height = A4 c.drawString(100, height - 100, "Hello, PDF!") c.showPage() c.save()这段代码会在页面左上角画出一行文字。drawString接受x和y坐标,坐标系的原点在页面左下角,这和很多图像处理库的原点位置不一样。初学时容易在这个地方犯迷糊,把文字画到页面外面去。
5.2 中文内容:字体注册是重点
reportlab默认字体不支持中文,直接drawString写中文汉字会显示成框框。解决办法是注册中文字体:
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas # 注册中文字体,Windows系统自带 pdfmetrics.registerFont(TTFont("SimHei", "C:/Windows/Fonts/simhei.ttf")) c = canvas.Canvas("chinese.pdf", pagesize=A4) width, height = A4 c.setFont("SimHei", 14) c.drawString(100, height - 100, "你好,Python PDF 操作") c.save()字体路径在不同系统上不一样,Windows常用的是simhei.ttf(黑体)或simsun.ttc(宋体),Mac和Linux需要自行下载字体文件。这个知识点在热搜词“pdf图片中文设置”里也被反复提到,核心其实就是字体注册。
5.3 用Table构建结构化报告
如果要做一份数据报表,用表格是最好的表达方式:
from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph from reportlab.lib.styles import getSampleStyleSheet doc = SimpleDocTemplate("table_report.pdf", pagesize=A4) data = [ ["产品", "销量", "增长率"], ["A产品", "1200", "15%"], ["B产品", "800", "8%"], ["C产品", "1500", "22%"], ] table = Table(data, colWidths=[150, 100, 100]) table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.grey), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, 0), 12), ("BOTTOMPADDING", (0, 0), (-1, 0), 12), ("GRID", (0, 0), (-1, -1), 0.5, colors.black), ])) doc.build([Paragraph("月度销售报表", getSampleStyleSheet()["Title"]), table])这里用SimpleDocTemplate配合Paragraph和Table,比单纯用Canvas灵活很多——段落可以自动换行,表格可以跨页拆分。实际项目中我大部分报告都是用这套组合做的。
6. 综合实战:批量处理某个目录下的PDF文件
把所有基础能力串起来,做一个真实的批量任务。假设场景是这样的:你有一个documents/目录,里面散落了上百个PDF,需要做下面这些操作:
- 提取每个PDF的第一页文本,判断文件的主题类型。
- 将所有加密的PDF统一解密(密码是
backup2024)。 - 按订单号合并到对应的汇总PDF里。
- 生成一个清单表格PDF,记录每个文件的处理结果。
import os from pypdf import PdfReader, PdfWriter import pdfplumber from reportlab.lib.pagesizes import A4 from reportlab.platypus import SimpleDocTemplate, Table input_dir = "documents" output_dir = "processed" os.makedirs(output_dir, exist_ok=True) summary_data = [["文件名", "页数", "处理状态"]] for file_name in os.listdir(input_dir): if not file_name.endswith(".pdf"): continue file_path = os.path.join(input_dir, file_name) try: # 读取基本信息 reader = PdfReader(file_path) page_count = len(reader.pages) # 处理加密 if reader.is_encrypted: reader.decrypt("backup2024") # 提取第一页文本 with pdfplumber.open(file_path) as pdf: first_text = pdf.pages[0].extract_text() topic = first_text.split("\n")[0][:30] if first_text else "无文本层" summary_data.append([file_name, str(page_count), f"成功: {topic}"]) except Exception as e: summary_data.append([file_name, "0", f"失败: {str(e)}"]) # 汇总表输出 doc = SimpleDocTemplate(os.path.join(output_dir, "summary.pdf"), pagesize=A4) table = Table(summary_data, colWidths=[200, 60, 220]) doc.build([table])这算是把前面几章内容做了一次融合。只有单一技能点容易发现不了问题,一旦把这些操作串成一个工作流,你会发现真正的瓶颈往往在“数据异常处理”上——某一份PDF损坏了、某一页是空白、某一个文件实际上是PPT伪装的,这些情况都得靠try-except兜住。
我在实际跑这种批量任务时,习惯把所有异常记录下来而不是中断整个流程。大批量数据处理的最重要原则是能跑完比什么都重要,遇到个别文件失败,先跳过,跑完再排查问题。
7. 常见问题与排查技巧实录
PDF处理领域的报错信息往往词不达意,我把平时最常遇到的情况整理成一张速查表,按排查优先级排列:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
pdfplumber.open()正常但extract_text()返回空 | 扫描型PDF,没有文字层 | 接入OCR工具 |
| 提取出中文变成乱码 | 字体编码映射不支持 | 先pdf.info查看编码,尽量用pdfplumber新版 |
pypdf报FileNotDecryptedError | 文件加密但未解密就读取页面 | 先调用decrypt()再访问.pages |
PdfReader读出的页数不对 | 文件有损坏或有交叉引用 | 用其他工具(如Acrobat)修复后再处理 |
convert_from_path报PDFInfoNotInstalledError | poppler未安装或路径未指定 | 安装poppler并传入poppler_path参数 |
| 合并后的PDF体积巨大 | 子文件中含大量高清图片 | 先压缩图片再合并,或调整子文件DPI |
| reportlab中文显示为方框 | 未注册中文字体 | 用TTFont注册系统中文字体文件 |
| 提取表格错位 | 表格合并单元格或跨页 | 用extract_tables({})调整参数,逐页测试 |
再补充两个经验性的判断。如果你发现PDF文本提取出来每行前面有大量空格,这通常是原PDF排版用了“分散对齐”或者特别宽的缩进,不是代码bug。如果你连续处理十来个PDF都没问题,但突然某个文件卡住不动,优先怀疑文件本身是损坏的,挂上超时机制或分块处理更稳妥。
关于OCR,如果确实遇到扫描件,推荐组合使用pdf2image把页面转成图片,再用Tesseract或PaddleOCR识别文字。这条路可以延伸出很多内容,但这篇文章不做展开,记得有这个方向就行。
最后分享一个我自己习惯的小技巧:处理重要的PDF文件之前,先复制一份出来在副本上做实验。PDF处理库的写操作偶尔会受底层文件锁定、编码问题影响,直接在原文件上操作一旦出错,找回原文件会非常麻烦。在我做过的所有PDF自动化项目里,这个习惯帮我避免了不少事故。
如果这篇文章能帮你把日常的PDF手工操作替换成脚本,那它的目标就达到了。后续可以沿着“PDF表格数据入库”“合同关键信息抽取”“批量生成带图表的报告”这些方向继续深入,核心基础都是这里的库和思路。