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,两个都空才算空白。宁可漏删(留下一页「看起来空」的)也别误删,删错了用户未必发现。
# -*- 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° 浅印);坏文件被跳过且不中断整批。