news 2026/10/7 17:47:26

Python PDF处理全攻略:从文本提取到批量自动化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python PDF处理全攻略:从文本提取到批量自动化

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报PDFInfoNotInstalledErrorpoppler未安装或路径未指定安装poppler并传入poppler_path参数
合并后的PDF体积巨大子文件中含大量高清图片先压缩图片再合并,或调整子文件DPI
reportlab中文显示为方框未注册中文字体用TTFont注册系统中文字体文件
提取表格错位表格合并单元格或跨页用extract_tables({})调整参数,逐页测试

再补充两个经验性的判断。如果你发现PDF文本提取出来每行前面有大量空格,这通常是原PDF排版用了“分散对齐”或者特别宽的缩进,不是代码bug。如果你连续处理十来个PDF都没问题,但突然某个文件卡住不动,优先怀疑文件本身是损坏的,挂上超时机制或分块处理更稳妥。

关于OCR,如果确实遇到扫描件,推荐组合使用pdf2image把页面转成图片,再用Tesseract或PaddleOCR识别文字。这条路可以延伸出很多内容,但这篇文章不做展开,记得有这个方向就行。

最后分享一个我自己习惯的小技巧:处理重要的PDF文件之前,先复制一份出来在副本上做实验。PDF处理库的写操作偶尔会受底层文件锁定、编码问题影响,直接在原文件上操作一旦出错,找回原文件会非常麻烦。在我做过的所有PDF自动化项目里,这个习惯帮我避免了不少事故。

如果这篇文章能帮你把日常的PDF手工操作替换成脚本,那它的目标就达到了。后续可以沿着“PDF表格数据入库”“合同关键信息抽取”“批量生成带图表的报告”这些方向继续深入,核心基础都是这里的库和思路。

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

高校实验室管理系统:ThinkPHP+Laravel双后端与微信小程序实战

高校实验室管理系统:ThinkPHP Laravel 双后端 微信小程序实战复盘前阵子接了一个高校实验室管理系统的项目,标题很直白:Thinkphp和Laravel框架微信小程序的高校实验室管理系统设计与实现。项目规模不大不小,但很有代表性——后端…

作者头像 李华
网站建设 2026/10/7 17:43:06

AI写论文先解决LaTeX适配:从工具选择到格式合规的完整指南

用AI写论文这事,我劝你先解决LaTeX适配,再谈“合规” 最近后台好多朋友问我同一个问题:论文初稿用AI生成倒是快,可一旦要投期刊、交学校盲审,格式细节就全崩了。图不听话、公式乱码、页眉字号不对、表格跨页不处理………

作者头像 李华
网站建设 2026/10/7 17:42:20

MOS管开关电路实战解析:NMOS/PMOS导通、驱动与防坑指南

做硬件的人绕不开MOS管开关电路。我在实验室第一次用PMOS管做12V电源开关时,就因为没搞懂“关断”条件,板子上电后负载一直有输出,差点把后面一级电路烧了。后来回头查资料才明白,PMOS关断不是“给高电平就能关”,而是…

作者头像 李华
网站建设 2026/10/7 17:42:09

D435i与IMU联合标定实战:用Kalibr实现高精度VIO和手眼协同

1. 这不是“调个参数就完事”的标定,而是让D435i和IMU真正“说同一种语言” 你手里的Intel RealSense D435i,镜头清晰、深度稳定、USB供电即插即用,是很多机器人、SLAM、AR项目里最常被选中的RGB-D相机。但如果你只把它当个“高清摄像头”用&…

作者头像 李华
网站建设 2026/10/7 17:42:07

单电源运放偏置实战:LM358交流放大电路1/2 VCC偏置设计与调试

1. 单电源运放偏置:一个被低估的实战门槛 很多人第一次用LM358搭交流放大电路时,都会遇到一个非常迷惑的现象:电路明明在仿真里跑得好好的,焊出来之后示波器一看,波形要么削顶,要么底部被压扁,要…

作者头像 李华
网站建设 2026/10/7 17:41:31

Kotlin伴生对象完全解析:从零理解companion object与Java static的区别

聊到Kotlin伴生对象,我总想起第一次在项目里看到 companion object 时的那种困惑:为什么类的内部会嵌一个 object ?这东西和 Java 的 static 到底差在哪?后来在 Android 和后端项目里写得多了,才慢慢摸透它背后的…

作者头像 李华