从“读写”到“思考”:Python PDF处理从入门到进阶实战(Day 43-45)

在日常办公和数据处理中,PDF(便携式文档格式)因其跨平台、不易篡改的特性,成为了最主流的文档格式之一。然而,正是这种“封闭性”使得从PDF中提取信息、对其进行批量处理变得困难。无论是财务人员需要从上千份报表中提取数据,还是行政人员需要合并分散的文档,亦或是开发者希望为文档自动化添加水印,这些需求都指向了一个核心技能:用Python高效地处理PDF文件

在“Python办公自动化”学习路径的第四阶段(第36-50天),我们的目标是让机器人学会“读写算”,而PDF处理正是其中的硬骨头。本文将聚焦于Day 43-45的核心内容,深入浅出地讲解如何利用Python三大主流库——PyPDF2pdfplumberPyMuPDF,实现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技术(如easyocrpytesseract)配合PyMuPDF提取图片区域进行识别 。

2. 合并与拆分:文档的“乐高”玩法

合并多个PDF和拆分单个PDF是办公中最常见的需求。PyPDF2 提供了直观的 PdfWriterPdfReader 来实现这一功能。

场景 A:合并 将当前目录下的 part1.pdfpart2.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添加水印是保护知识产权或标识文档状态的常用手段。PyMuPDFPyPDF2 都支持此功能,但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。用户希望:

  1. 提取所有PDF的文本,合并成一个总的报告文本。
  2. 将所有PDF文件合并成一个all_merged.pdf文件。
  3. 为合并后的总文件添加“公司机密”的文字水印。
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 。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐