{T}

Python 操作 PDF 文件指南

概述

PDF (Portable Document Format) 是最通用的文档交换格式,由 Adobe 于 1993 年发明,2008 年成为 ISO 32000 国际标准。PDF 的核心设计目标是在任何设备上保持一致的视觉呈现——"所见即所得"。

图表渲染中…
能力特点
PyPDF2 / pypdf合并、拆分、旋转、水印、加密、元数据功能全面,但不支持中文文本提取
pdfplumber文本提取、表格提取、图像提取、图表分析最佳中文支持,API 友好,推荐
PDFMiner.six底层文本/位置解析功能强大但 API 偏底层
reportlab从零创建 PDF编程式 PDF 生成,企业级
FPDF2从零创建 PDF轻量级,API 简单,适合简单报表
pytesseract + pdf2imageOCR 文字识别扫描件 PDF 必备

PDF 内部结构

理解 PDF 内部结构有助于选择正确的工具和处理策略:

图表渲染中…

PDF 文本存储原理

PDF 中的文本不是按阅读顺序存储的,而是按绘制指令排列。每个字符都有 (x, y) 坐标,PDF 阅读器根据坐标渲染,但提取文本时需要重新排序。这就是为什么某些库提取中文文本会乱序或丢失。

PDF 类型与处理策略

类型特征处理方式
文本型 PDF内容为可提取文本pdfplumber / PDFMiner
扫描型 PDF内容为图像,无可提取文本OCR(pytesseract + pdf2image)
混合型 PDF部分页面文本、部分扫描先尝试提取,失败则 OCR
表单型 PDF含可填写字段PyPDF2 / pypdf 读写表单
加密型 PDF需密码才能打开PyPDF2 decrypt

PyPDF2 / pypdf:PDF 编辑操作

PyPDF2 已停止维护(2023 年起),官方推荐迁移至其继任者 pypdf(API 兼容,import pypdf 即可,或安装 PyPDF2 时自动使用 pypdf 内核)。本文以 PyPDF2 语法讲解,pypdf 用法完全一致。

bash
pip install pypdf   # 或 pip install PyPDF2(自动依赖 pypdf)
python
import pypdf  # 兼容写法:import PyPDF2

# 读取 PDF 基本信息
reader = pypdf.PdfReader('sample.pdf')
print(f'页数: {len(reader.pages)}')
print(f'加密: {reader.is_encrypted}')

# 读取元数据
meta = reader.metadata
if meta:
    print(f'标题: {meta.title}')
    print(f'作者: {meta.author}')
    print(f'创建者: {meta.creator}')
    print(f'创建日期: {meta.creation_date}')

# 获取页面尺寸(单位:磅,1 磅 ≈ 0.353mm)
page = reader.pages[0]
box = page.mediabox
print(f'宽度: {float(box.width):.1f}pt, 高度: {float(box.height):.1f}pt')

拆分 PDF

python
import pypdf

reader = pypdf.PdfReader('input.pdf')

# 按页拆分
for i, page in enumerate(reader.pages):
    writer = pypdf.PdfWriter()
    writer.add_page(page)
    with open(f'page_{i+1}.pdf', 'wb') as f:
        writer.write(f)

# 按范围拆分(提取第 2-5 页)
writer = pypdf.PdfWriter()
for i in range(1, 5):  # 索引从 0 开始
    writer.add_page(reader.pages[i])
with open('extract_2-5.pdf', 'wb') as f:
    writer.write(f)

# 按书签/大纲拆分
def split_by_outline(input_path):
    """按 PDF 大纲(书签)拆分文档"""
    reader = pypdf.PdfReader(input_path)
    outlines = reader.outline

    for i, outline in enumerate(outlines):
        if isinstance(outline, list):
            continue  # 跳过子书签
        page_num = reader.get_destination_page_number(outline)
        print(f'书签: {outline.title}, 页码: {page_num + 1}')

合并 PDF

python
import pypdf
from pathlib import Path

# 基础合并
merger = pypdf.PdfWriter()

for pdf_file in sorted(Path('.').glob('*.pdf')):
    merger.append(pdf_file)

merger.write('merged.pdf')
merger.close()

# 精确控制合并位置
writer = pypdf.PdfWriter()
# 在第 2 页之后插入
writer.append('base.pdf', pages=(0, 2))     # 前 2 页
writer.append('insert.pdf')                   # 插入完整文档
writer.append('base.pdf', pages=(2,))        # 剩余页面
writer.write('inserted.pdf')
writer.close()

# 大文件合并(避免内存溢出)
def merge_large_pdfs(pdf_paths, output_path):
    """流式合并大 PDF 文件"""
    writer = pypdf.PdfWriter()
    for path in pdf_paths:
        reader = pypdf.PdfReader(path)
        for page in reader.pages:
            writer.add_page(page)
    with open(output_path, 'wb') as f:
        writer.write(f)

旋转与裁剪

python
reader = pypdf.PdfReader('input.pdf')
writer = pypdf.PdfWriter()

# 旋转
page = reader.pages[0]
page.rotate(90)    # 逆时针 90°
page.rotate(-90)   # 顺时针 90°
page.rotate(180)   # 上下颠倒
writer.add_page(page)

# 裁剪页面(只保留指定区域)
page = reader.pages[0]
page.mediabox.lower_left = (50, 50)       # 左下角
page.mediabox.upper_right = (500, 700)     # 右上角
writer.add_page(page)

# 缩放页面
from pypdf import Transformation
page = reader.pages[0]
page.scale(1.5, 1.5)  # 宽高各放大 1.5 倍
# page.scale_to(width=595, height=842)  # 缩放到 A4 尺寸
writer.add_page(page)

writer.write('processed.pdf')

添加水印

python
reader = pypdf.PdfReader('document.pdf')
watermark_reader = pypdf.PdfReader('watermark.pdf')
watermark = watermark_reader.pages[0]
writer = pypdf.PdfWriter()

for page in reader.pages:
    page.merge_page(watermark)  # 叠加水印
    writer.add_page(page)

writer.write('watermarked.pdf')

# 动态生成文字水印(使用 reportlab)
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
import io

def create_watermark(text, pagesize=A4, opacity=0.1):
    """生成文字水印 PDF 页面"""
    packet = io.BytesIO()
    c = canvas.Canvas(packet, pagesize=pagesize)
    c.saveState()
    c.setFont('Helvetica', 60)
    c.setFillColorRGB(0.5, 0.5, 0.5, alpha=opacity)
    c.translate(pagesize[0] / 2, pagesize[1] / 2)
    c.rotate(45)
    c.drawCentredString(0, 0, text)
    c.restoreState()
    c.save()
    packet.seek(0)
    return pypdf.PdfReader(packet).pages[0]

# 使用动态水印
watermark = create_watermark('CONFIDENTIAL')
for page in reader.pages:
    page.merge_page(watermark)
    writer.add_page(page)

加密与解密

python
# 加密
reader = pypdf.PdfReader('input.pdf')
writer = pypdf.PdfWriter()
for page in reader.pages:
    writer.add_page(page)

writer.encrypt(
    user_password='user123',        # 用户密码(打开文档需要)
    owner_password='owner456',      # 所有者密码(完全控制权限)
    permissions_flag=0b0000000000100  # 仅允许打印
)
writer.write('encrypted.pdf')

# 解密
reader = pypdf.PdfReader('encrypted.pdf')
if reader.is_encrypted:
    reader.decrypt('user123')
print(f'解密成功,页数: {len(reader.pages)}')

# 权限标志位说明:
# 0b0000000000001 — 打印
# 0b0000000000010 — 修改
# 0b0000000000100 — 复制内容
# 0b0000000001000 — 添加/修改注释

读取/填写 PDF 表单

python
reader = pypdf.PdfReader('form.pdf')

# 读取表单字段
fields = reader.get_form_text_fields()
if fields:
    for name, value in fields.items():
        print(f'{name}: {value}')

# 填写表单
writer = pypdf.PdfWriter()
writer.append('form.pdf')
writer.update_page_form_field_values(
    writer.pages[0],
    {
        'name': '张三',
        'email': 'zhangsan@example.com',
        'date': '2026-06-06',
    }
)
writer.write('filled_form.pdf')

PDFMiner.six:底层文本提取

bash
pip install pdfminer.six
python
from pdfminer.high_level import extract_text, extract_pages
from pdfminer.layout import LTTextBox, LTTextLine, LTFigure, LTChar

# 提取全部文本
text = extract_text('document.pdf')
print(text)

# 按页面提取,带布局信息
for page_layout in extract_pages('document.pdf'):
    for element in page_layout:
        if isinstance(element, LTTextBox):
            print(f'位置: ({element.x0:.0f}, {element.y0:.0f})')
            print(element.get_text().strip())

# 提取字体信息(检测扫描件)
def is_scanned_pdf(filepath):
    """检测 PDF 是否为扫描件(无可提取文本字符)"""
    char_count = 0
    for page_layout in extract_pages(filepath):
        for element in page_layout:
            if isinstance(element, LTTextBox):
                for line in element:
                    for char in line:
                        if isinstance(char, LTChar):
                            char_count += 1
    return char_count < 10  # 字符数极少则判定为扫描件

pdfplumber:中文 PDF 最佳实践

强烈推荐用于中文 PDF 处理,API 更友好,社区更活跃。

bash
pip install pdfplumber

文本提取

python
import pdfplumber

with pdfplumber.open('report.pdf') as pdf:
    print(f'总页数: {len(pdf.pages)}')

    for i, page in enumerate(pdf.pages):
        text = page.extract_text()
        if text:
            print(f'--- 第 {i+1} 页 ---')
            print(text[:200])

    # 按布局提取(保留空格和换行位置)
    text = page.extract_text(layout=True)

    # 提取单词级信息(含坐标)
    words = page.extract_words()
    for word in words[:5]:
        print(f'文字: {word["text"]}, 位置: ({word["x0"]:.0f}, {word["top"]:.0f})')

    # 按行提取
    lines = page.extract_text_lines()
    for line in lines[:3]:
        print(line['text'])

表格提取

python
import pdfplumber

with pdfplumber.open('financial_report.pdf') as pdf:
    page = pdf.pages[0]

    # 自动检测表格
    tables = page.extract_tables()

    for i, table in enumerate(tables):
        print(f'表格 {i+1}:')
        for row in table:
            print(' | '.join([cell or '' for cell in row]))

    # 自定义表格提取参数
    table_settings = {
        "vertical_strategy": "lines",       # 按线条检测垂直边框
        "horizontal_strategy": "lines",     # 按线条检测水平边框
        "intersection_tolerance": 10,       # 线条交点容差
        "min_words_vertical": 3,            # 最少词数(text 策略)
        "min_words_horizontal": 1,
        "snap_tolerance": 5,                # 对齐容差
    }

    tables = page.extract_tables(table_settings)

    # text 策略:无边框表格按文本间距检测
    text_tables = page.extract_tables({
        "vertical_strategy": "text",
        "horizontal_strategy": "text",
    })

    # 提取为 pandas DataFrame
    import pandas as pd
    table = page.extract_table()
    if table:
        df = pd.DataFrame(table[1:], columns=table[0])
        print(df.head())

图像提取

python
import pdfplumber
import fitz  # pymupdf

# pdfplumber 获取图像位置信息
with pdfplumber.open('document.pdf') as pdf:
    page = pdf.pages[0]
    for image in page.images:
        print(f'图像位置: x0={image["x0"]:.0f}, y0={image["top"]:.0f}, '
              f'宽度={image["width"]:.0f}, 高度={image["height"]:.0f}')

# pymupdf 提取实际图像(推荐)
# pip install pymupdf
doc = fitz.open('document.pdf')
for i, page in enumerate(doc):
    for j, img in enumerate(page.get_images(full=True)):
        xref = img[0]
        base_image = doc.extract_image(xref)
        with open(f'page{i+1}_img{j+1}.{base_image["ext"]}', 'wb') as f:
            f.write(base_image['image'])

reportlab:从零创建 PDF

reportlab 是 Python 最强大的 PDF 生成框架,支持精确排版、矢量图形、中文字体、条形码等。

bash
pip install reportlab

基础创建

python
from reportlab.lib.pagesizes import A4, letter
from reportlab.lib.units import cm, mm, inch
from reportlab.lib.colors import HexColor, black, white
from reportlab.pdfgen import canvas

# 创建 PDF
c = canvas.Canvas('hello.pdf', pagesize=A4)
width, height = A4  # 595.27 × 841.89 磅

# 绘制文本
c.setFont('Helvetica', 12)
c.drawString(100, 750, 'Hello, World!')

# 绘制中文(需要注册中文字体)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

pdfmetrics.registerFont(TTFont('SimSun', '/System/Library/Fonts/STSong.ttf'))
c.setFont('SimSun', 16)
c.drawString(100, 700, '你好,世界!')

# 绘制图形
c.setFillColor(HexColor('#4472C4'))
c.rect(100, 600, 200, 50, fill=1, stroke=0)  # 矩形

c.setFillColor(white)
c.setFont('Helvetica-Bold', 14)
c.drawCentredString(200, 615, 'Button Text')

# 绘制线条
c.setStrokeColor(HexColor('#333333'))
c.setLineWidth(0.5)
c.line(72, 580, width - 72, 580)  # 水平分割线

c.save()

使用 Platypus 高级排版

python
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib.colors import HexColor
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    PageBreak, Image, KeepTogether
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

# 注册中文字体
pdfmetrics.registerFont(TTFont('SimSun', '/System/Library/Fonts/STSong.ttf'))

# 创建文档模板
doc = SimpleDocTemplate(
    'report.pdf',
    pagesize=A4,
    topMargin=2*cm,
    bottomMargin=2*cm,
    leftMargin=2.5*cm,
    rightMargin=2.5*cm,
)

# 定义样式
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
    name='CNTitle',
    fontName='SimSun',
    fontSize=22,
    leading=30,
    alignment=TA_CENTER,
    spaceAfter=20,
    textColor=HexColor('#1a1a2e'),
))
styles.add(ParagraphStyle(
    name='CNBody',
    fontName='SimSun',
    fontSize=11,
    leading=18,
    spaceAfter=8,
    firstLineIndent=22,  # 首行缩进 2 字符
))

# 构建内容
elements = []
elements.append(Paragraph('项目开发报告', styles['CNTitle']))
elements.append(Spacer(1, 20))

elements.append(Paragraph(
    '本报告详细阐述了项目的开发过程、技术方案选择及最终实施成果。'
    '项目采用敏捷开发模式,经过 3 个迭代周期完成了核心功能的开发与交付。',
    styles['CNBody']
))

# 表格
table_data = [
    ['模块', '负责人', '进度', '状态'],
    ['用户管理', '张三', '100%', '已完成'],
    ['数据分析', '李四', '85%', '进行中'],
    ['报表导出', '王五', '60%', '进行中'],
]
table = Table(table_data, colWidths=[4*cm, 3*cm, 2.5*cm, 2.5*cm])
table.setStyle(TableStyle([
    ('FONTNAME', (0, 0), (-1, -1), 'SimSun'),
    ('FONTSIZE', (0, 0), (-1, -1), 10),
    ('BACKGROUND', (0, 0), (-1, 0), HexColor('#4472C4')),
    ('TEXTCOLOR', (0, 0), (-1, 0), white),
    ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
    ('GRID', (0, 0), (-1, -1), 0.5, HexColor('#cccccc')),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [white, HexColor('#f2f2f2')]),
]))
elements.append(table)

doc.build(elements)

页眉页脚与页码

python
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.lib.units import cm

def add_page_number(canvas_obj, doc):
    """每页添加页眉和页码"""
    page_num = canvas_obj.getPageNumber()

    # 页眉
    canvas_obj.setFont('Helvetica', 8)
    canvas_obj.setFillColor(gray)
    canvas_obj.drawString(2*cm, A4[1] - 1.5*cm, '机密文档 — 仅供内部使用')

    # 页码
    canvas_obj.drawCentredString(A4[0] / 2, 1.2*cm, f'— {page_num} —')

    # 页眉下方横线
    canvas_obj.setStrokeColor(gray)
    canvas_obj.setLineWidth(0.5)
    canvas_obj.line(2*cm, A4[1] - 1.7*cm, A4[0] - 2*cm, A4[1] - 1.7*cm)

doc = SimpleDocTemplate(
    'with_header_footer.pdf',
    pagesize=A4,
    topMargin=2.5*cm,
    bottomMargin=2*cm,
)

elements = []
for i in range(10):
    elements.append(Paragraph(f'第 {i+1} 节内容...', styles['CNBody']))
    if i < 9:
        elements.append(PageBreak())

doc.build(elements, onFirstPage=add_page_number, onLaterPages=add_page_number)

OCR 扫描件处理

扫描件 PDF 的文字以图像形式存储,需要 OCR 才能提取。

图表渲染中…
bash
pip install pytesseract pdf2image pillow
# 还需安装 Tesseract OCR 引擎:
# macOS: brew install tesseract tesseract-lang
# Ubuntu: sudo apt install tesseract-ocr tesseract-ocr-chi-sim
python
import pytesseract
from pdf2image import convert_from_path
from pathlib import Path

# PDF 转图片
images = convert_from_path('scanned.pdf', dpi=300)

# OCR 提取文本
for i, image in enumerate(images):
    text = pytesseract.image_to_string(
        image,
        lang='chi_sim+eng',  # 中文+英文
        config='--psm 6'     # 页面分割模式
    )
    print(f'--- 第 {i+1} 页 ---')
    print(text)

# PSM 模式说明:
# 0  — 仅方向和脚本检测
# 3  — 全自动页面分割(默认)
# 6  — 假设为统一文本块
# 11 — 稀疏文本,无特定顺序

# 批量扫描件 PDF OCR
def batch_ocr_pdf(input_dir, output_dir):
    """批量处理扫描件 PDF"""
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    for pdf_file in Path(input_dir).glob('*.pdf'):
        print(f'处理: {pdf_file.name}')
        images = convert_from_path(str(pdf_file), dpi=300)
        full_text = []

        for i, image in enumerate(images):
            text = pytesseract.image_to_string(image, lang='chi_sim+eng')
            full_text.append(f'=== 第 {i+1} 页 ===\n{text}')

        output_file = output_dir / f'{pdf_file.stem}.txt'
        output_file.write_text('\n\n'.join(full_text), encoding='utf-8')
        print(f'  → 已保存: {output_file.name}')

FPDF2:轻量级 PDF 创建

bash
pip install fpdf2
python
from fpdf import FPDF

class ChinesePDF(FPDF):
    def __init__(self):
        super().__init__()
        # 添加中文字体(需要 .ttf 文件)
        self.add_font('SimSun', '', '/System/Library/Fonts/STSong.ttf', uni=True)

    def header(self):
        self.set_font('SimSun', '', 8)
        self.cell(0, 10, '自动化生成报告', 0, 1, 'C')
        self.ln(5)

    def footer(self):
        self.set_y(-15)
        self.set_font('SimSun', '', 8)
        self.cell(0, 10, f'第 {self.page_no()}/{{nb}} 页', 0, 0, 'C')

pdf = ChinesePDF()
pdf.alias_nb_pages()
pdf.add_page()

pdf.set_font('SimSun', '', 16)
pdf.cell(0, 15, '月度销售报告', 0, 1, 'C')
pdf.ln(10)

pdf.set_font('SimSun', '', 11)
pdf.cell(0, 8, '报告日期:2026年6月', 0, 1)
pdf.ln(5)

# 简单表格
pdf.set_fill_color(68, 114, 196)
pdf.set_text_color(255, 255, 255)
pdf.set_font('SimSun', '', 10)
for header in ['产品', '销量', '单价', '金额']:
    pdf.cell(45, 8, header, 1, 0, 'C', True)
pdf.ln()

pdf.set_text_color(0, 0, 0)
pdf.set_fill_color(242, 242, 242)
data = [
    ('产品A', '1,200', '¥49.9', '¥59,880'),
    ('产品B', '850', '¥89.0', '¥75,650'),
    ('产品C', '500', '¥129.0', '¥64,500'),
]
for i, row in enumerate(data):
    fill = i % 2 == 0
    for cell in row:
        pdf.cell(45, 7, cell, 1, 0, 'C', fill)
    pdf.ln()

pdf.output('simple_report.pdf')

库选择决策

图表渲染中…

实战案例:批量合同生成系统

python
"""
批量合同生成系统
功能:根据 Excel 数据 + Word 模板 → 生成个性化合同 PDF
流程:读取数据 → 填充模板 → 转换 PDF → 合并/加密
"""
import pypdf
from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.colors import HexColor, white, black
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from datetime import datetime
import json


# 注册中文字体(根据系统选择字体路径)
FONT_PATHS = {
    'macos': '/System/Library/Fonts/STSong.ttf',
    'windows': 'C:/Windows/Fonts/simsun.ttc',
    'linux': '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
}
import platform
system = platform.system().lower()
font_path = FONT_PATHS.get(system if system != 'darwin' else 'macos', FONT_PATHS['linux'])
pdfmetrics.registerFont(TTFont('Song', font_path))


class ContractGenerator:
    """合同 PDF 生成器"""

    def __init__(self, output_dir='contracts'):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)

    def generate(self, data):
        """生成单份合同 PDF"""
        filename = f'合同_{data["contract_id"]}_{data["party_b"]}.pdf'
        filepath = self.output_dir / filename

        doc = SimpleDocTemplate(
            str(filepath), pagesize=A4,
            topMargin=2.5*cm, bottomMargin=2*cm,
            leftMargin=2.5*cm, rightMargin=2.5*cm,
        )

        # 样式
        title_style = ParagraphStyle(
            'Title', fontName='Song', fontSize=20,
            leading=28, alignment=TA_CENTER, spaceAfter=20,
        )
        body_style = ParagraphStyle(
            'Body', fontName='Song', fontSize=12,
            leading=22, spaceAfter=10,
            firstLineIndent=24,
        )
        sig_style = ParagraphStyle(
            'Signature', fontName='Song', fontSize=12,
            leading=20, alignment=TA_LEFT, spaceAfter=5,
        )

        elements = []

        # 标题
        elements.append(Paragraph('技术服务合同', title_style))
        elements.append(Spacer(1, 15))
        elements.append(HRFlowable(width='100%', thickness=1, color=HexColor('#333')))
        elements.append(Spacer(1, 15))

        # 合同编号与日期
        elements.append(Paragraph(
            f'合同编号:{data["contract_id"]}  签订日期:{data["sign_date"]}',
            ParagraphStyle('Info', fontName='Song', fontSize=10, alignment=TA_RIGHT)
        ))
        elements.append(Spacer(1, 15))

        # 合同主体
        elements.append(Paragraph(
            f'甲方(委托方):{data["party_a"]}',
            sig_style
        ))
        elements.append(Paragraph(
            f'乙方(服务方):{data["party_b"]}',
            sig_style
        ))
        elements.append(Spacer(1, 10))

        # 服务内容
        elements.append(Paragraph(
            f'根据《中华人民共和国民法典》及相关法律法规,甲乙双方经友好协商,'
            f'就{data["service_name"]}项目达成如下协议:',
            body_style
        ))

        elements.append(Paragraph('一、服务内容', body_style))
        elements.append(Paragraph(
            f'乙方为甲方提供{data["service_name"]}服务,'
            f'服务期限自{data["start_date"]}起至{data["end_date"]}止。',
            body_style
        ))

        elements.append(Paragraph('二、费用及支付', body_style))
        elements.append(Paragraph(
            f'本合同总金额为人民币{data["amount"]}元整(¥{data["amount_num"]:,.2f})。'
            f'甲方应在合同签订后{data["payment_days"]}个工作日内支付首期款项。',
            body_style
        ))

        # 费用明细表
        table_data = [
            ['项目', '金额(元)', '备注'],
            [data['service_name'], f'{data["amount_num"]:,.2f}', '含税'],
            ['合计', f'{data["amount_num"]:,.2f}', ''],
        ]
        table = Table(table_data, colWidths=[6*cm, 4*cm, 4*cm])
        table.setStyle(TableStyle([
            ('FONTNAME', (0, 0), (-1, -1), 'Song'),
            ('FONTSIZE', (0, 0), (-1, -1), 11),
            ('BACKGROUND', (0, 0), (-1, 0), HexColor('#4472C4')),
            ('TEXTCOLOR', (0, 0), (-1, 0), white),
            ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
            ('GRID', (0, 0), (-1, -1), 0.5, HexColor('#999')),
            ('ROWBACKGROUNDS', (0, 1), (-1, -1), [white, HexColor('#f8f8f8')]),
            ('BOLD', (0, -1), (-1, -1), True),
        ]))
        elements.append(table)
        elements.append(Spacer(1, 20))

        elements.append(Paragraph('三、违约责任', body_style))
        elements.append(Paragraph(
            '任何一方未按合同约定履行义务的,应承担违约责任,'
            '并赔偿对方因此遭受的损失。',
            body_style
        ))

        elements.append(Spacer(1, 40))

        # 签章区域
        sig_data = [
            ['甲方(盖章)', '', '乙方(盖章)', ''],
            ['', '', '', ''],
            ['授权代表:', '', '授权代表:', ''],
            ['日期:', '', '日期:', ''],
        ]
        sig_table = Table(sig_data, colWidths=[3.5*cm, 3.5*cm, 3.5*cm, 3.5*cm])
        sig_table.setStyle(TableStyle([
            ('FONTNAME', (0, 0), (-1, -1), 'Song'),
            ('FONTSIZE', (0, 0), (-1, -1), 11),
            ('TOPPADDING', (0, 0), (-1, -1), 8),
            ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
        ]))
        elements.append(sig_table)

        # 水印
        doc.build(elements, onFirstPage=self._add_watermark, onLaterPages=self._add_watermark)
        return filepath

    @staticmethod
    def _add_watermark(canvas_obj, doc):
        """添加'样本'水印"""
        canvas_obj.saveState()
        canvas_obj.setFont('Song', 60)
        canvas_obj.setFillColorRGB(0.85, 0.85, 0.85, alpha=0.3)
        canvas_obj.translate(A4[0] / 2, A4[1] / 2)
        canvas_obj.rotate(45)
        canvas_obj.drawCentredString(0, 0, '样本')
        canvas_obj.restoreState()

    @staticmethod
    def encrypt_pdf(input_path, output_path, password):
        """加密 PDF"""
        reader = pypdf.PdfReader(str(input_path))
        writer = pypdf.PdfWriter()
        for page in reader.pages:
            writer.add_page(page)
        writer.encrypt(password)
        with open(str(output_path), 'wb') as f:
            writer.write(f)

    @staticmethod
    def merge_pdfs(pdf_paths, output_path):
        """合并多个 PDF"""
        writer = pypdf.PdfWriter()
        for path in pdf_paths:
            writer.append(str(path))
        with open(str(output_path), 'wb') as f:
            writer.write(f)


# 使用
generator = ContractGenerator()

# 批量生成
contracts_data = [
    {
        'contract_id': 'HT-2026-001',
        'party_a': 'XX科技有限公司',
        'party_b': 'YY数据服务公司',
        'service_name': '数据分析平台建设',
        'sign_date': '2026年6月6日',
        'start_date': '2026年7月1日',
        'end_date': '2026年12月31日',
        'amount': '伍拾万',
        'amount_num': 500000.00,
        'payment_days': 15,
    },
    {
        'contract_id': 'HT-2026-002',
        'party_a': 'XX科技有限公司',
        'party_b': 'ZZ云计算公司',
        'service_name': '云服务器运维托管',
        'sign_date': '2026年6月6日',
        'start_date': '2026年7月1日',
        'end_date': '2027年6月30日',
        'amount': '叁拾万',
        'amount_num': 300000.00,
        'payment_days': 10,
    },
]

pdf_paths = []
for data in contracts_data:
    path = generator.generate(data)
    pdf_paths.append(path)
    print(f'已生成: {path.name}')

    # 加密版本
    encrypted_path = path.parent / f'encrypted_{path.name}'
    generator.encrypt_pdf(path, encrypted_path, 'contract2026')
    print(f'已加密: {encrypted_path.name}')

# 合并所有合同
generator.merge_pdfs(pdf_paths, generator.output_dir / 'all_contracts.pdf')
print('所有合同已合并')

常见陷阱

陷阱说明正确做法
PyPDF2 提取中文乱码PyPDF2 的文本提取对中文支持差用 pdfplumber
忽略 PDF 密码加密 PDF 直接读取失败reader.decrypt()
合并大 PDF 时内存溢出PdfWriter 全部在内存中分批处理或使用 append_pages_from_reader()
表格线不完整无边框表格无法检测使用 "text" 策略按文本间距检测
扫描版 PDF文字以图像形式存在,无法直接提取需要 OCR(pytesseract + pdf2image)
reportlab 中文乱码默认字体不支持中文注册中文字体 pdfmetrics.registerFont()
PDF/A 归档标准长期保存需符合 PDF/A 规范使用 pypdf 或 Ghostscript 转换
表单字段名称未知需要知道字段名才能填写先用 get_form_text_fields() 获取字段名

延伸阅读

版本差异(自动化办公库 → 当前稳定版)

本文编写时当前稳定版
openpyxl(Excel)旧版3.1.x
python-docx(Word)旧版1.1.x
python-pptx(PPT)旧版1.0.x
reportlab(PDF)旧版4.x
PyPDF2/pypdfPyPDF2推荐 pypdf(4.x/5.x,PyPDF2 已停止维护)
Pillow(图像)旧版11.x

本文讲解的自动化办公流程(读写 Excel/Word/PDF/PPT)与核心 API 在最新版本中成立;注意 PyPDF2 已迁移至 pypdf。