2026-10-09 · 本文所有输出均为作者在本机真实运行结果
报销季最常见的两件事:一是把十几张发票 PDF 合并成一个交上去;二是交完之后财务说"麻烦按每张发票拆回单独文件",而原文件已经被你删了、只剩那个大 PDF。手动拖拽既慢又容易错页。这篇把两个方向一次写清:合并 和 按页数拆回。
合并只是页的堆叠,无序无脑。难的是拆——你必须知道"哪些页属于哪份原文件"。有两种可靠依据:按已知页数切(你要先记录每份文件几页),或者 按书签/分隔页切。本文先给最通用的按页数切法,附完整可跑脚本。
from pypdf import PdfWriter, PdfReader
import json
def merge(files, out="merged.pdf"):
w = PdfWriter()
counts = []
for f in files:
r = PdfReader(f)
for pg in r.pages:
w.add_page(pg)
counts.append(len(r.pages)) # 记录每份的页数,拆回时要用
w.write(out)
return counts
def split_by_counts(src, counts, names, out_dir="out"):
import os
os.makedirs(out_dir, exist_ok=True)
r = PdfReader(src)
start = 0
for name, cnt in zip(names, counts):
w = PdfWriter()
for pg in r.pages[start:start + cnt]:
w.add_page(pg)
w.write(os.path.join(out_dir, name))
start += cnt
counts = merge(["发票-1.pdf", "发票-2.pdf", "发票-3.pdf"])
print("每份页数:", counts)
测试文件页数分别是 2、3、4 页,合并后总页数 9。按 counts 拆回后逐页检查内容:
made: ['发票-1.pdf', '发票-2.pdf', '发票-3.pdf']
merged pages: 9
split pages: 9
page1 text: ■■■■ 1
page2 text: doc1 page2
page4 text: doc2 page2
第 1 页来自原文件 1、第 2 页是原文件 1 的第 2 页、第 4 页正好是"文件 2 的第 2 页"——说明切的边界完全对上了(2 页 + 3 页 + 4 页)。第 1 页中文显示成方块是 reportlab 测试字体缺中文所致,与你合并的真实文件无关。
merge 时顺手把 counts 存下来(或写进 JSON)。等你拿到一个来源不明的合并 PDF 才想拆,就只能靠书签或分隔页了。split_by_counts 里的 names 必须和 counts 同序。zip 会静默按最短截断——两边长度不一致时不报错,直接少拆一个文件。merge 别直接读同一份文件两次:如果你把 w.add_page(pg) 的 pg 在多个 writer 之间复用,pypdf 会因页对象被"收养"报错。每次循环重新 PdfReader(f) 最稳。PdfReader(f) 之后要先 .decrypt(pw),否则 .pages 直接抛 FileNotDecryptedError。授权密码(只读锁)跟打开密码是两回事,别混。r.pages[0] 是第 1 页。按"人类页码"切的时候务必减 1,否则整体错一页——这类错误在 9 页文件里肉眼很难发现。