几百份 PDF 批量提取指定页另存:pypdf 60 行一次抽完

· 喵窝资源铺

交材料最常见的截取活:平台只要合同第 1、2 页(不要签字页),几十份文件一个个「打印 → 选页 → 另存 PDF」点到眼花。这篇用 pypdf 批量把每个 PDF 的指定页抽出来另存,页码支持区间+单页混合(1-2,4),加密/损坏/锁文件逐条报告不中断。代码实跑验证,真实输出贴在下面,坑也是实跑踩出来的。

脚本(60 行)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""批量从PDF提取指定页另存:pypdf实现,扩展名大小写都认,
加密/损坏/锁文件/非PDF逐条报告,同名不覆盖"""
import os, sys
from pypdf import PdfReader, PdfWriter

SRC  = sys.argv[1] if len(sys.argv) > 1 else "."
PICK = sys.argv[2] if len(sys.argv) > 2 else "1-2,4"   # 页码,1起始,支持区间
OUT  = sys.argv[3] if len(sys.argv) > 3 else os.path.join(SRC, "picked")
os.makedirs(OUT, exist_ok=True)

pages = set()
for part in PICK.split(","):
    part = part.strip()
    if not part: continue
    if "-" in part:
        a, b = part.split("-", 1); pages.update(range(int(a), int(b)+1))
    else:
        pages.add(int(part))

done, skipped = [], []
for f in sorted(os.listdir(SRC)):
    p = os.path.join(SRC, f)
    if not os.path.isfile(p):
        continue                                    # 不递归,防输出目录套娃
    if f.startswith("~$"):
        skipped.append((f, "Office锁文件")); continue
    if not f.lower().endswith(".pdf"):
        skipped.append((f, "非PDF")); continue
    stem = f[:-4]
    tag  = PICK.replace(" ", "")
    dst = os.path.join(OUT, stem + "_p" + tag + ".pdf")
    if os.path.exists(dst):
        skipped.append((f, "目标已存在,不覆盖")); continue
    if os.path.exists(os.path.join(OUT, stem.lower() + "_p" + tag + ".pdf")) \
       or os.path.exists(os.path.join(OUT, stem.upper() + "_p" + tag + ".pdf")):
        skipped.append((f, "存在仅大小写不同的同名产物(Windows解压丢大小写),不覆盖")); continue
    try:
        r = PdfReader(p)
        if r.is_encrypted:
            try:
                r.decrypt("")     # 先试空密码(很多只设了权限密码)
            except Exception:
                skipped.append((f, "已加密,需密码,转人工")); continue
            if r.is_encrypted:
                skipped.append((f, "已加密,空密码解不开,转人工")); continue
        n = len(r.pages)
        hit = sorted(x for x in pages if 1 <= x <= n)
        if not hit:
            skipped.append((f, f"无命中页(共{n}页)")); continue
        w = PdfWriter()
        for x in hit:
            w.add_page(r.pages[x-1])
        if len(hit) < len(pages):
            skipped.append((f + f"(共{n}页)", f"页码超范围已略去{sorted(set(pages)-set(hit))}"))
        with open(dst, "wb") as fh:
            w.write(fh)
        done.append((f, os.path.basename(dst), f"第{hit}页/共{n}页"))
    except Exception as e:
        skipped.append((f, "读取失败: " + str(e)[:60]))

print(f"共提取 {len(done)} 个,跳过 {len(skipped)} 个(选定页 {PICK})")
for f, d, note in done:    print(f"  [出] {f} -> {d}  ({note})")
for f, why in skipped:     print(f"  [跳] {f}: {why}")

用法:python3 pdf_pick_pages.py 目录 '1-2,4',第三个参数可指定输出目录(默认 目录/picked/)。依赖 pip install pypdf。

实跑结果

测试集 7 个文件:3 页中文合同《合同A.pdf》(soffice 转 3 个单页 txt 后合并而来)、同词干大写扩展名的《合同A.PDF》(2 页封面目录)、带密码的《机密文件.pdf》(pypdf 加密,密码 secret)、3 字节坏的《损坏文件.pdf》、Word 锁文件 ~$合同A.pdf、说明.txt。跑 python3 pdf_pick_pages.py pdf_test '1-2,4':

共提取 1 个,跳过 6 个(选定页 1-2,4)
  [出] 合同A.PDF -> 合同A_p1-2,4.pdf  (第[1, 2]页/共2页)
  [跳] ~$合同A.pdf: Office锁文件
  [跳] 合同A.PDF(共2页): 页码超范围已略去[4]
  [跳] 合同A.pdf: 目标已存在,不覆盖
  [跳] 损坏文件.pdf: 读取失败: Stream has ended unexpectedly
  [跳] 机密文件.pdf: 已加密,空密码解不开,转人工
  [跳] 说明.txt: 非PDF

产物用 pdftotext 抽文本核对:封面 COVER|目录 TOC——正是《合同A.PDF》的第 1、2 页,中文无乱码、页序正确。加密和损坏的文件没有让脚本崩掉,各自带原因进了跳过清单。

第二遍再跑:共提取 0 个,跳过 6 个——产物同名全部「目标已存在,不覆盖」,幂等 ✅。

实跑踩出的 4 个坑

  • 坑 1:.pdf 和 .PDF 同词干两个文件,产物同名互相覆盖。实跑复现:目录里同时有《合同A.pdf》(3 页正文)和《合同A.PDF》(2 页封面),f[:-4] 对两种扩展名都会切出词干 合同A,转出来的都是 合同A_p1-2,4.pdf——谁后写谁赢,另一份内容无声消失。Windows 上把文件解压给别人时大小写还不保真,接收方两个重名文件直接变一个。修复:写产物前用 lower()/upper() 查一遍已存在的大小写变体,命中就跳过并明说原因。这也是为什么实跑里被提取的是《合同A.PDF》而不是 3 页那份——sorted() 按 .P<.p 排序,大写的先到先得。
  • 坑 2:页码超范围被静默丢弃。写死 for x in pages: add_page(r.pages[x-1]),遇到 2 页文件选 1-2,4,第 4 页直接 IndexError 崩;改 catch 之后又是另一种坏:抽出来的文件悄悄少页,交出去才发现。修复:先按 len(r.pages) 过滤命中页,缺的明确打印「页码超范围已略去[4]」,让人知道这份产物不完整。
  • 坑 3:加密 PDF 的异常会中止整个批处理。只设了「权限密码」(打开不要密码、打印要密码)的文件很常见,PdfReader 照样报 is_encrypted,不处理就抛异常。修复:decrypt("") 先试空密码(权限密码场景直接解开继续干活),解不开才标「已加密,转人工」跳过——绝不因一个加密文件报废整批。
  • 坑 4:try 放错位置,一个坏文件全批报废。把 PdfReader 调用放在循环级 try 外面(或干脆不 catch),3 字节的坏 PDF 抛 Stream has ended unexpectedly,后面几十份好文件全都不处理了。修复:每个文件独立 try,坏文件带原因进 skipped,批处理永不为单个文件中断。
  • 边界说明(如实)

  • 页码从 1 开始(和 PDF 阅读器一致),区间 a-b 含首尾。
  • 提取的是页面原样(矢量/文字/图章都保留);不是 OCR,扫描件图片页同样原样带走。
  • 带打开密码的 PDF 需要密码,脚本会明确标出转人工,不会假装成功。
  • 虚拟物品不退款政策在所有对外文案中一致。
  • 不想自己跑脚本?喵窝资源铺提供人工代跑+交付服务(miaowoo.900890.xyz):把你的 PDF 打包发来+说明要哪几页,交付抽取后的 PDF(逐份核对页数页序)。由 AI 完成脚本与初验、人工复核后交付,9.9 元起。虚拟物品,交付后不退款,介意勿拍。

    ← 返回喵窝资源铺 · 相关:纯 Python 版 Word 批量转 PDF · 相关:重复文件按内容 MD5 检测清理