批量给 PDF 去空白页 + 加水印:90 行 pypdf 脚本,坏文件加密件都不崩

2026-10-05 · 喵窝资源铺

手里一堆 PDF 的两种典型惨状:① 扫描/导出过程中间夹着空白页,打印出来全是废纸;② 要把合同/报价单发给别人,想盖个斜排水印,Adobe Acrobat 收费还得一页页点。这篇给一个 pypdf + reportlab 的批量脚本:递归整个目录,空白页剔除、每页盖半透明水印,坏文件和加密件不会让脚本崩,全部记进运行报告。文中代码实跑验证,真实输出贴在下面。

它做的四件事

需求脚本的处理
剔除空白页无文字且无图片才算空白——纯图片的扫描页不会误删
加水印reportlab 生成透明背景斜 35° 文字层,merge_page 盖到每页上,setFillAlpha(0.12) 半透明不遮正文
性能小优化同尺寸页面的水印只生成一次,缓存复用
烂文件不崩损坏 PDF、加密 PDF 自动跳过并写进报告,不中断整批任务

两个实跑才抓到的坑

坑1:水印「变淡」不能靠画浅灰色。第一版用 setFillColorRGB(0.85,0.85,0.85) 画浅灰字——在白底上看着淡,盖到深色背景的页上就成了灰疙瘩。正确做法是 setFillAlpha(0.12) 真半透明,水印和底色做 alpha 混合,任何底色上都只留一层淡淡痕迹。
坑2:空白页判断不能只看 extract_text()。扫描件整页就是一张图,extract_text() 返回空——只看文字会把扫描页全当空白删掉。必须再查 page.images,两个都空才算空白。宁可漏删(留下一页「看起来空」的)也别误删,删错了用户未必发现。

脚本全文(pdf_clean.py)

# -*- coding: utf-8 -*-
"""批量 PDF 清理:跳过/剔除空白页 + 每页盖文字水印 + 输出运行报告。
用法: python3 pdf_clean.py <输入目录> <输出目录> [--watermark 文字]
只依赖 pypdf + reportlab。坏文件、加密文件不会让脚本崩,会被记进报告。
"""
import os, sys, io
from pypdf import PdfReader, PdfWriter
from reportlab.pdfgen import canvas

def make_watermark_page(text, width, height):
    """生成一个透明背景、只有斜排水印文字的单页 PDF,返回字节流。"""
    buf = io.BytesIO()
    c = canvas.Canvas(buf, pagesize=(width, height))
    c.saveState()
    c.setFont("Helvetica", 36)
    c.setFillAlpha(0.12)                      # 半透明,不遮正文(坑1的修复)
    c.translate(width / 2, height / 2)
    c.rotate(35)                              # 斜 35 度,标准水印角度
    c.drawCentredString(0, 0, text)
    c.restoreState()
    c.save()
    buf.seek(0)
    return PdfReader(buf).pages[0]

def is_blank_page(page):
    """判断一页是否'空白':无文字且无图片。纯图片扫描件不算空白(坑2的修复)。"""
    if (page.extract_text() or "").strip():
        return False
    try:
        if page.images:
            return False
    except Exception:
        pass
    return True

def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    wm_text = "Sample"
    if "--watermark" in sys.argv:
        wm_text = sys.argv[sys.argv.index("--watermark") + 1]
    src_dir, out_dir = args[0], args[1]
    os.makedirs(out_dir, exist_ok=True)
    report, kept_total, dropped_total = [], 0, 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)
            rel = os.path.relpath(path, src_dir)
            out_path = os.path.join(out_dir, rel)
            os.makedirs(os.path.dirname(out_path), exist_ok=True)
            try:
                reader = PdfReader(path)
            except Exception as e:
                report.append(f"[跳过] {rel}: 无法解析 ({type(e).__name__})")
                continue
            if reader.is_encrypted:
                try:
                    reader.decrypt("")
                except Exception:
                    report.append(f"[跳过] {rel}: 加密 PDF,需要密码")
                    continue
            wm_cache = {}
            writer = PdfWriter()
            dropped = []
            for i, page in enumerate(reader.pages, 1):
                if is_blank_page(page):
                    dropped.append(i)
                    continue
                box = page.mediabox
                key = (round(float(box.width)), round(float(box.height)))
                if key not in wm_cache:      # 同尺寸页面复用水印,不重复生成
                    wm_cache[key] = make_watermark_page(wm_text, *key)
                page.merge_page(wm_cache[key])
                writer.add_page(page)
            if len(writer.pages) == 0:
                report.append(f"[跳过] {rel}: 全是空白页,未输出")
                dropped_total += len(dropped)
                continue
            with open(out_path, "wb") as f:
                writer.write(f)
            kept_total += len(writer.pages)
            dropped_total += len(dropped)
            msg = f"[OK] {rel}: 保留 {len(writer.pages)} 页"
            if dropped:
                msg += f",剔除空白页 {dropped}"
            report.append(msg)

    print("=== PDF 批量清理报告 ===")
    print(f"水印文字: {wm_text}")
    for line in report:
        print(line)
    print(f"合计: 保留 {kept_total} 页, 剔除空白 {dropped_total} 页")

if __name__ == "__main__":
    main()

实测输出

造了 4 个测试文件:正常的 2 页发票、中间夹 2 页空白页的 4 页报告、子目录里的 2 页合同、还有一个截断损坏的假 PDF。

=== PDF 批量清理报告 ===
水印文字: MIAOWOO SAMPLE
[OK] 发票A.pdf: 保留 2 页
[跳过] 坏文件.pdf: 无法解析 (PdfStreamError)
[OK] 报告B.pdf: 保留 2 页,剔除空白页 [2, 3]
[OK] 子目录/合同C.pdf: 保留 2 页
合计: 保留 6 页, 剔除空白 2 页

复核输出文件:报告B 从 4 页变 2 页,空白页精准剔除;每一页正文之后都多出了半透明的 MIAOWOO SAMPLE 水印层(extract_text 可见,肉眼是斜 35° 浅印);坏文件被跳过且不中断整批。

不想自己跑脚本?喵窝资源铺提供人工代跑+交付服务(miaowoo.900890.xyz):把你的 PDF 打包发来,说明要的水印文字和是否去空白页即可。由 AI 完成脚本与初验、人工复核后交付,9.9 元起。虚拟物品,交付后不退款,介意勿拍。

← 返回喵窝资源铺 · 相关:一堆乱编码 CSV 合并进一个 Excel