【AI 加持下的 Python 编程实战 2_13】第九章:繁琐任务的自动化(中)——自动批量合并 PDF 文档

发布于:2025-08-05 ⋅ 阅读:(13) ⋅ 点赞:(0)

全新第二版《Learn AI-assisted Python Programming》封面

【全新第二版《Learn AI-assisted Python Programming》封面】

写在前面
上篇 演示了 Copilot 协助完成日常工作的一个开胃菜,本篇将进一步深入,看看时隔近一年后,微软主打的 Copilot 助手在 PDF 文件批量合并上有什么进步。本节示例将更加贴合真实工作场景,并按照最新的 GPT 模型进行了本地实测,还考虑到了很多突发情况,可以非常方便地迁移到大家的真实场景。一起先睹为快吧!

3 示例1:自动清理电子邮件中的缩进符号

(详见 上篇

4 示例2:自动操作 PDF 文件

本示例明显比之前的“开胃菜”要难一些:不仅业务场景变复杂了,操作流程也考虑了一些新的报错情况,需要中途更换其他第三方模块。这样的示例才比较贴合真实环境——不是每次问 Copilot 都会那么一帆风顺。

4.1 明确需求

已知文件夹 reports 中有 100 份 PDF 格式的报告文件(1.pdf2.pdf,…… 100.pdf),同级目录下的 covers 文件夹则存有这 100 份报告对应的封面文件,格式也是 PDF 的(Cover1.pdfCover2.pdf,…… Cover100.pdf)。现在需要利用 Copilot 编写一个基于 Python 的工具脚本,将这 100 份封面批量添加到各自对应的报告文件,并将合并后的新报告文件放到 final 文件夹下,如图 9-3 所示:

图 9-3:示例 2 中批量合并 PDF 报告内容与对应封面文件示意图

【图 9-3:示例 2 中批量合并 PDF 报告内容与对应封面文件示意图】

当然这里的封面和报告的内容也是用类似方法批量生成的,确保封面与报告内容的一对一关系(包括大小写组合规则的统一)就是开发者自己的事了,这些都属于数据整理或清洗的准备工作(示例文件详见 PC 端上传的同步配套压缩包)。

4.2 方案海选

和示例 1 一样,先直接用 Github Copilot 最新的 GPT-4.1 提问:

I need to write a Python program that merges .pdf files. How do I do this?

得到如下回复(目测比原书回复更简练):

img9-4

方案海选阶段我们仍然希望不安装第三方模块,因此继续追问:

I need to write a Python program that merges .pdf files. How do I do this?

得到如下回复:

GitHub Copilot

Python does not include built-in support for PDF manipulation, so you need an external library like PyPDF2 or pypdf to merge PDFs directly in Python.

Alternatives without installing a Python module:

  • Use online PDF merging tools (e.g., Smallpdf, PDF Merge).
  • Use command-line tools like pdftk or Ghostscript if you have them installed.
  • On Windows, you can use Microsoft Print to PDF to combine files manually.

If you must use Python, installing a module is required for PDF merging.

这次明显比最初只提示 pdftk 更加全面了,但同样绕不开第三方模块(话又说回来,堂堂一门编程语言为甚要和下游厂商深度绑定呢?),因此只能放弃挣扎。为了货比三家,还是继续拓展一下都有哪些模块可用。于是再问:

Are there other Python .pdf libraries that I should consider?

同样得到一版最新的工具列表(这才过了多久啊,PyPDF2 就停更了么……):

Github Copilot

Yes, there are several Python libraries for working with PDFs. Here are some popular options:

  1. PyPDF2 / pypdf
    • Good for merging, splitting, and basic manipulation.
    • pypdf is the actively maintained fork of PyPDF2.
  2. PdfPlumber
    • Great for extracting text, tables, and metadata from PDFs.
    • Not focused on merging, but excellent for data extraction.
  3. PDFMiner
    • Powerful for text extraction and analysis.
    • Not designed for merging or creating PDFs.
  4. ReportLab
    • Used for creating PDFs from scratch.
    • Can also manipulate existing PDFs, but more complex.
  5. pdfrw
    • Can read and write PDF files.
    • Useful for merging, splitting, and rotating pages.

For merging PDFs, pypdf and pdfrw are the most straightforward choices.
If you need advanced PDF creation, consider ReportLab. For extraction, use pdfplumber or PDFMiner.

为了方便海选,再让它做个优缺点对比,这可是 Copilot 的基本功 1

Can you summarize the strengths and weaknesses of these libraries in tabular form?

整理后的结果几乎是秒出:

Github Copilot

Library Strengths Weaknesses
pypdf / PyPDF2 Easy to use for merging, splitting, rotating, and basic manipulation; actively maintained (pypdf) Limited support for complex PDF features (forms, annotations); not ideal for text extraction
pdfrw Can read/write PDFs; good for merging, splitting, rotating; pure Python Documentation is sparse; less active development; may struggle with complex PDFs
ReportLab Powerful for creating PDFs from scratch; supports graphics and charts Steep learning curve for beginners; not focused on editing existing PDFs
pdfplumber Excellent for extracting text, tables, and metadata; easy to use for data extraction Not designed for merging or editing PDFs; extraction may not be perfect for all layouts
PDFMiner Advanced text extraction and analysis; supports layout analysis Slow on large files; not for merging or creating PDFs; complex API

Let me know if you want recommendations based on your specific use case!

这个结果和原书大同小异,只不过还是要排除不支持合并功能的 pdfplumberPDFMiner。经过上述分析对比后,就可以比较放心地选用第一个推荐模块 pypdf 来完成任务了(Copilot 应该是基于我的需求做了默认排序)。

4.3 脚本实现

先本地安装 PyPDF2

pip install PyPDF2

然后还是以工具函数的方式,让 Copilot 根据人为指定的签名和 docstring 生成 Python 代码(从 L15 开始生成代码):

import PyPDF2
 
def add_covers(report_dir, cover_dir, final_dir):
    '''
    report_dir is a directory of reports named 1.pdf, 2.pdf, and so on.
    These files may be one page or more than one page.
 
    cover_dir is a directory of covers, with one cover per report.
    The filenames in this directory are cover1.pdf, cover2.pdf, and 
    so on. Each of these files is one page.
 
    Add the cover to the beginning of each report,
    and store all resulting pdfs in final_dir.
    '''
    import os
    if not os.path.exists(final_dir):
        os.makedirs(final_dir)
    for i in range(1, 100):  # Assuming a maximum of 99 reports
        report_path = os.path.join(report_dir, f"{i}.pdf")
        cover_path = os.path.join(cover_dir, f"cover{i}.pdf")
        final_path = os.path.join(final_dir, f"final{i}.pdf")
        if os.path.exists(report_path) and os.path.exists(cover_path):
            with open(report_path, 'rb') as report_file, open(cover_path, 'rb') as cover_file:
                report_reader = PyPDF2.PdfReader(report_file)
                cover_reader = PyPDF2.PdfReader(cover_file)
                
                writer = PyPDF2.PdfWriter()
                writer.add_page(cover_reader.pages[0])
                for page in report_reader.pages:
                    writer.add_page(page)
                with open(final_path, 'wb') as final_file:
                    writer.write(final_file)
    print(f"Added covers to reports and saved in {final_dir}")
    
if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description='Add covers to reports.')
    parser.add_argument('report_dir', type=str, help='Directory containing reports')
    parser.add_argument('cover_dir', type=str, help='Directory containing covers')
    parser.add_argument('final_dir', type=str, help='Directory to save final PDFs')
    
    args = parser.parse_args()
    
    add_covers(args.report_dir, args.cover_dir, args.final_dir)
    
# demo.py - A script to add covers to reports using PyPDF2
# Usage: python demo.py <report_dir> <cover_dir> <final_dir>

上述代码直接将参数放到了命令行,执行起来更灵活也更方便了。准备好下列文件夹和脚本文件后,就可以测试了:

图 9-5:测试前的项目结构(final 为新建的空文件夹)

【图 9-5:测试前的项目结构(final 为新建的空文件夹)】

然后运行下列命令完成测试:

> python demo.py 'reports' 'covers' 'final'

果然如作者所说,又是一键搞定的节奏……:

图 9-6:实测合并脚本效果图(无报错、均符合预期)

【图 9-6:实测合并脚本效果图(无报错、均符合预期)】

原书提到的两个问题在最新版大模型中已不复存在:一个是缺少了 import os 语句,另一个是 PdfFileReader 方法的失效。第一个问题过于简单就不分析了,但第二个问题的消失还是有必要验证一下的:是开源作者主动改了,还是 Copilot 修改调用方法了?

火速定位到模块的 Github 页面(详见 这里),然后通过版本号 3.0.1 定位到了变更说明的具体位置:

图 9-7:定位到 Github 社区官方变更方法名的位置

【图 9-7:定位到 Github 社区官方变更方法名的位置】

而这个 pypdf4.0.0 版对应的是 PyPDF23.0.0(详见 这里):

图 9-9:查看源码变更方法名的具体日期(这就是用 Git 托管的好处)

【图 9-8:进一步定位到 PyPDF2 的具体版本】

该变更对应的日期是 2022 年 12 月,确实如作者所说,在原书第一版的编写过程中就已经修复了:

图 9-8:进一步定位到 PyPDF2 的具体版本

【图 9-9:查看源码变更方法名的具体日期(这就是用 Git 托管的好处)】

于是验证无误:本来想作为教学案例的接口失效报错,被模块作者提前当作 Bug 给修复了(在没有提前沟通的情况下,要是在公开出版物中被这样点名无异于宣告模块作者的社会性死亡)。

但是真实工作场景下,遇到特别小众的第三方模块,接口破坏性变更的情况就太多了:你通常会以用户的身份发起一个 issue 提议来提醒作者更新(要是能帮忙修复这个 Bug 就更好了,直接提 PR 还能写到今后的简历里)。想解决这个问题通常等不到源码更新,此时就面临新的抉择:

  1. 退回到某个历史版本;
  2. 使用报错信息中的推荐方案;
  3. 换用新的第三方模块。

由于第二条路往往靠的是源码作者的职业素养,小众的模块一般都没有这么贴心的服务,因此直接忽略。接下来重点尝试剩下的两种方式:切换历史版本和换新库。

1 切换历史版本

先清空 final 文件夹,并按照旧接口的写法修改代码(即目标函数的 L24L25):

# before
report_reader = PyPDF2.PdfReader(report_file)
cover_reader = PyPDF2.PdfReader(cover_file)
# after
report_reader = PyPDF2.PdfFileReader(report_file)
cover_reader = PyPDF2.PdfFileReader(cover_file)

报错情况如下:

python .\demo.py 'reports' 'covers' 'final'
Traceback (most recent call last):
  File "F:\mydesktop\ai-demo\demo.py", line 44, in <module>
    add_covers(args.report_dir, args.cover_dir, args.final_dir)
  File "F:\mydesktop\ai-demo\demo.py", line 24, in add_covers
    report_reader = PyPDF2.PdfFileReader(report_file)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "F:\mydesktop\ai-demo\venv\Lib\site-packages\PyPDF2\_reader.py", line 1974, in __init__
    deprecation_with_replacement("PdfFileReader", "PdfReader", "3.0.0")
  File "F:\mydesktop\ai-demo\venv\Lib\site-packages\PyPDF2\_utils.py", line 369, in deprecation_with_replacement    deprecation(DEPR_MSG_HAPPENED.format(old_name, removed_in, new_name))
  File "F:\mydesktop\ai-demo\venv\Lib\site-packages\PyPDF2\_utils.py", line 351, in deprecation
    raise DeprecationError(msg)
PyPDF2.errors.DeprecationError: PdfFileReader is deprecated and was removed in PyPDF2 3.0.0. Use PdfReader instead.

根据提示,要用 3.0.0 之前的版本,于是运行以下命令退回到 3.0.0 之前的最新版:

pip install "PyPDF2 < 3.0.0"

实测结果如下(回退成功):

图 9-10:将 PyPDF2 模块回退到 3.0.0 以前的最新版实测截图

【图 9-10:将 PyPDF2 模块回退到 3.0.0 以前的最新版实测截图】

再次运行该脚本,合并就又成功了(截图略)。

2 改用新的第三方模块

再来看另一条常见的解决方案:改用新的第三方模块。虽然有点冒险,好在有 Copilot 护航,可以很好地解决陌生工具库学习曲线陡峭的一系列问题。

由于 Copilot 的提示内容已经完全更新,这里就不按原书内容照搬 PyMuPDF 模块了。根据最新的推荐第三方模块,下一个备选项是 pdfrw,于是直接提问:

How can I use the Python pdfrw library to merge .pdf files?

Github Copilot

You can use the pdfrw library to merge PDF files in Python as follows:

# Install pdfrw if you haven't already:
# pip install pdfrw

from pdfrw import PdfReader, PdfWriter

pdfs = ['file1.pdf', 'file2.pdf', 'file3.pdf']  # List your PDF files here
writer = PdfWriter()

for pdf in pdfs:
    reader = PdfReader(pdf)
    writer.addpages(reader.pages)

writer.write('merged_output.pdf')

Replace 'file1.pdf', 'file2.pdf', 'file3.pdf' with your actual PDF filenames.
This will create a new file called merged_output.pdf containing all the merged PDFs.

这个回复同样很简洁,就它了。

先本地安装 pdfrw 模块:

pip install pdfrw

然后清空刚才的 PyPDF2 的代码,改为 pdfrw 的即可:

import os
from pdfrw import PdfReader, PdfWriter
 
def add_covers(report_dir, cover_dir, final_dir):
    '''
    report_dir is a directory of reports named 1.pdf, 2.pdf, and so on.
    These files may be one page or more than one page.
 
    cover_dir is a directory of covers, with one cover per report.
    The filenames in this directory are cover1.pdf, cover2.pdf, and so on.
    Each of these files is one page.
 
    Add the cover to the beginning of each report,
    and store all resulting pdfs in final_dir.
    '''

剩下的工作就是根据 Copilot 的回复按回车确认,直到代码生成结束。完整代码如下:

import os
from pdfrw import PdfReader, PdfWriter
 
def add_covers(report_dir, cover_dir, final_dir):
    '''
    report_dir is a directory of reports named 1.pdf, 2.pdf, and so on.
    These files may be one page or more than one page.
 
    cover_dir is a directory of covers, with one cover per report.
    The filenames in this directory are cover1.pdf, cover2.pdf, and so on.
    Each of these files is one page.
 
    Add the cover to the beginning of each report,
    and store all resulting pdfs in final_dir.
    '''
    if not os.path.exists(final_dir):
        os.makedirs(final_dir)
    for i in range(1, 100):  # Assuming a maximum of 99 reports
        report_path = os.path.join(report_dir, f'{i}.pdf')
        cover_path = os.path.join(cover_dir, f'cover{i}.pdf')
        
        if not os.path.exists(report_path) or not os.path.exists(cover_path):
            continue
        
        report_pdf = PdfReader(report_path)
        cover_pdf = PdfReader(cover_path)
        
        writer = PdfWriter()
        writer.addpage(cover_pdf.pages[0])
        for page in report_pdf.pages:
            writer.addpage(page)
        final_pdf_path = os.path.join(final_dir, f'final{i}.pdf')
        writer.write(final_pdf_path)
        print(f'Created {final_pdf_path} with cover and report {i}.pdf')
        
if __name__ == '__main__':
    report_directory = 'reports'
    cover_directory = 'covers'
    final_directory = 'final_reports'
    
    add_covers(report_directory, cover_directory, final_directory)
    print('All reports processed with covers added.')
# This script adds covers to reports and saves them in a specified directory.
# Ensure you have pdfrw installed: pip install pdfrw
# Adjust the report_directory, cover_directory, and final_directory as needed.

L39 可知,Copilot 很贴心地自动新建了一个结果文件夹 final_reports,可以直接运行而不用担心覆盖之前的处理结果。运行命令:

python demo.py

实测结果如下(完全符合预期):

图 9-11:改用 pdfrw 模块重写 PDF 合并工具脚本实测结果截图

【图 9-11:改用 pdfrw 模块重写 PDF 合并工具脚本实测结果截图】

时隔近一年不得不承认,尽管 Copilot 用到的 GPT-4.1 模型可能不如 Claude 3.5 强大,但其具备的辅助编程能力已经可以胜任日常环境下的绝大部分重复性强的任务了。

4.4 基本流程梳理

最后再复盘梳理一下本示例场景下的基本处理流程(尤其是涉及中途变更 Python 模块的情况),如图 9-12 所示:

图 9-12:考虑换用其他 Python 模块时的 Copilot 辅助编程基本流程

【图 9-12:考虑换用其他 Python 模块时的 Copilot 辅助编程基本流程】

后话
本节算上我自行补充的内容,确实需要各位好好消化一下,须知磨刀不误砍柴工,这个过程一定不能偷懒。下一篇将介绍本章最后一个演示案例:自动合并手机图片库,敬请期待!


  1. 以表格形式输出是我后来加的,为了方便作对比。这里的提示词我都尽量坚持用英文,以免因中文数据集训练不足而导致提示内容出现较大的偏差。现在也应该没人再质疑掌握英文的重要性了吧。 ↩︎


网站公告

今日签到

点亮在社区的每一天
去签到