夜雨聆风学习资料网

ARTICLE · 1074990

Python办公——批量合并 PDF,还自动生成书签

Python办公——批量合并 PDF,还自动生成书签

📎 pdf_merge —— 批量合并 PDF,还自动生成书签

报销单、合同、报告、扫描件……十几个 PDF 要拼成一份交上去。在线合并网站怕泄密,Adobe 收费,那就用 20 行 Python 自己搞定。


适用人群

  • • 财务 / 行政:报销凭证、发票、审批单拼成一份归档
  • • 老师:多份练习卷、讲义合成一本,方便打印和分发
  • • HR:应聘材料(简历 + 证书 + 成绩单)合成一份完整档案
  • • 销售 / 咨询:方案正文 + 报价单 + 案例附件 = 一份交付物
  • • 任何人:手机扫描出来的一堆单页 PDF,想拼成一个文件
  • • 在意隐私的人:合同、工资单这类文件,绝对不该上传到在线合并网站

功能简介

能力
说明
批量合并
扫描整个目录,把所有 PDF 按顺序拼成一份
自动生成书签
每份原文件生成一个可点击书签,阅读器左侧目录直接跳转
自然排序part2
 排在 part10 前面(普通排序会搞反)
两种排序
按文件名 / 按修改时间(--sort mtime)
手动指定顺序
--files a.pdf b.pdf c.pdf
,完全按你给的顺序
加密 PDF 处理
空密码加密的自动解开,真加密的跳过并提示
坏文件跳过
某个 PDF 损坏不会中断整批,只跳过它并说明原因
进度可见
每个文件多少页、在合并结果里从第几页开始,都打印出来

生成结果:output/merged.pdf


环境准备

pip install PyPDF2

国内镜像:

pip install PyPDF2 -i https://pypi.tuna.tsinghua.edu.cn/simple

验证:

python -c "import PyPDF2; print(PyPDF2.__version__)"
  • • Python 3.8+(本脚本在 Python 3.13 + PyPDF2 3.0.1 实测通过)
  • • 纯 Python 库,不需要装任何外部程序

关于 PyPDF2 与 pypdf

PyPDF2 已经改名为 pypdf(3.0.1 是 PyPDF2 的最后一个版本,功能等同)。本脚本用 PyPDF2 是因为它名气大、教程多。想用新名字只改导入行:

from pypdf import PdfReader, PdfWriter      # pip install pypdffrom pypdf.errors import PdfReadError

其余代码一行都不用改,API 完全一致。


使用方法

cd pdf_mergepython pdf_merge.py

实测输出:

========================================================PDF 合并 —— pdf_merge========================================================[输入] ...\pdf_merge\testdata\pdfs[输入] 找到 3 个 PDF,排序方式:name[1/3] 01_封面与目录.pdf —— 2 页(合并后第 1 页起)[2/3] 02_数据明细.pdf —— 3 页(合并后第 3 页起)[3/3] 03_附件与说明.pdf —— 2 页(合并后第 6 页起)--------------------------------------------------------[完成] 合并 3 个文件,共 7 页[完成] 已生成 3 个书签(阅读器左侧目录可点击跳转)[完成] 输出文件:...\output\merged.pdf(38.1 KB)

用 Chrome 或 Edge 打开 output/merged.pdf,点左侧的书签图标,能看到「01_封面与目录 / 02_数据明细 / 03_附件与说明」三个可点击条目。

常用参数

# 合并指定目录python pdf_merge.py --indir "D:/报销单" --out "D:/2026年3月报销.pdf"# 按修改时间排序(适合扫描件:按扫描先后顺序拼)python pdf_merge.py --sort mtime# 完全手动控制顺序python pdf_merge.py --files 封面.pdf 正文.pdf 附件.pdf --out 完整报告.pdf# 不要书签(想让合并痕迹不明显时)python pdf_merge.py --no-bookmark
参数
默认值
说明
--indir./testdata/pdfs
待合并 PDF 所在目录
--out./output/merged.pdf
输出文件
--sortnamename
 按文件名 / mtime 按修改时间
--files
无
手动指定文件与顺序(给了就忽略 --indir)
--no-bookmark
关
加上则不生成书签

顺序怎么保证?(实战经验)

最稳的办法是给文件名加数字前缀,像测试素材那样:

01_封面与目录.pdf02_数据明细.pdf03_附件与说明.pdf

补零很重要(01 而不是 1),虽然本脚本做了自然排序、1 也能排对,但补零后在资源管理器里看着也是对的,交接给别人不会出错。


核心代码讲解

1. 合并的本质:把页面搬过去

writer = PdfWriter()for path in files:    reader = PdfReader(str(path))for page in reader.pages:        writer.add_page(page)      # 一页一页搬with out_path.open("wb") as f:    writer.write(f)

就这么简单。PDF 合并不需要重新排版(和 PPT 转 PDF 完全不同),页面对象直接搬过去就行,所以纯 Python 库完全够用,而且无损。

2. 书签:关键是算对起始页

start_index = len(writer.pages)      # 搬之前先记住已有多少页for page in reader.pages:    writer.add_page(page)writer.add_outline_item(path.stem, start_index)   # 指向这份文件的第一页

len(writer.pages) 在添加前取值,正好等于新文件第一页的索引(从 0 开始)。所以:第 1 个文件 start_index=0,第 2 个文件(前面有 2 页)start_index=2……打印时 +1 转成人类页码,就是输出里的「合并后第 3 页起」。

path.stem 是不含扩展名的文件名(01_封面与目录.pdf → 01_封面与目录),用它当书签标题最自然。

3. 自然排序:part2 为什么会排到 part10 后面

defnatural_key(path: Path):    parts = re.split(r"(\d+)", path.name.lower())      # 按数字段切开,并保留数字return [int(p) if p.isdigit() else p for p in parts]

字符串排序是逐字符比的:"part10" 和 "part2" 比到第 5 个字符,'1' < '2',所以 part10 会排在前面 —— 合并出来页序就乱了。

re.split(r"(\d+)", "part10.pdf") 得到 ['part', '10', '.pdf'],把数字段转成 int 后变成 ['part', 10, '.pdf'],再和 ['part', 2, '.pdf'] 比较时就是 10 > 2,顺序正确。

⚠️ 小注意:正则里的括号 (\d+) 必须有,否则 re.split 会把数字丢掉。

4. 加密 PDF:先试试空密码

if reader.is_encrypted:if reader.decrypt("") == 0:      # 返回 0 表示密码不对print("有密码保护,无法合并")returnNoneprint("带空密码加密,已自动解开")

现实中大量 PDF 是「有加密标记但密码为空」的——比如某些系统导出的发票、设了「禁止编辑」但不禁止打开的文件。decrypt("") 能直接解开这类文件。真有密码的就跳过并提示,不要让整批任务失败。

如果你知道密码,改成 reader.decrypt("你的密码") 即可。

5. 单个文件坏掉不能拖垮整批

for path in files:    reader = open_reader(path)     # 内部捕获异常,失败返回 Noneif reader isNone:continue# 跳过这个,继续下一个

这是批量脚本的基本素养:一个坏苹果不能毁掉一整筐。open_reader() 里把四种常见意外都拦下来了:

异常
真实场景
PdfReadError
文件损坏、下载不完整、其实是改了后缀的图片
PermissionError
文件正被 PDF 阅读器打开
OSError
网络盘断开、磁盘错误
is_encrypted
有密码保护

另外还检查了 len(reader.pages) == 0(0 页空 PDF),这种文件合并了也没意义。

6. writer.close() 与 finally

try:with out_path.open("wb") as f:        writer.write(f)except PermissionError:    ...finally:    writer.close()      # 无论成功失败都释放资源

PdfWriter 持有对源文件的引用,用 finally 确保释放,否则批量处理大量文件时可能出现「文件句柄用尽」或文件被锁住无法删除。

7. 也可以用 PdfMerger(但不推荐了)

老教程常见这种写法:

from PyPDF2 import PdfMergermerger = PdfMerger()for f in files:    merger.append(f)          # 会自动带上原文件的书签merger.write("merged.pdf")

区别:

  • • PdfMerger 会继承原文件内部已有的书签结构,更省事
  • • 但它在 PyPDF2 3.x 里已标记为过时(deprecated),未来的 pypdf 里会移除
  • • 用 PdfWriter + add_outline_item 对书签的控制更精确(比如书签标题可以自定义、加日期、加页数)

本脚本选 PdfWriter,兼容未来的 pypdf。


测试素材说明

testdata/pdfs/├── 01_封面与目录.pdf    # 2 页:报告封面 + 目录├── 02_数据明细.pdf      # 3 页:数据总览 + 分区域明细 + 渠道构成└── 03_附件与说明.pdf    # 2 页:口径说明 + 联系人

三份都是用 fpdf2 生成的真实中文 PDF(嵌入黑体字体,每页带页脚和页码),模拟一份被拆成三块的「月度经营分析报告」,共 11–14 KB 一份。

为什么这样设计素材?

  • • 文件名带 01/02/03 前缀 → 验证排序正确
  • • 每份页数不同(2 / 3 / 2)→ 验证书签起始页计算正确(应为第 1 / 3 / 6 页)
  • • 内容有前后逻辑(封面→数据→附件)→ 合并后一眼能看出顺序对不对
  • • 中文文件名 + 中文内容 → 验证中文路径和编码处理

实测结果:3 个文件合并成 7 页,书签起始页分别是 1 / 3 / 6,输出 38.1 KB。

想验证容错,可以做这些实验:

# 造一个假 PDF(其实是文本文件),看脚本会不会跳过echo"这不是PDF" > testdata/pdfs/04_坏文件.pdfpython pdf_merge.py       # 应提示「无法解析(文件可能损坏)」并继续合并其余 3 个# 记得测完删掉rm testdata/pdfs/04_坏文件.pdf

常见问题

Q1:合并后的文件为什么比几个原文件加起来还小 / 还大?PDF 内部会共享资源(字体、图片)。合并时同一个字体可能被复用,所以可能更小;但如果每份文件嵌入的字体子集不同,也可能略大。都是正常的。

Q2:合并后能压缩体积吗?PyPDF2 能力有限,可以试试:

for page in writer.pages:    page.compress_content_streams()      # 压缩内容流,对文字型 PDF 有效

图片型 PDF 想大幅压缩,需要 pikepdf 或 Ghostscript,那是另一个话题。

Q3:书签是中文,在某些阅读器里乱码?PyPDF2 写书签用 UTF-16 编码,主流阅读器(Adobe、Chrome、Edge、福昕)都正常。个别老旧国产阅读器可能有问题,可以把书签标题改成英文或加序号规避。

Q4:想给每一页加页码怎么办?PyPDF2 不擅长「画东西」。标准做法是用 reportlab 生成一个只有页码的透明 PDF,再用 page.merge_page() 叠上去。属于进阶玩法。

Q5:只想合并每个文件的第 1 页?改一行:

for page in reader.pages[:1]:      # 只取第 1 页    writer.add_page(page)

Q6:报 PermissionError 写不进去output/merged.pdf 正被 PDF 阅读器打开。关掉再运行(Chrome / Edge 打开过 PDF 也会占用,注意关标签页)。

Q7:合并出来顺序不对

  1. 1. 先看运行日志里的文件顺序(脚本会逐个打印)
  2. 2. 用 --sort mtime 试试(扫描件适用)
  3. 3. 最稳的办法:文件名加 01_ 02_ 03_ 前缀,或直接用 --files 手动指定

Q8:能合并成千上万个 PDF 吗?可以,但注意内存:PdfWriter 会把所有页面留在内存里。上千个文件建议分批合并(先合成 10 个中间文件,再合并这 10 个)。

Q9:加密 PDF 提示无法合并,但我知道密码把脚本里的 reader.decrypt("") 改成 reader.decrypt("你的密码"),或者加个 --password 参数传进去。


打包成 exe

第 0 步:安装 PyInstaller

pip install pyinstallerpyinstaller --version

第 1 步:基础打包

cd pdf_mergepyinstaller -F pdf_merge.py
  • • -F(--onefile):单文件 exe,产物 dist/pdf_merge.exe
  • • PyPDF2 是纯 Python 库、无二进制扩展,打包非常顺利,体积约 12 MB

分发结构:

PDF合并工具/├── pdf_merge.exe├── testdata/│   └── pdfs/            ← 同事把要合并的 PDF 丢进来└── output/    └── merged.pdf       ← 自动生成

想让路径更直观,可以打包时改默认值,比如让 exe 直接读旁边的 待合并 文件夹:

parser.add_argument("--indir", default=str(here / "待合并"), ...)parser.add_argument("--out", default=str(here / "合并结果.pdf"), ...)

这依赖 base_dir():

defbase_dir() -> Path:ifgetattr(sys, "frozen", False):                   # 打包后为 Truereturn Path(sys.executable).resolve().parent    # exe 所在目录return Path(__file__).resolve().parent

第 2 步:-w 参数的取舍

pyinstaller -F -w pdf_merge.py
  • • -w(--noconsole / --windowed):不弹黑窗口

⚠️ 本脚本用 print 汇报「哪个文件几页、从第几页起、哪个跳过了」,加 -w 后全部看不见,出问题无法定位。

场景
建议
命令行交付(推荐)
-F
,不加 -w
双击使用、想看到结果
-F
 + main() 末尾 input("按回车键退出...")
做成拖拽式 GUI 后
才用 -F -w

用 -w 一定要留日志:

import sysifgetattr(sys, "frozen", False):    sys.stdout = sys.stderr = open(base_dir() / "run.log", "w", encoding="utf-8")

第 3 步:--add-data 打包资源

# Windows 分号,macOS/Linux 冒号pyinstaller -F --add-data "testdata;testdata" pdf_merge.py

⚠️ -F 模式下这些资源会解压到 sys._MEIPASS(随机临时目录),只读,程序退出即删除。读取必须用:

defresource_path(rel: str) -> Path:ifgetattr(sys, "frozen", False) andhasattr(sys, "_MEIPASS"):return Path(sys._MEIPASS) / relreturn Path(__file__).resolve().parent / rel

本脚本不需要 --add-data:待合并的 PDF 是用户文件,输出的 merged.pdf 必须可写,都该在 exe 旁边。

真正会用到 --add-data 的例子:内置一份「使用说明.txt」

pyinstaller -F --add-data "使用说明.txt;." pdf_merge.py
类型
用什么
用户的 PDF、输出结果
base_dir()
(可写)
内置说明书 / 图标 / 模板
--add-data
 + resource_path()(只读)

第 4 步:加图标

pyinstaller -F --icon=app.ico pdf_merge.py
  • • 必须是真 .ico(png 改后缀不行)
  • • 转换:
from PIL import ImageImage.open("app.png").save("app.ico", sizes=[(256,256),(64,64),(32,32),(16,16)])
  • • 图标不生效:删 build/dist/ 重打;Windows 图标缓存也可能骗你

第 5 步:瘦身

pyinstaller -F --clean ^  --exclude-module matplotlib ^  --exclude-module numpy ^  --exclude-module pandas ^  --exclude-module PIL ^  --exclude-module tkinter ^  --exclude-module PyQt5 ^  --icon=app.ico pdf_merge.py

头号有效手段:干净虚拟环境

python -m venv build_envbuild_env\Scripts\activatepip install PyPDF2 pyinstallerpyinstaller -F pdf_merge.py

本脚本只需 PyPDF2,干净环境下 12 MB 以内。特别注意排除 PIL:如果全局装了 Pillow,PyPDF2 的可选图片功能可能把它带进来,一下多出 10 MB 以上。

UPX(可选,再省 30%,但增加杀软误报):

pyinstaller -F --upx-dir "D:/tools/upx" pdf_merge.py

第 6 步:完整推荐命令(本脚本)

pyinstaller -F --clean --name "PDF合并工具" --icon=app.ico ^  --exclude-module matplotlib --exclude-module numpy --exclude-module pandas ^  --exclude-module PIL --exclude-module tkinter ^  pdf_merge.py

常见坑

坑 1:exe 提示「没有找到任何 .pdf 文件」exe 旁边没有 testdata/pdfs 目录,或者 PDF 放错层级了。交付时把目录结构一起打包,或用 --indir 指定:

pdf_merge.exe --indir "D:/我的PDF" --out "D:/合并.pdf"

坑 2:ModuleNotFoundError: No module named 'PyPDF2'打包环境没装库,或多 Python 版本混淆。用同一解释器:

D:/python13/python.exe -m PyInstaller -F pdf_merge.py

坑 3:双击 exe 一闪而过CMD 里运行 / 加 input() / pdf_merge.exe > log.txt 2>&1,三选一。

坑 4:中文路径与中文文件名运行阶段没问题(全程 pathlib.Path,测试素材就是中文名)。但打包时项目路径建议放纯英文目录,PyInstaller 对中文路径偶有问题。

坑 5:杀毒软件报毒PDF 工具类 exe 被误报较常见。别用 UPX、加白名单、或做代码签名。

坑 6:exe 体积比预期大很多八成是全局环境里的 pandas / numpy / matplotlib / PIL 被连带打包。用 --clean + 干净虚拟环境重打,能立刻从 60 MB 降到 12 MB。

坑 7:Windows 打的 exe 给不了 Mac 同事不支持交叉编译,需在 Mac 上再打一次。

坑 8:-F 启动慢单文件每次运行都解压。追求启动速度就不加 -F,改成文件夹分发。


小结

  • • 一句话价值:把「在线合并网站 + 手动排序」换成一条本地命令,快且不泄密
  • • 核心 API:PdfReader 读 → writer.add_page(page) 搬 → writer.add_outline_item(标题, 起始索引) 加书签 → writer.write(f) 写
  • • 三个实用细节:
    1. 1. start_index = len(writer.pages)(添加前取值)就是书签该指向的页
    2. 2. 自然排序 re.split(r"(\d+)", name),否则 part10 会跑到 part2 前面
    3. 3. 加密 PDF 先试 decrypt(""),很多「假加密」文件能直接解开
  • • 批量脚本的素养:单个文件坏掉要跳过并说明原因,绝不能让整批任务崩掉
  • • 打包要点:-F 不加 -w、排除 PIL/pandas 显著瘦身、干净虚拟环境

配套使用:

  1. 1. 用 ppt_to_pdf 把课件转成 PDF,再用本脚本合成一本完整讲义
  2. 2. 合并后想拆开单页发送,用 pdf_split
  3. 3. 想知道合并结果里都写了什么,用本系列的 pdf_extract_text

本脚本属于「Python 办公自动化 30 例 · PPT 与 PDF 处理」分组 · 作者:小庄

相关学习资料