2026-10-05 · 喵窝资源铺
两个高频场景:① 一个 5 页的合同只要发第 2-4 页给别人;② 一批 PDF 要全部拆成单页分发盖章。Adobe Acrobat 收费、WPS 得一页页另存。这篇给一个 pypdf 的批量脚本:递归整个目录,默认整本拆单页,加 --pages 2-4,7 就只抽出指定页;坏文件不会让脚本崩,全部记进报告。文中代码实跑验证,真实输出贴在下面。
| 需求 | 脚本的处理 |
|---|---|
| 整本拆单页 | 每页输出 文件名_p1.pdf、_p2.pdf……方便逐页分发 |
| 按区间抽取 | --pages 2-4,7 支持区间和单页混写,输出 文件名_p2-p4.pdf |
| 烂文件不崩 | 损坏 PDF 自动跳过并写进报告;页码超界明确报错拒绝,不静默给错结果 |
坑1:在 PDF 里画中文,Helvetica 会变成一排 ■。写测试文件时用drawString(100, 700, "合同第1页"),生成的 PDF 里文字全是 ■■■■——Helvetica 是标准 14 字体之一,只嵌了西文字形,没有 CJK 字体。要么用 reportlab 注册 TTF 中文字体,要么造测试文件时干脆用 ASCII 文字(本文的测试就用了文件名拼音)。真实拆分不受影响(拆分是复制整页,不重新画字),但只要你想往 PDF 里写中文,就会撞上这个。
坑2:页码区间必须校验上限,不能静默截断。用户传--pages 2-99而文件只有 5 页,如果悄悄输出 2-5 页,用户可能以为拿到了全部——发出去少页是要出事的。正确做法是b > total直接 raiseValueError,让这条文件进「[跳过]」报告,用户一眼看到哪个文件页数对不上。宁可明确失败,不要静默出错。
# -*- coding: utf-8 -*-
"""批量 PDF 拆分:每个文件一键拆成单页 PDF,或按页码区间抽出来另存。
用法:
python3 pdf_split.py <输入目录> <输出目录> # 每页一个 PDF
python3 pdf_split.py <输入目录> <输出目录> --pages 2-4,7 # 只抽出指定页
只依赖 pypdf。坏文件不崩,记进报告。
"""
import os, sys, re
from pypdf import PdfReader, PdfWriter
def parse_ranges(spec, total):
"""把 '2-4,7' 解析成页码列表(1-based)。"""
pages = []
for part in spec.split(","):
m = re.match(r"^(\d+)-(\d+)$", part.strip())
if m:
a, b = int(m.group(1)), int(m.group(2))
if a > b or b > total:
raise ValueError(f"区间 {part} 超出总页数 {total}")
pages.extend(range(a, b + 1))
else:
p = int(part)
if p > total:
raise ValueError(f"页码 {p} 超出总页数 {total}")
pages.append(p)
return pages
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
spec = None
if "--pages" in sys.argv:
spec = sys.argv[sys.argv.index("--pages") + 1]
src_dir, out_dir = args[0], args[1]
os.makedirs(out_dir, exist_ok=True)
report, out_total = [], 0
for root, _dirs, files in os.walk(src_dir):
for fn in sorted(files):
if not fn.lower().endswith(".pdf"):
continue
path = os.path.join(root, fn)
base = os.path.splitext(fn)[0]
try:
reader = PdfReader(path)
except Exception as e:
report.append(f"[跳过] {fn}: 无法解析 ({type(e).__name__})")
continue
if reader.is_encrypted:
try:
reader.decrypt("")
except Exception:
report.append(f"[跳过] {fn}: 加密 PDF")
continue
total = len(reader.pages)
if spec:
try:
pages = parse_ranges(spec, total)
except ValueError as e:
report.append(f"[跳过] {fn}: {e}")
continue
w = PdfWriter()
for p in pages:
w.add_page(reader.pages[p - 1])
out_name = f"{base}_p{pages[0]}-p{pages[-1]}.pdf" if len(pages) > 1 else f"{base}_p{pages[0]}.pdf"
with open(os.path.join(out_dir, out_name), "wb") as f:
w.write(f)
report.append(f"[OK] {fn}: 抽出 {pages} -> {out_name}")
out_total += len(pages)
else:
for i, page in enumerate(reader.pages, 1):
w = PdfWriter()
w.add_page(page)
with open(os.path.join(out_dir, f"{base}_p{i}.pdf"), "wb") as f:
w.write(f)
report.append(f"[OK] {fn}: 拆成 {total} 个单页 PDF")
out_total += total
print("=== PDF 批量拆分报告 ===")
for line in report:
print(line)
print(f"合计输出: {out_total} 个 PDF")
if __name__ == "__main__":
main()
造了 3 个测试文件:5 页的「合同多页」、1 页的「单页便签」、还有一个截断损坏的假 PDF。
=== PDF 批量拆分报告 === [OK] 单页便签.pdf: 拆成 1 个单页 PDF [OK] 合同多页.pdf: 拆成 5 个单页 PDF [跳过] 坏文件.pdf: 无法解析 (PdfStreamError) 合计输出: 6 个 PDF
输出目录里 合同多页_p1.pdf 到 _p5.pdf 共 5 个单页,逐页打开 extract_text() 核对,内容分别是 page 1 到 page 5,没有串页。
按区间抽取 --pages 2-4:
=== PDF 批量拆分报告 === [跳过] 单页便签.pdf: 区间 2-4 超出总页数 1 [OK] 合同多页.pdf: 抽出 [2, 3, 4] -> 合同多页_p2-p4.pdf [跳过] 坏文件.pdf: 无法解析 (PdfStreamError) 合计输出: 3 个 PDF
抽出的 合同多页_p2-p4.pdf 用 PdfReader 复核正好 3 页,内容为 page 2/3/4。超界区间 --pages 2-99 被明确拒绝:[跳过] 合同多页.pdf: 区间 2-99 超出总页数 5——不是悄悄给你 5 页了事。