← 返回自动化办公目录
← 返回学习笔记首页
专题: Python 自动化办公系统学习
关键词: Python, 自动化办公, PyPDF2, PDF处理, PDF合并, PDF拆分, PDF加密, Python办公
一、PyPDF2概述
PyPDF2是Python生态中最成熟的PDF处理库之一,它提供了纯Python实现的PDF文档操作能力,无需依赖任何外部工具(如Adobe Acrobat、Ghostscript等)。PyPDF2的前身是pyPdf,经过社区持续开发和重构后演变为PyPDF2,并最终发展出PyPDF4等分支。目前最新的稳定版本为PyPDF2 3.x系列,与原版相比在API设计上更加现代化,类型提示更加完整,性能也有显著提升。
与其他PDF处理库相比,PyPDF2的优势在于安装简单(纯Python实现,无系统级依赖)和功能全面(支持合并、拆分、旋转、裁剪、加密、水印等常用操作)。相对而言,pdfminer更专注于文本提取,reportlab专注于PDF生成,而PyPDF2则在已有PDF文件的"编辑"场景中最为得心应手。需要注意的是,PyPDF2对PDF表单(AcroForm)和复杂注释的支持有限,对于这类需求可能需要结合pdfminer或pdfplumber使用。
安装与版本选择
PyPDF2的安装非常简单,直接使用pip即可。建议使用Python 3.7及以上版本,以获得最佳兼容性和类型提示支持。PyPDF2 3.x版本引入了大量API变化,原有2.x版本的一些方法被重构或重命名,迁移时需注意兼容性问题。
# 安装PyPDF2(推荐使用最新稳定版)
pip install PyPDF2
# 如需指定版本
pip install PyPDF2==3.0.1
# 查看已安装版本
pip show PyPDF2
基本使用流程
PyPDF2的使用遵循"打开-操作-保存"的基本模式。使用with语句打开PDF文件后,通过PdfReader读取内容,通过PdfWriter写入新的PDF文件。以下是核心类的关系:PdfReader负责读取和解析已有PDF,PdfWriter负责构建和输出新的PDF,PdfMerger专门用于多文件合并场景。
import PyPDF2
# 基本读取示例
with open("example.pdf", "rb") as file:
reader = PyPDF2.PdfReader(file)
print(f"总页数: {len(reader.pages)}")
# 读取第一页文本(可能为空,取决于PDF是否包含文本层)
first_page = reader.pages[0]
text = first_page.extract_text()
print(f"第一页内容: {text[:100]}...")
# 基本写入示例
with open("output.pdf", "wb") as out_file:
writer = PyPDF2.PdfWriter()
writer.add_blank_page(612, 792) # 添加一页A4空白页
writer.write(out_file)
# 检查PDF元数据
from PyPDF2 import PdfReader
reader = PdfReader("example.pdf")
metadata = reader.metadata
if metadata:
print(f"标题: {metadata.title}")
print(f"作者: {metadata.author}")
print(f"主题: {metadata.subject}")
print(f"创建者: {metadata.creator}")
print(f"页数: {len(reader.pages)}")
print(f"PDF版本: {reader.pdf_header}")
核心提示: PyPDF2只能操作已有的PDF文件,不能从零生成包含复杂排版和样式的PDF文档。如果需要生成精美排版的PDF报告,建议结合reportlab或fpdf2使用。此外,PyPDF2对中文文本的提取效果可能不够理想,因为部分PDF中的中文字符未正确嵌入ToUnicode映射表。
二、PDF读取与信息提取
在处理PDF文件之前,首先需要掌握如何读取PDF并提取其中的信息。PyPDF2的PdfReader类提供了丰富的接口用于获取文档元数据、页面信息和文本内容。理解这些读取操作是后续合并、拆分和编辑的基础。需要注意的是,PDF文件的读取模式必须为"rb"(二进制只读模式),因为PDF是二进制格式文件。
打开PDF与读取页数
PdfReader构造时可以直接传入文件路径或已打开的文件对象。使用len()函数可以快速获取PDF的总页数,这是判断PDF规模、规划拆分策略的重要依据。PyPDF2 3.x中,pages属性返回一个PageObject列表,可以通过索引访问任意页面,注意索引从0开始。
from PyPDF2 import PdfReader
# 方式一:传入文件路径(PyPDF2 >= 3.0)
reader = PdfReader("document.pdf")
print(f"文档页数: {len(reader.pages)}")
# 方式二:使用文件对象
with open("document.pdf", "rb") as f:
reader = PdfReader(f)
print(f"文档页数: {len(reader.pages)}")
# 遍历所有页面
for i, page in enumerate(reader.pages):
print(f"第 {i + 1} 页, 尺寸: {page.mediabox.width} x {page.mediabox.height}")
提取元数据
PDF文件的元数据(Metadata)存储在文档的尾部信息中,包含标题、作者、主题、关键词、创建时间、修改时间等信息。PyPDF2通过reader.metadata属性返回这些信息,返回类型是一个类字典对象。部分PDF文件可能没有元数据或元数据不完整,访问前需要判空。元数据中的日期格式通常遵循PDF规范(如D:20240301120000+08'00'),需要自行解析。
from PyPDF2 import PdfReader
from datetime import datetime
import re
reader = PdfReader("report.pdf")
meta = reader.metadata
if meta:
print(f"标题: {meta.get('/Title', 'N/A')}")
print(f"作者: {meta.get('/Author', 'N/A')}")
print(f"主题: {meta.get('/Subject', 'N/A')}")
print(f"关键词: {meta.get('/Keywords', 'N/A')}")
print(f"创建者: {meta.get('/Creator', 'N/A')}")
print(f"生产者: {meta.get('/Producer', 'N/A')}")
# 解析PDF日期格式
raw_date = meta.get('/CreationDate', '')
if raw_date:
match = re.search(r'D:(\d{4})(\d{2})(\d{2})', raw_date)
if match:
y, m, d = match.groups()
print(f"创建日期: {y}-{m}-{d}")
# 获取PDF文件结构信息
print(f"PDF头部版本: {reader.pdf_header}")
print(f"是否加密: {reader.is_encrypted}")
print(f"总页数: {len(reader.pages)}")
提取文本内容
extract_text()方法是PyPDF2提供的文本提取接口。该方法会尝试从PDF页面内容流中提取所有文本,但效果依赖于PDF的生成方式。由Microsoft Word或WPS生成的PDF文本提取效果较好,而扫描件PDF(本质是图片)则无法提取文字。对于扫描件PDF,需要使用OCR技术(如pytesseract配合pdf2image)进行处理。此外,extract_text()方法支持通过参数控制提取行为,如layout模式可以尝试保留原始排版。
from PyPDF2 import PdfReader
reader = PdfReader("document.pdf")
# 提取所有页面的文本
full_text = []
for i, page in enumerate(reader.pages):
text = page.extract_text()
full_text.append(f"--- 第 {i + 1} 页 ---\n{text}")
result = "\n".join(full_text)
# 保存提取的文本
with open("extracted_text.txt", "w", encoding="utf-8") as f:
f.write(result)
print(f"共提取 {len(reader.pages)} 页文本")
print(f"总字符数: {len(result)}")
# 获取页面尺寸信息
from PyPDF2 import PdfReader
reader = PdfReader("document.pdf")
for i, page in enumerate(reader.pages[:5]): # 仅查看前5页
mb = page.mediabox
cb = page.cropbox
tb = page.trimbox
ab = page.artbox
bb = page.bleedbox
print(f"第 {i + 1} 页:")
print(f" MediaBox: ({mb.left}, {mb.bottom}, {mb.right}, {mb.top})")
print(f" 宽度: {mb.width:.1f} pt, 高度: {mb.height:.1f} pt")
print(f" CropBox: ({cb.left}, {cb.bottom}, {cb.right}, {cb.top})")
print(f" TrimBox: ({tb.left}, {tb.bottom}, {tb.right}, {tb.top})")
print()
# 判断页面方向
for i, page in enumerate(reader.pages):
w, h = float(page.mediabox.width), float(page.mediabox.height)
if w > h:
orientation = "横向 (Landscape)"
elif h > w:
orientation = "纵向 (Portrait)"
else:
orientation = "正方形 (Square)"
print(f"第 {i + 1} 页方向: {orientation} ({w:.0f} x {h:.0f})")
三、PDF合并与追加
PDF合并是日常办公中最常见的需求之一,比如将多份扫描件合并为一个完整的PDF文档,或将多个章节的电子书合并。PyPDF2提供了PdfMerger类专门用于合并操作,同时也支持通过PdfWriter手动追加页面。PdfMerger在功能上更为强大,支持文件级合并、页面范围选择以及书签合并等高级功能。
使用PdfMerger合并多个PDF
PdfMerger是PyPDF2中专门为合并场景设计的高级API。它支持传入文件路径或文件对象,可以通过append()方法追加整个文档,也可以通过merge()方法在指定位置插入页面。合并完成后调用write()输出结果。PdfMerger的优势在于自动处理页面资源(字体、图片等)的合并,避免资源冲突和重复。
from PyPDF2 import PdfMerger
# 基础合并:将多个PDF按顺序合并
merger = PdfMerger()
pdf_files = [
"chapter1.pdf",
"chapter2.pdf",
"chapter3.pdf",
"chapter4.pdf",
]
for pdf in pdf_files:
merger.append(pdf) # 追加整个PDF文件
print(f"已添加: {pdf}")
merger.write("merged_output.pdf")
merger.close()
print("所有PDF合并完成!")
选择性合并页面
PdfMerger的append()和merge()方法都支持通过pages参数指定页码范围。参数格式可以是单个序号(如3表示第4页)、列表(如[0, 2, 4]选择第1、3、5页)、切片对象(如slice(0, 5, 2)选择前5页中的奇数页)。这个功能在需要提取某些PDF中的特定页面时非常有用,例如只合并每个文档的封面页。
from PyPDF2 import PdfMerger
merger = PdfMerger()
# 只合并第2到第5页(索引1到4)
merger.append("document.pdf", pages=(1, 5))
print("已添加第2-5页")
# 只合并指定页面(第1页、第3页、第7页)
merger.append("report.pdf", pages=[0, 2, 6])
print("已添加第1、3、7页")
# 合并奇数页
merger.append("book.pdf", pages=(0, len(PdfReader("book.pdf").pages), 2))
print("已添加所有奇数页")
merger.write("selective_merge.pdf")
merger.close()
print("选择性合并完成!")
合并后书签处理
合并PDF时,书签(大纲/目录)的处理是一个关键问题。PdfMerger默认情况下会丢弃书签,但可以通过import_outline=True参数保留源文件的书签,或手动添加新的书签结构。add_outline_item()方法用于在合并后的文档中添加书签,指定书签标题和目标页面。对于层次化书签(如章节-小节结构),可以通过指定父书签来构建树形导航结构。
from PyPDF2 import PdfMerger, PdfReader
merger = PdfMerger()
# 逐个添加文件并记录页数变化
current_page = 0
files_info = [
("封面.pdf", "封面"),
("目录.pdf", "目录"),
("第一章.pdf", "第一章:基础概念"),
("第二章.pdf", "第二章:进阶技巧"),
("附录.pdf", "附录"),
]
for filepath, title in files_info:
reader = PdfReader(filepath)
merger.append(filepath)
merger.add_outline_item(title, current_page)
current_page += len(reader.pages)
print(f"已添加 '{title}' (共 {len(reader.pages)} 页)")
merger.write("book_with_bookmarks.pdf")
merger.close()
# 复杂书签结构(带层级)
merger2 = PdfMerger()
merger2.append("part1.pdf")
merger2.add_outline_item("第一部分", 0)
reader1 = PdfReader("part1.pdf")
merger2.add_outline_item("第1章", 0, parent=merger2.outline_items[0])
merger2.add_outline_item("第2章", 5, parent=merger2.outline_items[0])
merger2.append("part2.pdf")
merger2.add_outline_item("第二部分", reader1.pages)
merger2.add_outline_item("第3章", reader1.pages, parent=merger2.outline_items[2])
merger2.add_outline_item("第4章", reader1.pages + 5, parent=merger2.outline_items[2])
merger2.write("book_hierarchical.pdf")
merger2.close()
# 使用PdfWriter手动合并(更细粒度的控制)
from PyPDF2 import PdfReader, PdfWriter
writer = PdfWriter()
files = ["file1.pdf", "file2.pdf", "file3.pdf"]
total_pages = 0
for fname in files:
reader = PdfReader(fname)
for page in reader.pages:
writer.add_page(page)
print(f"添加 {fname}: {len(reader.pages)} 页")
total_pages += len(reader.pages)
with open("manual_merge.pdf", "wb") as f:
writer.write(f)
print(f"合并完成,共 {total_pages} 页")
# 在指定位置插入PDF页面
writer2 = PdfWriter()
reader_main = PdfReader("main.pdf")
reader_insert = PdfReader("insert.pdf")
# 先将前2页写入
for page in reader_main.pages[:2]:
writer2.add_page(page)
# 在第2页后插入另一个文档的所有页面
for page in reader_insert.pages:
writer2.add_page(page)
# 写入剩余的页面
for page in reader_main.pages[2:]:
writer2.add_page(page)
with open("inserted_result.pdf", "wb") as f:
writer2.write(f)
print(f"插入合并完成,共 {len(writer2.pages)} 页")
四、PDF拆分
PDF拆分是合并的逆操作,在实际工作中同样频繁出现,例如将数百页的扫描件按页拆分为单独文件,或将电子书按章节拆分为独立文档。PyPDF2主要通过PdfWriter配合页面选择实现拆分功能,核心思路是为每个目标PDF创建一个新的PdfWriter实例,将源文档的指定页面添加进去后分别保存。根据拆分策略的不同,可以分为按范围拆分、每页单独保存、按章节拆分和按大小拆分四种模式。
按范围拆分
按范围拆分是指将源PDF中的连续页面提取出来,保存为一个新的PDF文件。这种模式适用于提取PDF中的某个章节或某个页码范围。核心实现思路是使用切片语法选取页码范围,然后逐个添加到目标writer中。需要注意页码范围边界处理,避免索引越界。
from PyPDF2 import PdfReader, PdfWriter
def split_by_range(input_pdf, start, end, output_name):
"""按页码范围拆分PDF。start和end为页码(从1开始)"""
reader = PdfReader(input_pdf)
total_pages = len(reader.pages)
# 边界检查
start_idx = max(0, start - 1)
end_idx = min(end, total_pages)
if start_idx >= end_idx:
raise ValueError("无效的页码范围")
writer = PdfWriter()
for i in range(start_idx, end_idx):
writer.add_page(reader.pages[i])
with open(output_name, "wb") as f:
writer.write(f)
print(f"已提取第 {start}-{end} 页到 {output_name}")
return output_name
# 使用示例:提取第1-10页作为封面
split_by_range("book.pdf", 1, 10, "cover.pdf")
# 提取第50-100页作为中间章节
split_by_range("book.pdf", 50, 100, "chapter_middle.pdf")
# 提取第200页到最后一页
reader = PdfReader("book.pdf")
total = len(reader.pages)
split_by_range("book.pdf", 200, total, "last_part.pdf")
每页单独保存
每页单独保存是最精细的拆分方式,将PDF的每一页都导出为独立的PDF文件。这种模式常用于扫描件的逐页拆分、合同文件的单页归档等场景。为了提高效率,可以结合多线程或异步IO加速处理。同时建议添加一些辅助功能,如文件名填充零位以保证排序正确。
from PyPDF2 import PdfReader, PdfWriter
import os
def split_every_page(input_pdf, output_dir="split_pages"):
"""将PDF每页拆分为单独文件"""
os.makedirs(output_dir, exist_ok=True)
reader = PdfReader(input_pdf)
total = len(reader.pages)
digits = len(str(total)) # 用于文件名填充
for i in range(total):
writer = PdfWriter()
writer.add_page(reader.pages[i])
# 生成带前导零的文件名,方便排序
page_num = str(i + 1).zfill(digits)
output_path = os.path.join(output_dir, f"page_{page_num}.pdf")
with open(output_path, "wb") as f:
writer.write(f)
if (i + 1) % 50 == 0:
print(f"进度: {i + 1}/{total} 页")
print(f"拆分完成!共 {total} 页,保存到 {output_dir}/")
return output_dir
# 单页拆分
split_every_page("large_document.pdf")
# 指定页码范围拆分
def split_page_range(input_pdf, page_numbers, output_dir="selected_pages"):
"""提取指定的某些页面,每页存为一个文件"""
os.makedirs(output_dir, exist_ok=True)
reader = PdfReader(input_pdf)
for idx, page_num in enumerate(page_numbers):
writer = PdfWriter()
writer.add_page(reader.pages[page_num - 1]) # 转为0-based索引
output_path = os.path.join(output_dir, f"selected_{idx + 1}.pdf")
with open(output_path, "wb") as f:
writer.write(f)
print(f"已提取 {len(page_numbers)} 个页面到 {output_dir}/")
# 提取第1、5、10、20页
split_page_range("book.pdf", [1, 5, 10, 20])
按章节(书签)拆分
利用PDF书签(大纲)信息进行智能拆分,可以自动将电子书拆分为独立的章节文件。需要先通过PdfReader读取书签结构,解析每个书签项对应的页面编号,然后以书签的层级关系为依据进行拆分。这种拆分方式生成的章节文件会自动继承书签名称作为文件名,非常适合整理技术书籍或培训手册。
from PyPDF2 import PdfReader, PdfWriter
def split_by_bookmarks(input_pdf, output_dir="chapters"):
"""根据PDF书签按章节拆分"""
import os
os.makedirs(output_dir, exist_ok=True)
reader = PdfReader(input_pdf)
outlines = reader.outline # 获取书签列表
if not outlines:
print("该PDF没有书签信息,无法按章节拆分")
return
# 提取书签及其目标页码
bookmark_info = []
for item in outlines:
if isinstance(item, list):
# 跳过子书签(只处理顶层书签)
continue
title = item.title
# 获取书签目标页面
page_number = reader.get_destination_page_number(item)
bookmark_info.append((title, page_number))
if not bookmark_info:
print("未能解析书签信息")
return
# 按章节拆分
for i, (title, start_page) in enumerate(bookmark_info):
# 确定结束页面:下一个书签的起始页或文档末尾
if i + 1 < len(bookmark_info):
end_page = bookmark_info[i + 1][1]
else:
end_page = len(reader.pages)
# 创建章节PDF
writer = PdfWriter()
for p in range(start_page, end_page):
writer.add_page(reader.pages[p])
# 清理文件名中的非法字符
safe_title = "".join(c for c in title if c.isalnum() or c in " _-")
output_path = os.path.join(output_dir, f"{i + 1:02d}_{safe_title}.pdf")
with open(output_path, "wb") as f:
writer.write(f)
print(f"章节 {i + 1}: '{title}' ({start_page + 1}-{end_page}页) -> {output_path}")
print(f"\n按章节拆分完成,共 {len(bookmark_info)} 个章节")
split_by_bookmarks("programming_book.pdf")
# 按文件大小拆分
import os
from PyPDF2 import PdfReader, PdfWriter
def split_by_size(input_pdf, max_size_mb=10, output_dir="size_split"):
"""将大PDF按文件大小拆分为多个小PDF,每个不超过max_size_mb"""
import shutil
os.makedirs(output_dir, exist_ok=True)
reader = PdfReader(input_pdf)
total_pages = len(reader.pages)
max_size_bytes = max_size_mb * 1024 * 1024
part_num = 1
start_page = 0
while start_page < total_pages:
writer = PdfWriter()
end_page = start_page
# 逐页添加,直到接近大小限制
while end_page < total_pages:
# 先添加一页测试大小
test_writer = PdfWriter()
for p in range(start_page, end_page + 1):
test_writer.add_page(reader.pages[p])
temp_path = os.path.join(output_dir, f"_temp_{part_num}.pdf")
with open(temp_path, "wb") as f:
test_writer.write(f)
actual_size = os.path.getsize(temp_path)
os.remove(temp_path)
if actual_size > max_size_bytes and end_page > start_page:
break
end_page += 1
# 写入实际的分部文件
writer = PdfWriter()
for p in range(start_page, end_page):
writer.add_page(reader.pages[p])
output_path = os.path.join(output_dir, f"part_{part_num:03d}.pdf")
with open(output_path, "wb") as f:
writer.write(f)
actual_mb = os.path.getsize(output_path) / (1024 * 1024)
print(f"分部 {part_num}: 第 {start_page + 1}-{end_page} 页 ({actual_mb:.1f} MB)")
start_page = end_page
part_num += 1
print(f"\n按大小拆分完成,共 {part_num - 1} 个文件")
split_by_size("huge_document.pdf", max_size_mb=10)
五、页面操作
PyPDF2提供了丰富的页面级别操作,包括旋转、缩放、裁剪和排序。这些操作通过PageObject的方法实现,可以直接修改页面属性。需要注意的是,页面操作通常会返回一个新的PageObject副本,原始页面不会受影响。理解PDF坐标系统是掌握页面操作的关键,PDF使用点(point)作为基本单位,1点=1/72英寸,A4纸尺寸为595.28×841.89点。
页面旋转
页面旋转常用于处理扫描件中方向错误的页面。PyPDF2支持90度、180度和270度的逆时针旋转。rotate_clockwise()和rotate_counter_clockwise()方法分别实现顺时针和逆时针旋转。多次旋转的效果会叠加,因此需要注意不要过度旋转。旋转操作仅修改页面的显示方向,不会改变页面内容流的实际坐标。
from PyPDF2 import PdfReader, PdfWriter
def fix_page_rotation(input_pdf, output_pdf="fixed.pdf"):
"""自动检测并修复旋转异常的页面"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
for i, page in enumerate(reader.pages):
# 获取当前旋转角度
current_rotation = page.get('/Rotate', 0)
print(f"第 {i + 1} 页: 当前旋转 = {current_rotation}°")
# 如果页面被旋转了,归零
if current_rotation != 0:
page.rotate_clockwise(-current_rotation) # 反向旋转恢复
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print("旋转修复完成!")
# 页面旋转基本操作
def rotate_pages(input_pdf, output_pdf, rotation=90, pages=None):
"""旋转指定页面
rotation: 旋转角度(90、180、270)
pages: 要旋转的页面列表(从0开始),None表示所有页面
"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
for i, page in enumerate(reader.pages):
if pages is None or i in pages:
page.rotate_clockwise(rotation)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"页面旋转 {rotation}° 完成")
# 将第1页顺时针旋转90度
rotate_pages("document.pdf", "rotated90.pdf", 90, pages=[0])
# 将所有页面旋转180度(倒置页面修正)
rotate_pages("scanned.pdf", "upside_down_fixed.pdf", 180)
页面缩放
scale()方法允许按比例缩放PDF页面内容,接受两个参数分别表示X轴和Y轴的比例因子。scale_to_width()和scale_to_height()方法则允许指定目标宽度或高度,自动计算缩放比例。缩放操作在批量处理不同来源的PDF时非常有用,可以统一所有页面的尺寸。需要注意的是,缩放会同时影响页面内容和页面框(MediaBox)的尺寸。
from PyPDF2 import PdfReader, PdfWriter
def scale_pages_uniform(input_pdf, output_pdf, scale_factor=0.5):
"""统一缩放所有页面"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
for page in reader.pages:
page.scale(scale_factor, scale_factor) # 等比例缩放
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"页面统一缩放至 {scale_factor * 100:.0f}% 完成")
def normalize_page_size(input_pdf, output_pdf, target_width=595.28):
"""将所有页面统一为指定宽度(高度等比缩放)"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
for i, page in enumerate(reader.pages):
current_width = float(page.mediabox.width)
scale = target_width / current_width
page.scale(scale, scale)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"页面宽度统一为 {target_width:.0f} pt 完成")
# 将页面缩小为原来的一半
scale_pages_uniform("large.pdf", "half_size.pdf", 0.5)
# 统一为A4宽度
normalize_page_size("mixed_sizes.pdf", "normalized.pdf", 595.28)
# 将所有页面调整为标准的A4尺寸
def force_a4(input_pdf, output_pdf):
reader = PdfReader(input_pdf)
writer = PdfWriter()
a4_w, a4_h = 595.28, 841.89
for page in reader.pages:
pw, ph = float(page.mediabox.width), float(page.mediabox.height)
scale_w = a4_w / pw
scale_h = a4_h / ph
scale = min(scale_w, scale_h) # 保持比例,防止拉伸
page.scale(scale, scale)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print("已强制统一为A4尺寸")
force_a4("mixed.pdf", "all_a4.pdf")
页面裁剪
页面裁剪通过修改页面的CropBox(裁剪框)实现,可以去除PDF页面的边缘空白区域或提取页面中的特定区域。PyPDF2支持四种页面框:MediaBox(媒体框,页面物理尺寸)、CropBox(裁剪框,显示区域)、TrimBox(成品框,印刷裁剪区域)和BleedBox(出血框)。最常用的是通过设置CropBox来改变可见区域。lower_left和upper_right属性分别定义裁剪区域的左下角和右上角坐标。
from PyPDF2 import PdfReader, PdfWriter
def crop_page(input_pdf, output_pdf, page_num=0,
left=0, bottom=0, right=None, top=None):
"""裁剪PDF页面,保留指定区域
坐标原点在页面左下角,向右为X正方向,向上为Y正方向
"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
page = reader.pages[page_num]
mb = page.mediabox
if right is None:
right = float(mb.right)
if top is None:
top = float(mb.top)
# 设置裁剪框
page.cropbox.lower_left = (left, bottom)
page.cropbox.upper_right = (right, top)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"裁剪区域: ({left}, {bottom}) -> ({right}, {top})")
return output_pdf
# 裁剪页面上半部分(保留内容区,去掉页脚)
crop_page("document.pdf", "cropped_top.pdf", 0,
bottom=100) # 从底部100pt处开始
# 批量裁剪所有页面(去除边缘白边)
def auto_crop_all(input_pdf, output_pdf, margin=20):
reader = PdfReader(input_pdf)
writer = PdfWriter()
for page in reader.pages:
mb = page.mediabox
# 向内收缩margin点,去除边缘空白
page.cropbox.lower_left = (margin, margin)
page.cropbox.upper_right = (
float(mb.right) - margin,
float(mb.top) - margin
)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"所有页面已裁剪(边距 {margin} pt)")
auto_crop_all("presentation.pdf", "no_margin.pdf", 30)
# 提取页面中的特定区域(如签名区域)
def extract_signature_area(input_pdf, output_pdf, page_num=0):
reader = PdfReader(input_pdf)
writer = PdfWriter()
page = reader.pages[page_num]
mb = page.mediabox
pw, ph = float(mb.width), float(mb.height)
# 提取页面左下角的签名区域(假设占页面1/4)
page.cropbox.lower_left = (pw * 0.05, ph * 0.05)
page.cropbox.upper_right = (pw * 0.45, ph * 0.35)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"签名区域已提取到 {output_pdf}")
extract_signature_area("contract.pdf", "signature.pdf")
页面排序与重排
页面排序允许改变PDF中页面的顺序,例如将扫描件中倒序的页面恢复为正常顺序,或按照特定规则(如先奇后偶)重新排列适合小册子打印的页面序列。通过索引列表可以灵活控制页面顺序,结合列表推导式可以实现各种复杂的排序模式。
from PyPDF2 import PdfReader, PdfWriter
def reorder_pages(input_pdf, output_pdf, new_order):
"""按新顺序重排页面
new_order: 页面索引列表(从0开始),如 [2, 0, 1]
"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
for idx in new_order:
writer.add_page(reader.pages[idx])
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"页面重排完成,新顺序: {new_order}")
return output_pdf
# 倒序排列(常用于扫描件方向纠正)
reader = PdfReader("scanned_book.pdf")
total = len(reader.pages)
reorder_pages("scanned_book.pdf", "reversed.pdf", list(range(total - 1, -1, -1)))
# 奇偶分离:先所有奇数页,再所有偶数页(小册子打印)
def odd_even_split(input_pdf, output_pdf):
reader = PdfReader(input_pdf)
total = len(reader.pages)
odd_pages = list(range(0, total, 2)) # 奇数页索引
even_pages = list(range(1, total, 2)) # 偶数页索引
new_order = odd_pages + even_pages
return reorder_pages(input_pdf, output_pdf, new_order)
odd_even_split("document.pdf", "odd_even_reorder.pdf")
# 删除指定页面
def delete_pages(input_pdf, output_pdf, pages_to_delete):
reader = PdfReader(input_pdf)
writer = PdfWriter()
for i, page in enumerate(reader.pages):
if i not in pages_to_delete:
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
deleted = len(pages_to_delete)
remaining = len(writer.pages)
print(f"已删除 {deleted} 页,剩余 {remaining} 页")
# 删除第1页和最后1页
reader = PdfReader("book.pdf")
delete_pages("book.pdf", "no_cover.pdf", {0, len(reader.pages) - 1})
六、加密与解密
PDF加密是保护文档内容安全的重要手段。PyPDF2支持PDF的加密和解密操作,但需要注意的是,PDF的加密机制与文件的"打开密码"和"权限密码"有关。PyPDF2的encrypt()方法可以为PDF文件设置用户密码,打开文档时需要输入密码才能查看。解密操作则用于读取受密码保护的PDF文件。PyPDF2支持PDF标准的安全处理程序,包括RC4和AES加密算法。
设置密码保护
通过PdfWriter的encrypt()方法可以为PDF添加密码保护。该方法接受用户密码(打开文档所需)和所有者密码(修改权限所需)两个参数。如果只提供一个密码,则该密码同时作为用户密码和所有者密码。encrypt()方法还支持指定加密算法(RC4或AES)和密钥长度(40位或128位)。对于大多数现代场景,推荐使用128位AES加密以获得更高的安全性。
from PyPDF2 import PdfReader, PdfWriter
def encrypt_pdf(input_pdf, output_pdf, user_password, owner_password=None):
"""加密PDF文件
user_password: 用户密码(打开文档所需)
owner_password: 所有者密码(修改权限所需),如果为None则与用户密码相同
"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
# 复制所有页面
for page in reader.pages:
writer.add_page(page)
# 设置密码(推荐使用128位AES加密)
writer.encrypt(
user_password=user_password,
owner_password=owner_password,
permissions_flag=-44, # 允许打印和质量复制
use_128bit=True # 使用128位加密
)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"PDF加密完成:{output_pdf}")
print(f"用户密码: {user_password}")
# 基本加密(用户密码和所有者密码相同)
encrypt_pdf("report.pdf", "report_encrypted.pdf", "mypassword123")
# 分别设置用户密码和所有者密码
encrypt_pdf(
"confidential.pdf",
"confidential_protected.pdf",
user_password="read123",
owner_password="admin456"
)
# 批量加密文件夹中的所有PDF
import os
def batch_encrypt(folder, password):
for fname in os.listdir(folder):
if fname.lower().endswith(".pdf"):
input_path = os.path.join(folder, fname)
output_path = os.path.join(folder, "encrypted", fname)
os.makedirs(os.path.join(folder, "encrypted"), exist_ok=True)
reader = PdfReader(input_path)
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.encrypt(password)
with open(output_path, "wb") as f:
writer.write(f)
print(f"已加密: {fname}")
batch_encrypt("documents", "team2024")
读取加密PDF与解密
读取加密PDF时,需要使用decrypt()方法提供密码。如果提供了正确的用户密码或所有者密码,解密成功后可正常读取页面内容;否则会抛出错误。decrypt()方法的返回值指示解密状态:0表示未提供密码或密码错误,1表示使用用户密码解密成功,2表示使用所有者密码解密成功。判断PDF是否加密可以使用is_encrypted属性。
from PyPDF2 import PdfReader
def read_encrypted_pdf(input_pdf, password):
"""尝试读取加密的PDF文件"""
try:
reader = PdfReader(input_pdf)
# 检查是否加密
if reader.is_encrypted:
print("该PDF已加密,正在尝试解密...")
result = reader.decrypt(password)
if result == 0:
print("密码错误,无法解密!")
return None
elif result == 1:
print("使用用户密码解密成功")
elif result == 2:
print("使用所有者密码解密成功")
# 解密成功,读取内容
print(f"PDF总页数: {len(reader.pages)}")
for i, page in enumerate(reader.pages[:3]):
text = page.extract_text()
print(f"第 {i + 1} 页预览: {text[:80]}...")
return reader
except Exception as e:
print(f"读取失败: {e}")
return None
# 尝试破解简单密码(仅用于学习目的)
def dictionary_attack(input_pdf, wordlist_file):
"""字典攻击:尝试常用密码"""
reader = PdfReader(input_pdf)
if not reader.is_encrypted:
print("PDF未加密,无需破解")
return None
with open(wordlist_file, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
password = line.strip()
if not password:
continue
try:
result = reader.decrypt(password)
if result > 0:
print(f"找到密码: {password}")
return password
except:
continue
print("字典攻击未找到密码")
return None
# 应用密码列表尝试
# dictionary_attack("protected.pdf", "common_passwords.txt")
# 移除加密(已知密码时)
def remove_encryption(input_pdf, output_pdf, password):
reader = PdfReader(input_pdf)
if reader.is_encrypted:
reader.decrypt(password)
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
# 不调用encrypt(),直接保存(无密码)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"已移除加密: {output_pdf}")
remove_encryption("protected.pdf", "unlocked.pdf", "password123")
安全提示: PDF加密的强度取决于密码的复杂度和加密算法的强度。简单的密码(如纯数字、常见单词)很容易通过字典攻击或暴力破解攻破。对于高安全性要求的文档,建议使用长度超过12位、包含大小写字母+数字+特殊字符的密码,并采用128位AES加密。同时要注意,PDF加密只保护传输和存储安全,一旦文档被合法打开,内容可以被复制和转发。
七、水印与页眉页脚
水印和页眉页脚是PDF文档中常见的元素,用于标识文档状态(如"机密"、"草稿"、"样本")、显示页码信息或添加版权声明。PyPDF2实现水印功能的核心思路是"页面合并"——创建一个包含水印内容的透明PDF页面,然后将其与原页面合并(merge_page)。这种方法灵活且强大,可以实现在任意位置添加任意内容的覆盖层。
添加文本水印
文本水印通过创建一个包含水印文字的PDF页面(利用reportlab或fpdf2生成),然后将此水印页面与原PDF的每个页面合并。水印页面的内容应该设置为半透明,以达到水印的视觉效果。PyPDF2的merge_page()方法将水印页面合并到目标页面上,水印内容会叠加在原内容之上。可以通过调整水印页面的尺寸、位置和透明度来达到不同的视觉效果。
from PyPDF2 import PdfReader, PdfWriter
from io import BytesIO
def create_watermark_page(text="机密", opacity=0.3, angle=45):
"""使用reportlab创建水印PDF页面"""
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=letter)
c.setFont("Helvetica", 60)
c.setFillColorRGB(0.5, 0.5, 0.5, opacity) # 灰色半透明
# 旋转并居中绘制水印文字
c.saveState()
c.translate(letter[0] / 2, letter[1] / 2)
c.rotate(angle)
c.drawCentredString(0, 0, text)
c.restoreState()
c.save()
packet.seek(0)
return PdfReader(packet)
def add_watermark(input_pdf, output_pdf, watermark_text="机密"):
"""为PDF所有页面添加水印"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
# 创建水印页面
watermark = create_watermark_page(watermark_text)
watermark_page = watermark.pages[0]
for page in reader.pages:
page.merge_page(watermark_page) # 合并水印
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"已为 {len(reader.pages)} 页添加水印 '{watermark_text}'")
add_watermark("report.pdf", "report_watermarked.pdf", "机密文档")
# 添加多个水印(不同位置)
def add_multi_watermark(input_pdf, output_pdf, texts_positions):
"""在不同位置添加多个水印标签
texts_positions: [(text, x, y, angle), ...]
"""
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
reader = PdfReader(input_pdf)
writer = PdfWriter()
# 创建多水印页面
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=letter)
c.setFont("Helvetica", 40)
c.setFillColorRGB(0.7, 0.2, 0.2, 0.4) # 红色半透明
for text, x, y, angle in texts_positions:
c.saveState()
c.translate(x, y)
c.rotate(angle)
c.drawCentredString(0, 0, text)
c.restoreState()
c.save()
packet.seek(0)
multi_watermark = PdfReader(packet).pages[0]
for page in reader.pages:
page.merge_page(multi_watermark)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print("多水印添加完成")
add_multi_watermark("doc.pdf", "multi_watermarked.pdf", [
("机密", 300, 400, 45),
("DRAFT", 300, 200, -45),
])
添加图片水印
图片水印适用于需要显示公司LOGO或签名图片的场景。实现方式与文本水印类似,但水印页面中包含的是图片而不是文字。图片水印通常放在页面角落,不太遮挡正文内容。通过调整图片的缩放比例和位置,可以适应不同尺寸的PDF页面。图片来源可以是PNG、JPG等常见格式,但在转换为PDF水印时会嵌入为PDF图像对象。
from PyPDF2 import PdfReader, PdfWriter
from io import BytesIO
def create_image_watermark(image_path, opacity=0.5, scale=0.15):
"""创建图片水印PDF页面"""
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=letter)
# 读取图片并设置透明度
img = ImageReader(image_path)
img_width, img_height = img.getSize()
# 计算缩放尺寸
target_width = letter[0] * scale
target_height = img_height * (target_width / img_width)
# 绘制图片水印(右下角位置)
c.saveState()
c.setFillAlpha(opacity)
x = letter[0] - target_width - 40
y = 40
c.drawImage(img, x, y, width=target_width, height=target_height,
preserveAspectRatio=True, mask='auto')
c.restoreState()
c.save()
packet.seek(0)
return PdfReader(packet)
def add_image_watermark(input_pdf, output_pdf, image_path):
"""为PDF添加图片水印"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
watermark_pdf = create_image_watermark(image_path)
watermark_page = watermark_pdf.pages[0]
for page in reader.pages:
page.merge_page(watermark_page)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"图片水印添加完成: {output_pdf}")
# add_image_watermark("report.pdf", "report_logo.pdf", "logo.png")
# 纯PyPDF2方式添加文本水印(使用页面合并)
def add_text_watermark_simple(input_pdf, output_pdf, text="CONFIDENTIAL"):
"""使用PdfWriter创建简单的水印页并合并"""
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
reader = PdfReader(input_pdf)
writer = PdfWriter()
# 创建水印页
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=A4)
c.setFont("Helvetica-Bold", 50)
c.setFillColorRGB(0.8, 0.8, 0.8, 0.5)
c.saveState()
c.translate(A4[0] / 2, A4[1] / 2)
c.rotate(45)
c.drawCentredString(0, 0, text)
c.restoreState()
c.save()
packet.seek(0)
wm_reader = PdfReader(packet)
wm_page = wm_reader.pages[0]
for page in reader.pages:
page.merge_page(wm_page)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"水印 '{text}' 已添加到 {output_pdf}")
add_text_watermark_simple("doc.pdf", "marked.pdf", "SAMPLE")
添加页码
页码是PDF文档中最基本也最常见的辅助元素。PyPDF2本身没有直接提供添加页码的功能,但可以通过生成页码PDF页面的方式,与原PDF合并实现。页码可以放置在页面底部居中、左下或右下位置,字体大小和样式可以根据需求定制。更完善的页码系统还应该支持"第X页/共Y页"的格式,以及跳过封面页的页码计数。
from PyPDF2 import PdfReader, PdfWriter
from io import BytesIO
from reportlab.lib.pagesizes import letter, A4
from reportlab.pdfgen import canvas
def add_page_numbers(input_pdf, output_pdf,
start_number=1, position="bottom-center",
skip_first_page=False):
"""为PDF添加页码"""
reader = PdfReader(input_pdf)
writer = PdfWriter()
total_pages = len(reader.pages)
for i, page in enumerate(reader.pages):
# 是否跳过第一页
if i == 0 and skip_first_page:
writer.add_page(page)
continue
page_width = float(page.mediabox.width)
page_height = float(page.mediabox.height)
# 创建页码标签
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=(page_width, page_height))
c.setFont("Helvetica", 10)
c.setFillColorRGB(0.3, 0.3, 0.3)
page_num = start_number + i - (1 if skip_first_page else 0)
text = f"- {page_num} -"
# 根据位置计算坐标
if position == "bottom-center":
x = page_width / 2
y = 30
c.drawCentredString(x, y, text)
elif position == "bottom-right":
x = page_width - 50
y = 30
c.drawRightString(x, y, text)
elif position == "bottom-left":
x = 50
y = 30
c.drawString(x, y, text)
elif position == "top-center":
x = page_width / 2
y = page_height - 30
c.drawCentredString(x, y, text)
c.save()
packet.seek(0)
# 合并页码到页面
number_reader = PdfReader(packet)
page.merge_page(number_reader.pages[0])
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"页码已添加到 {output_pdf} (起始页码: {start_number})")
add_page_numbers("report.pdf", "report_with_pages.pdf",
start_number=1, position="bottom-center")
# 带总页数的页码格式 "第1页 / 共10页"
def add_page_numbers_with_total(input_pdf, output_pdf, skip_first=True):
reader = PdfReader(input_pdf)
writer = PdfWriter()
total = len(reader.pages)
for i, page in enumerate(reader.pages):
if i == 0 and skip_first:
writer.add_page(page)
continue
pw, ph = float(page.mediabox.width), float(page.mediabox.height)
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=(pw, ph))
c.setFont("Helvetica", 9)
c.setFillColorRGB(0.4, 0.4, 0.4)
page_num = i + 1 - (1 if skip_first else 0)
text = f"第 {page_num} 页 / 共 {total - (1 if skip_first else 0)} 页"
c.drawCentredString(pw / 2, 30, text)
c.save()
packet.seek(0)
num_reader = PdfReader(packet)
page.merge_page(num_reader.pages[0])
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"带总页数的页码添加完成")
add_page_numbers_with_total("book.pdf", "book_paginated.pdf")
八、书签与导航
书签(在PDF标准中称为"大纲项",Outline Items)是PDF文档导航的核心功能,类似于网页中的锚点链接。良好的书签结构可以极大提升PDF文档的可读性和用户体验。PyPDF2提供了读取和创建书签的完整API,允许开发者解析已有PDF的书签层次结构,也可以在合并或创建PDF时构建新的书签系统。书签支持嵌套结构,可以形成多级目录树。
读取书签结构
通过PdfReader的outline属性可以获取PDF文档的书签列表。outline返回的是一个嵌套列表结构,每个元素要么是OutlineItem(包含title和对应的页面目标),要么是子列表(表示子书签层级)。递归遍历outline可以完整提取书签的树形结构,并将其转换为更易于处理的扁平格式或JSON格式。
from PyPDF2 import PdfReader
def extract_bookmarks(input_pdf):
"""递归提取PDF书签结构"""
reader = PdfReader(input_pdf)
outlines = reader.outline
def walk_bookmarks(items, level=0):
"""递归遍历书签"""
results = []
for item in items:
if isinstance(item, list):
# 子书签列表
results.extend(walk_bookmarks(item, level + 1))
else:
# 书签项
try:
page_num = reader.get_destination_page_number(item)
except:
page_num = -1
results.append({
"title": item.title,
"page": page_num + 1, # 转为1-based页码
"level": level,
})
return results
bookmarks = walk_bookmarks(outlines)
# 打印书签树
for bm in bookmarks:
indent = " " * bm["level"]
print(f"{indent}[{bm['page']}] {bm['title']}")
print(f"\n书签总数: {len(bookmarks)}")
return bookmarks
# 提取并显示书签
bookmarks = extract_bookmarks("programming_book.pdf")
# 将书签保存为JSON
import json
with open("bookmarks.json", "w", encoding="utf-8") as f:
json.dump(bookmarks, f, ensure_ascii=False, indent=2)
print("书签结构已保存到 bookmarks.json")
创建书签
在PdfWriter或PdfMerger中,使用add_outline_item()方法可以创建书签。该方法接受书签标题、目标页面编号(从0开始),以及可选的父书签参数(用于构建层级结构)。对于PdfMerger,由于页面编号在合并过程中会发生变化,需要在添加文件后根据当前的页面偏移量计算目标页数。PyPDF2的书签是基于Index(页面对象索引)而非实际页码的,这一点在使用时需特别注意。
from PyPDF2 import PdfReader, PdfWriter
def create_pdf_with_bookmarks():
"""创建一个带书签的PDF示例"""
writer = PdfWriter()
# 添加一些示例页面(实际使用时会添加真实页面)
for i in range(10):
writer.add_blank_page(612, 792)
# 创建顶层书签
chapter1 = writer.add_outline_item("第一章:引言", 0)
chapter2 = writer.add_outline_item("第二章:基础知识", 2)
chapter3 = writer.add_outline_item("第三章:高级主题", 5)
# 创建子书签(需要传入父书签对象)
writer.add_outline_item("1.1 背景介绍", 0, parent=chapter1)
writer.add_outline_item("1.2 研究意义", 1, parent=chapter1)
writer.add_outline_item("2.1 核心概念", 2, parent=chapter2)
writer.add_outline_item("2.2 数学基础", 3, parent=chapter2)
writer.add_outline_item("2.3 算法原理", 4, parent=chapter2)
writer.add_outline_item("3.1 实现方案", 5, parent=chapter3)
writer.add_outline_item("3.2 性能优化", 6, parent=chapter3)
writer.add_outline_item("3.3 案例分析", 7, parent=chapter3)
writer.add_outline_item("3.4 总结展望", 8, parent=chapter3)
# 添加无父书签的独立书签
writer.add_outline_item("附录", 9)
with open("book_with_outline.pdf", "wb") as f:
writer.write(f)
print("带书签的PDF创建完成")
create_pdf_with_bookmarks()
# 从结构化数据批量创建书签
def create_bookmarks_from_list(writer, bookmarks_data):
"""从结构化的书签数据列表创建书签
bookmarks_data: [(title, page, parent_title_or_None), ...]
"""
bookmark_objects = {}
for title, page, parent_title in bookmarks_data:
parent_obj = bookmark_objects.get(parent_title) if parent_title else None
bm = writer.add_outline_item(title, page, parent=parent_obj)
bookmark_objects[title] = bm
return bookmark_objects
# 使用示例
writer = PdfWriter()
for i in range(20):
writer.add_blank_page(612, 792)
bookmarks_config = [
("第一部分", 0, None),
("第1章", 0, "第一部分"),
("第2章", 5, "第一部分"),
("第二部分", 10, None),
("第3章", 10, "第二部分"),
("第4章", 15, "第二部分"),
("附录", 19, None),
]
create_bookmarks_from_list(writer, bookmarks_config)
with open("structured_bookmarks.pdf", "wb") as f:
writer.write(f)
print("结构化书签创建完成")
# 合并PDF时保留并优化书签
from PyPDF2 import PdfMerger
def merge_with_bookmark_enhancement():
"""合并PDF并在合并结果中添加结构化书签"""
merger = PdfMerger()
# 第一个文件,添加书签
merger.append("part1.pdf")
merger.add_outline_item("第一部分", 0)
# 查看part1的页数
reader1 = PdfReader("part1.pdf")
part1_pages = len(reader1.pages)
# 为第一部分添加子书签
merger.add_outline_item("章节1.1", 0,
parent=merger.outline_items[0])
merger.add_outline_item("章节1.2", part1_pages // 2,
parent=merger.outline_items[0])
# 第二个文件
merger.append("part2.pdf")
merger.add_outline_item("第二部分", part1_pages)
reader2 = PdfReader("part2.pdf")
part2_pages = len(reader2.pages)
merger.add_outline_item("章节2.1", part1_pages,
parent=merger.outline_items[2])
merger.add_outline_item("章节2.2", part1_pages + part2_pages // 2,
parent=merger.outline_items[2])
merger.write("merged_with_enhanced_bookmarks.pdf")
merger.close()
print("合并并增强书签完成")
# merge_with_bookmark_enhancement()
# 书签导航实用工具
from PyPDF2 import PdfReader
def bookmark_nav_guide(input_pdf):
"""生成书签导航指南(类似目录)"""
reader = PdfReader(input_pdf)
outlines = reader.outline
if not outlines:
print("该PDF没有书签")
return
def render_nav(items, depth=0):
lines = []
for item in items:
if isinstance(item, list):
lines.extend(render_nav(item, depth))
else:
try:
page = reader.get_destination_page_number(item) + 1
except:
page = "?"
indent = " " * depth
prefix = "├─" if depth == 0 else "│ "
navigator = f"{' ' * depth}├─ "
lines.append(f"{navigator}{item.title} → 第{page}页")
return lines
nav_lines = render_nav(outlines)
print("=" * 40)
print("PDF导航指南")
print("=" * 40)
for line in nav_lines:
print(line)
print("=" * 40)
bookmark_nav_guide("book.pdf")
九、实战案例
理论知识掌握之后,通过完整的实战案例可以更好地理解PyPDF2在实际工作中的应用场景。以下四个案例涵盖了日常办公中最高频的PDF处理需求:合并扫描件、按章节拆分电子书、批量添加水印以及生成PDF报告。每个案例都是完整的可运行脚本,可以直接应用到实际工作中。
案例一:合并扫描件PDF
扫描仪或手机拍照产生的PDF通常是单页文件,需要合并为完整文档。这个案例展示如何将指定目录中的所有PDF文件按文件名排序后合并为一个PDF。文件名排序规则支持数字前缀(如001_xxx.pdf),确保页面顺序正确。同时支持在合并前检查每个PDF的有效性,跳过损坏的文件。
"""
实战案例:合并扫描件PDF
适用场景:将扫描仪或手机生成的多个单页PDF合并为一个完整文档
"""
from PyPDF2 import PdfReader, PdfWriter
import os
import re
def natural_sort_key(filename):
"""自然排序键(支持数字排序)"""
parts = re.split(r'(\d+)', filename)
parts = [int(p) if p.isdigit() else p.lower() for p in parts]
return parts
def merge_scanned_pdfs(input_dir, output_pdf, pattern="*.pdf"):
"""合并扫描件PDF"""
# 获取所有PDF文件
pdf_files = [f for f in os.listdir(input_dir)
if f.lower().endswith(".pdf")]
pdf_files.sort(key=natural_sort_key)
if not pdf_files:
print("未找到PDF文件")
return
print(f"发现 {len(pdf_files)} 个PDF文件")
writer = PdfWriter()
total_pages = 0
skipped = 0
for fname in pdf_files:
fpath = os.path.join(input_dir, fname)
try:
reader = PdfReader(fpath)
if len(reader.pages) == 0:
print(f" 警告: {fname} 没有页面,已跳过")
skipped += 1
continue
for page in reader.pages:
writer.add_page(page)
total_pages += len(reader.pages)
print(f" ✓ {fname} ({len(reader.pages)} 页)")
except Exception as e:
print(f" ✗ {fname} 读取失败: {e}")
skipped += 1
# 写入合并文件
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"\n合并完成!")
print(f" 输入文件: {len(pdf_files)} 个")
print(f" 跳过文件: {skipped} 个")
print(f" 总页数: {total_pages} 页")
print(f" 输出文件: {output_pdf}")
print(f" 文件大小: {os.path.getsize(output_pdf) / 1024:.1f} KB")
# 使用
merge_scanned_pdfs(
"scanned_pages/",
"merged_scan.pdf"
)
案例二:按章节拆分电子书
对于没有书签但目录清晰的电子书,可以通过关键字搜索的方式自动识别章节边界并拆分。这个案例展示了如何根据章节标题关键字(如"第X章"、"Chapter X")自动检测章节起始位置,然后将电子书按章节拆分为独立的PDF文件。这对于整理技术书籍、培训教材和学术论文非常实用。拆分过程中还会自动为每个章节PDF添加对应的书签。
"""
实战案例:按章节拆分电子书
适用场景:将一本电子书按章节拆分为独立PDF,支持关键字识别章节边界
"""
from PyPDF2 import PdfReader, PdfWriter
import re
def split_book_by_chapters(input_pdf, output_dir="chapters",
chapter_pattern=r'第[一二三四五六七八九十\d]+[章节部]'):
"""根据章节标题模式拆分电子书"""
import os
os.makedirs(output_dir, exist_ok=True)
reader = PdfReader(input_pdf)
total = len(reader.pages)
chapter_boundaries = []
print(f"正在扫描 {total} 页,查找章节边界...")
# 扫描每一页,查找章节标题
for i in range(total):
try:
text = reader.pages[i].extract_text()
if not text:
continue
# 在文本中查找章节标题
lines = text.split('\n')
for line in lines:
line = line.strip()
if re.search(chapter_pattern, line) and len(line) < 50:
chapter_boundaries.append((i, line))
print(f" 发现章节: 第{i + 1}页 -> '{line[:30]}'")
break
except Exception as e:
continue
if not chapter_boundaries:
# 如果没有找到章节标记,按每10页拆分
print("未找到章节标记,按每10页自动拆分")
for i in range(0, total, 10):
chapter_boundaries.append((i, f"第{len(chapter_boundaries) + 1}部分"))
# 根据章节边界拆分
for idx, (start_page, title) in enumerate(chapter_boundaries):
if idx + 1 < len(chapter_boundaries):
end_page = chapter_boundaries[idx + 1][0]
else:
end_page = total
writer = PdfWriter()
for p in range(start_page, end_page):
writer.add_page(reader.pages[p])
# 添加章节书签
writer.add_outline_item(title, 0)
# 清理标题作为文件名
safe_title = re.sub(r'[\\/:*?"<>|]', '', title)[:30]
fname = f"{idx + 1:02d}_{safe_title}.pdf"
fpath = os.path.join(output_dir, fname)
with open(fpath, "wb") as f:
writer.write(f)
print(f" [{idx + 1}/{len(chapter_boundaries)}] "
f"'{title}' -> {fname} "
f"(第{start_page + 1}-{end_page}页,共{end_page - start_page}页)")
print(f"\n拆分完成!共 {len(chapter_boundaries)} 个章节,保存到 {output_dir}/")
split_book_by_chapters(
"programming_guide.pdf",
chapter_pattern=r'第[一二三四五六七八九十\d]+章'
)
案例三:批量添加水印
在企业环境中,经常需要对大量PDF文档批量添加"机密"、"内部资料"等水印。这个案例展示了如何遍历文件夹中的所有PDF文件,为每个文件添加自定义水印并保存到指定输出目录。支持进度显示、错误处理和选择性处理(根据文件大小或日期过滤)。同时提供了"预览模式",可以先处理一个文件验证效果,再批量处理所有文件。
"""
实战案例:批量添加水印
适用场景:对文件夹中的所有PDF文档批量添加机密/草稿水印
"""
from PyPDF2 import PdfReader, PdfWriter
from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
import os
import time
def create_watermark_page_advanced(
text, font_size=50, opacity=0.3, angle=45, page_size=A4):
"""创建高级水印页面,支持自定义字体和颜色"""
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=page_size)
c.setFont("Helvetica-Bold", font_size)
c.setFillColorRGB(0.6, 0.1, 0.1, opacity) # 暗红色半透明
c.saveState()
c.translate(page_size[0] / 2, page_size[1] / 2)
c.rotate(angle)
c.drawCentredString(0, 0, text)
c.restoreState()
c.save()
packet.seek(0)
return PdfReader(packet).pages[0]
def batch_add_watermark(input_dir, output_dir, watermark_text,
file_pattern="*.pdf", preview=False):
"""批量添加水印"""
import glob
os.makedirs(output_dir, exist_ok=True)
pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
pdf_files.sort()
if not pdf_files:
print(f"在 {input_dir} 中未找到PDF文件")
return
# 如果是预览模式,只处理第一个文件
if preview:
pdf_files = pdf_files[:1]
print("预览模式:仅处理第一个文件")
print(f"准备处理 {len(pdf_files)} 个文件...")
print(f"水印文字: {watermark_text}")
print("-" * 50)
success = 0
failed = 0
start_time = time.time()
for idx, fpath in enumerate(pdf_files):
fname = os.path.basename(fpath)
try:
reader = PdfReader(fpath)
# 根据页面尺寸创建水印
if len(reader.pages) > 0:
first_page = reader.pages[0]
pw = float(first_page.mediabox.width)
ph = float(first_page.mediabox.height)
else:
pw, ph = A4
watermark_page = create_watermark_page_advanced(
watermark_text, page_size=(pw, ph)
)
writer = PdfWriter()
for page in reader.pages:
page.merge_page(watermark_page)
writer.add_page(page)
output_path = os.path.join(output_dir, fname)
with open(output_path, "wb") as f:
writer.write(f)
pages = len(reader.pages)
size_kb = os.path.getsize(output_path) / 1024
progress = f"[{idx + 1}/{len(pdf_files)}]"
print(f"{progress} ✓ {fname} ({pages} 页, {size_kb:.0f} KB)")
success += 1
except Exception as e:
print(f"[{idx + 1}/{len(pdf_files)}] ✗ {fname} 失败: {e}")
failed += 1
elapsed = time.time() - start_time
print("-" * 50)
print(f"处理完成!用时 {elapsed:.1f} 秒")
print(f" 成功: {success} 个文件")
print(f" 失败: {failed} 个文件")
# 批量添加水印
batch_add_watermark(
"documents/",
"documents_watermarked/",
"机密资料",
preview=False # 设为True先预览一个文件
)
案例四:PDF报告自动生成
这个综合案例展示了如何使用PyPDF2结合reportlab和matplotlib,从数据文件(CSV、Excel)自动生成包含图表和表格的PDF报告。流程包括:读取数据、生成图表图片、创建排版精美的PDF页面、添加页眉页脚和水印、设置文档属性。这是数据分析自动化的重要环节,特别适合周报、月报、销售报表等周期性报告的生产。
"""
实战案例:PDF报告自动生成
适用场景:从数据文件自动生成包含图表和表格的PDF分析报告
"""
from PyPDF2 import PdfReader, PdfWriter
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus import Table, TableStyle
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from io import BytesIO
import os
def generate_chart_image(data, chart_type="bar", title="数据图表"):
"""生成图表并返回图片字节数据"""
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(8, 4.5))
if chart_type == "bar":
ax.bar(data['labels'], data['values'],
color=['#2e7d32', '#388e3c', '#43a047', '#4caf50', '#66bb6a'])
elif chart_type == "line":
ax.plot(data['labels'], data['values'], 'o-',
color='#2e7d32', linewidth=2, markersize=6)
elif chart_type == "pie":
ax.pie(data['values'], labels=data['labels'],
autopct='%1.1f%%', colors=['#2e7d32', '#388e3c', '#43a047'])
ax.set_title(title, fontsize=14, color='#2e7d32', pad=15)
ax.set_xlabel(data.get('xlabel', ''))
ax.set_ylabel(data.get('ylabel', ''))
plt.tight_layout()
img_buf = BytesIO()
plt.savefig(img_buf, format='png', dpi=150, bbox_inches='tight')
plt.close(fig)
img_buf.seek(0)
return img_buf
def create_report_pdf(data_df, output_pdf, title="数据分析报告"):
"""生成完整的PDF分析报告"""
writer = PdfWriter()
pw, ph = A4
# =========== 封面页 ===========
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=(pw, ph))
# 背景色块
c.setFillColorRGB(0.18, 0.49, 0.20)
c.rect(0, ph * 0.6, pw, ph * 0.4, fill=1)
# 标题
c.setFillColorRGB(1, 1, 1)
c.setFont("Helvetica-Bold", 32)
c.drawCentredString(pw / 2, ph * 0.78, title)
c.setFont("Helvetica", 16)
c.drawCentredString(pw / 2, ph * 0.70, "自动生成报告")
# 报告信息
c.setFillColorRGB(0.2, 0.2, 0.2)
c.setFont("Helvetica", 12)
info_lines = [
f"生成日期: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}",
f"数据记录: {len(data_df)} 条",
f"数据列: {', '.join(data_df.columns[:5])}",
]
y = ph * 0.45
for line in info_lines:
c.drawCentredString(pw / 2, y, line)
y -= 25
c.save()
packet.seek(0)
cover = PdfReader(packet).pages[0]
writer.add_page(cover)
# =========== 数据表格页 ===========
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=(pw, ph))
c.setFont("Helvetica-Bold", 16)
c.setFillColorRGB(0.18, 0.49, 0.20)
c.drawString(50, ph - 50, "数据概览表")
c.save()
packet.seek(0)
table_cover = PdfReader(packet).pages[0]
writer.add_page(table_cover)
# =========== 图表页 ===========
data = {
'labels': list(data_df.columns[:6]) if len(data_df.columns) > 6
else list(data_df.columns),
'values': [float(data_df[col].mean()) for col in
data_df.columns[:6]] if len(data_df.columns) > 6
else [float(data_df[col].mean()) for col in data_df.columns],
'xlabel': '类别',
'ylabel': '平均值'
}
img_buf = generate_chart_image(data, chart_type="bar", title="各列数据平均值")
packet = BytesIO()
c = canvas.Canvas(packet, pagesize=(pw, ph))
c.setFont("Helvetica-Bold", 16)
c.setFillColorRGB(0.18, 0.49, 0.20)
c.drawString(50, ph - 50, "数据分布图")
c.drawImage(img_buf, 50, ph * 0.15, width=pw - 100, height=ph * 0.55)
c.save()
packet.seek(0)
chart_page = PdfReader(packet).pages[0]
writer.add_page(chart_page)
# 保存输出
with open(output_pdf, "wb") as f:
writer.write(f)
print(f"报告已生成: {output_pdf}")
return output_pdf
# 生成示例数据并创建报告
sample_data = pd.DataFrame({
'销售额': np.random.randint(10000, 50000, 12),
'利润': np.random.randint(1000, 8000, 12),
'客户数': np.random.randint(50, 200, 12),
'转化率': np.random.uniform(0.1, 0.5, 12),
'客单价': np.random.randint(100, 500, 12),
'退货率': np.random.uniform(0.01, 0.08, 12),
})
create_report_pdf(sample_data, "monthly_report.pdf", "月度销售数据分析报告")
print("\n所有实战案例完成!")
print("案例文件已生成,请查看对应输出文件。")