从“读写”到“思考”:Python PDF处理从入门到进阶实战(Day 43-45)
从“读写”到“思考”:Python PDF处理从入门到进阶实战(Day 43-45)
在日常办公和数据处理中,PDF(便携式文档格式)因其跨平台、不易篡改的特性,成为了最主流的文档格式之一。然而,正是这种“封闭性”使得从PDF中提取信息、对其进行批量处理变得困难。无论是财务人员需要从上千份报表中提取数据,还是行政人员需要合并分散的文档,亦或是开发者希望为文档自动化添加水印,这些需求都指向了一个核心技能:用Python高效地处理PDF文件。
在“Python办公自动化”学习路径的第四阶段(第36-50天),我们的目标是让机器人学会“读写算”,而PDF处理正是其中的硬骨头。本文将聚焦于Day 43-45的核心内容,深入浅出地讲解如何利用Python三大主流库——PyPDF2、pdfplumber 和 PyMuPDF,实现PDF的文本提取、合并/拆分以及水印添加。这不仅仅是一份API文档的罗列,更是一次从基础“读写”到场景化“思考”的进阶之旅。
温馨提示:本文所有示例代码均基于Python 3.8+及以下库的最新版本,建议你在虚拟环境中进行操作。
pip install PyPDF2 pdfplumber PyMuPDF
一、工具选型:没有最好,只有最合适
在处理PDF之前,我们需要先认识一下三位“主角”。根据一篇2025年发布的关于PDF解析工具的对比研究,不同的工具在不同的文档类型和任务上表现差异显著 。没有万能的工具,只有最合适的选择。
| 库名 | 核心优势 | 主要局限 | 最佳应用场景 |
|---|---|---|---|
| PyPDF2 | 擅长页面级操作(合并、拆分、旋转、加密解密);纯Python实现,轻量易用。 | 文本提取能力较差,尤其对中文支持不友好;无法处理扫描件;项目维护已不如前。 | PDF文档的结构化重组,如合并多个会议纪要、拆分大型手册、批量加密文件。 |
| pdfplumber | 专注于内容提取,能较好地保留文本布局,甚至支持表格解析。 | 不支持创建或修改PDF(如写入内容、添加水印)。 | 数据抽取,如从报表中提取指定段落、批量读取发票信息、解析科研论文中的表格数据。 |
| PyMuPDF | 速度极快,功能全面,底层基于MuPDF。支持文本、图像、矢量图形提取,也支持页面写入和修改。 | GPL协议对商用可能有限制(需注意);功能庞大,学习曲线稍陡。 | 高性能处理,需要对PDF进行渲染、OCR预处理、或进行复杂的图文数据提取。 |
根据官方文档的性能对比测试,在处理7031页PDF的复制(合并)任务时,PyMuPDF仅需3.05秒,而PyPDF2则需要494.04秒,速度相差超过160倍 。因此,在追求极致性能的场景下,PyMuPDF是首选。
二、基础实战:让机器学会“读”与“写”
1. 提取文本:从“看不懂”到“读得懂”
文本提取是后续数据分析的基础。对于由文本构成的PDF(非扫描件),pdfplumber通常是首选,因为它能更好地保留原文的排版顺序。
场景: 假设我们需要从一份名为report.pdf的报告中提取所有文本,并查找特定的关键词。
import pdfplumber
def extract_text_from_pdf(pdf_path):
"""
使用pdfplumber提取PDF中的所有文本
"""
full_text = []
with pdfplumber.open(pdf_path) as pdf:
print(f"文档共 {len(pdf.pages)} 页")
for i, page in enumerate(pdf.pages):
text = page.extract_text()
if text:
full_text.append(f"--- 第 {i+1} 页 ---")
full_text.append(text)
else:
print(f"警告:第 {i+1} 页未提取到文本,可能为图片页面。")
return "\n".join(full_text)
# 使用示例
if __name__ == "__main__":
extracted_content = extract_text_from_pdf("example/report.pdf")
with open("output/report_content.txt", "w", encoding="utf-8") as f:
f.write(extracted_content)
print("文本提取完成,已保存至 report_content.txt")
代码解析:
pdfplumber.open()打开文件,返回PDF对象。pdf.pages是一个包含每一页的列表。page.extract_text()是核心方法,它会分析页面上的文字块,并尝试按照阅读顺序返回文本 。- 如果遇到扫描件或图片型PDF,
extract_text()会返回空字符串,此时需要引入OCR技术(如easyocr或pytesseract)配合PyMuPDF提取图片区域进行识别 。
2. 合并与拆分:文档的“乐高”玩法
合并多个PDF和拆分单个PDF是办公中最常见的需求。PyPDF2 提供了直观的 PdfWriter 和 PdfReader 来实现这一功能。
场景 A:合并 将当前目录下的 part1.pdf、part2.pdf 合并为一个完整的 merged.pdf。
import PyPDF2
from PyPDF2 import PdfReader, PdfWriter
def merge_pdfs(pdf_list, output_filename):
"""
合并多个PDF文件
"""
pdf_writer = PdfWriter()
for pdf_file in pdf_list:
try:
pdf_reader = PdfReader(pdf_file)
# 将源PDF的所有页面添加到写入器
for page in pdf_reader.pages:
pdf_writer.add_page(page)
print(f"已添加: {pdf_file} (共 {len(pdf_reader.pages)} 页)")
except Exception as e:
print(f"处理文件 {pdf_file} 时出错: {e}")
# 写入到输出文件
with open(output_filename, 'wb') as out_file:
pdf_writer.write(out_file)
print(f"合并完成,已保存至: {output_filename}")
# 使用示例
files_to_merge = ['part1.pdf', 'part2.pdf', 'part3.pdf']
merge_pdfs(files_to_merge, 'merged.pdf')
场景 B:拆分 将一个大文件 novel.pdf 按每50页拆分成多个小文件。
import PyPDF2
from PyPDF2 import PdfReader, PdfWriter
def split_pdf(input_pdf, pages_per_split=50):
"""
按指定页数拆分PDF
"""
pdf_reader = PdfReader(input_pdf)
total_pages = len(pdf_reader.pages)
for start_page in range(0, total_pages, pages_per_split):
pdf_writer = PdfWriter()
end_page = min(start_page + pages_per_split, total_pages)
for page_num in range(start_page, end_page):
pdf_writer.add_page(pdf_reader.pages[page_num])
output_filename = f"split_{start_page+1}_to_{end_page}.pdf"
with open(output_filename, 'wb') as out_file:
pdf_writer.write(out_file)
print(f"已生成: {output_filename} (包含 {end_page - start_page} 页)")
# 使用示例
split_pdf('novel.pdf', 50)
进阶提示: 在合并时,如果遇到需要压缩的情况,可以先用 PyMuPDF 对生成的临时文件进行压缩处理,通过 garbage=4, deflate=True 参数清理冗余数据 。
三、进阶技巧:添加水印与内容保护
为PDF添加水印是保护知识产权或标识文档状态的常用手段。PyMuPDF 和 PyPDF2 都支持此功能,但PyMuPDF提供了更精细的控制。
核心思想: 创建一个包含水印图形或文字的PDF页面(水印模板),然后将此页面作为“印章”合并到目标文档的每一页上。
方法一:使用 PyPDF2 添加简单水印
import PyPDF2
def add_watermark_pypdf2(input_pdf, watermark_pdf, output_pdf):
"""
使用PyPDF2为PDF添加水印(水印需提前制作成PDF)
"""
# 读取水印文件
watermark_obj = PyPDF2.PdfReader(watermark_pdf).pages[0]
# 读取源文件
pdf_reader = PyPDF2.PdfReader(input_pdf)
pdf_writer = PyPDF2.PdfWriter()
for page in pdf_reader.pages:
# 将水印页面合并到当前页面
page.merge_page(watermark_obj)
pdf_writer.add_page(page)
with open(output_pdf, 'wb') as out_file:
pdf_writer.write(out_file)
print(f"水印添加完成,已保存至: {output_pdf}")
# 使用示例(需要预先准备一个名为 watermark.pdf 的水印文件)
# add_watermark_pypdf2('document.pdf', 'watermark.pdf', 'document_watermarked.pdf')
方法二:使用 PyMuPDF 动态创建并添加文字水印
PyMuPDF的强大之处在于它可以直接在内存中创建包含特定文字的水印,无需依赖外部水印PDF文件。
import fitz # PyMuPDF
def add_text_watermark_pymupdf(input_pdf, output_pdf, watermark_text="CONFIDENTIAL"):
"""
使用PyMuPDF动态创建文字水印并添加到每一页
"""
# 打开源文档
doc = fitz.open(input_pdf)
# 创建水印形状参数
# 获取第一页的尺寸,用于确定水印位置
first_page = doc[0]
rect = first_page.rect # 页面矩形区域
width, height = rect.width, rect.height
# 定义水印的插入点(例如页面中心)
# 这里我们创建一个大的矩形,几乎覆盖整个页面,并设置透明度
text_rect = fitz.Rect(0, 0, width, height) # 覆盖全页
for page_num in range(len(doc)):
page = doc[page_num]
# 向页面插入文本,设置旋转45度、颜色浅灰、透明度高
page.insert_textbox(
text_rect, # 文本框位置
watermark_text, # 水印文字
fontsize=60, # 字体大小
rotate=45, # 旋转角度
fontname="helv", # 字体
fontfile=None,
align=fitz.TEXT_ALIGN_CENTER, # 居中对齐
color=(0.7, 0.7, 0.7), # 浅灰色 (RGB 值范围 0-1)
overlay=True, # 作为覆盖层(True=覆盖在原文之上)
stroke_opacity=0.3, # 笔触透明度
fill_opacity=0.3 # 填充透明度
)
# 保存修改后的文档
doc.save(output_pdf, garbage=4, deflate=True)
doc.close()
print(f"动态文字水印添加完成: {output_pdf}")
# 使用示例
add_text_watermark_pymupdf('report.pdf', 'report_watermarked.pdf', '内部资料 严禁外传')
代码亮点:
page.insert_textbox()提供了丰富的排版参数,让我们可以自由控制文字的样式、位置和透明度。overlay=True确保水印浮在原文上方,而不会替换原文。- 最后的
doc.save()参数garbage=4, deflate=True能有效压缩文件体积,一举两得 。
四、综合实战:构建一个PDF批量处理工具箱
理论结合实践,我们来设计一个完整的脚本,它能够接收用户指令,对一个文件夹内的所有PDF进行批量处理。这个脚本将整合我们之前学到的所有技能。
需求描述: 用户有一个source_pdfs文件夹,其中包含多个PDF。用户希望:
- 提取所有PDF的文本,合并成一个总的报告文本。
- 将所有PDF文件合并成一个
all_merged.pdf文件。 - 为合并后的总文件添加“公司机密”的文字水印。
import os
import pdfplumber
import PyPDF2
import fitz # PyMuPDF
from pathlib import Path
class PDFBatchProcessor:
"""
PDF批量处理器:集成了文本提取、合并、水印功能
"""
def __init__(self, source_dir, output_dir="output"):
self.source_dir = Path(source_dir)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True) # 确保输出目录存在
self.pdf_files = list(self.source_dir.glob("*.pdf"))
if not self.pdf_files:
raise FileNotFoundError(f"在 {source_dir} 中未找到PDF文件")
print(f"找到 {len(self.pdf_files)} 个PDF文件待处理。")
def extract_all_texts(self, output_txt="all_texts.txt"):
"""提取所有PDF的文本并合并到一个TXT文件"""
print("\n--- 开始批量提取文本 ---")
combined_text = []
for pdf_file in self.pdf_files:
print(f"正在处理: {pdf_file.name}")
try:
with pdfplumber.open(pdf_file) as pdf:
file_text = [f"===== 文件:{pdf_file.name} (共{len(pdf.pages)}页) ====="]
for i, page in enumerate(pdf.pages):
text = page.extract_text()
if text:
file_text.append(f"--- 第{i+1}页 ---")
file_text.append(text)
combined_text.extend(file_text)
except Exception as e:
combined_text.append(f"!!! 处理 {pdf_file.name} 时出错:{e} !!!")
output_path = self.output_dir / output_txt
with open(output_path, 'w', encoding='utf-8') as f:
f.write("\n".join(combined_text))
print(f"文本提取完成,已保存至: {output_path}")
def merge_all_pdfs(self, output_pdf="all_merged.pdf"):
"""合并所有PDF文件"""
print("\n--- 开始合并PDF ---")
pdf_writer = PyPDF2.PdfWriter()
for pdf_file in self.pdf_files:
print(f"正在添加: {pdf_file.name}")
try:
pdf_reader = PyPDF2.PdfReader(pdf_file)
for page in pdf_reader.pages:
pdf_writer.add_page(page)
except Exception as e:
print(f"警告: 添加 {pdf_file.name} 失败,{e}")
output_path = self.output_dir / output_pdf
with open(output_path, 'wb') as out_file:
pdf_writer.write(out_file)
print(f"合并完成,临时文件已保存至: {output_path}")
return output_path # 返回路径供下一步使用
def add_watermark_to_pdf(self, input_pdf_path, watermark_text="COMPANY CONFIDENTIAL"):
"""为指定的PDF添加文字水印(使用PyMuPDF)"""
print(f"\n--- 开始为 {input_pdf_path.name} 添加水印 ---")
# 打开源文档
doc = fitz.open(input_pdf_path)
# 获取页面尺寸
first_page = doc[0]
rect = first_page.rect
width, height = rect.width, rect.height
text_rect = fitz.Rect(0, 0, width, height)
for page_num in range(len(doc)):
page = doc[page_num]
page.insert_textbox(
text_rect,
watermark_text,
fontsize=50,
rotate=30,
fontname="helv",
align=fitz.TEXT_ALIGN_CENTER,
color=(0.8, 0.8, 0.8), # 更浅的灰色
overlay=True,
stroke_opacity=0.25,
fill_opacity=0.25
)
output_path = self.output_dir / f"watermarked_{input_pdf_path.name}"
doc.save(output_path, garbage=4, deflate=True)
doc.close()
print(f"水印添加完成,最终文件已保存至: {output_path}")
return output_path
def run_full_pipeline(self):
"""执行完整流程:提取文本 -> 合并PDF -> 添加水印"""
print("="*50)
print("启动PDF批量处理全流程")
print("="*50)
# 1. 提取文本
self.extract_all_texts()
# 2. 合并PDF
merged_file = self.merge_all_pdfs()
# 3. 为合并后的文件添加水印
final_file = self.add_watermark_to_pdf(merged_file, "内部机密 - 严禁外传")
print("\n" + "="*50)
print(f"全部处理完成!最终文件:{final_file}")
print("="*50)
# 使用示例
if __name__ == "__main__":
# 初始化处理器,指定源文件夹和输出文件夹
processor = PDFBatchProcessor(source_dir="./source_pdfs", output_dir="./my_output")
# 执行全流程
processor.run_full_pipeline()
在这个综合案例中,我们不仅复用了之前的函数,还引入了面向对象的编程思想,将功能封装在 PDFBatchProcessor 类中。这使得代码结构清晰,易于扩展。例如,你可以轻松地在 run_full_pipeline 方法中添加一个新的步骤——比如利用 pdfplumber 的表格提取功能,将特定页面的数据存入Excel 。
更多推荐


所有评论(0)