夜雨聆风学习资料网

ARTICLE · 1156020

用 Python 批量给 PDF 加水印,100 份文件 10 秒搞定

用 Python 批量给 PDF 加水印,100 份文件 10 秒搞定

打工人必备技能:再也不用手动一页页加"内部资料"了

一、先说说痛点

上周同事找我帮忙,说公司要发一批投标文件,200 多份 PDF,每一页都要加上"内部资料 严禁外传"的水印。

我问他打算怎么办,他说准备用 WPS 一份一份打开、加水印、另存为……

按一份 3 分钟算,200 份就是 10 个小时。

我说:给我 20 分钟,写个脚本。

结果脚本跑完 200 份 PDF 只用了 13 秒。

今天就把这个脚本分享出来,复制粘贴就能用。


二、先看效果

处理前:干干净净的 PDF
处理后:每页铺满灰色斜体水印,不影响阅读,又能起到警示作用

支持:

  • ✅ 平铺水印 / 居中单个水印

  • ✅ 自定义文字、字号、颜色、透明度、角度

  • ✅ 文件夹递归批量处理,保留目录结构

  • ✅ 中文不乱码


三、环境准备

只需要装两个库:

pip install pypdf reportlab

⚠️ 注意:pypdf 是 PyPDF2 的官方新版,别装成老掉牙的 PyPDF2 了。


四、核心思路

加 PDF 水印其实就三步:

1. 用 reportlab 造一个"水印页"(尺寸和原页面一样大)2. 用 pypdf 把水印页"盖"到原 PDF 的每一页上3. 遍历文件夹,重复上面两步

难点只有一个:中文水印会变成乱码,因为 reportlab 默认字体不含中文,必须手动注册中文字体。


五、完整代码

新建文件 pdf_watermark.py,直接复制:

# -*- coding: utf-8 -*-"""批量给 PDF 添加水印依赖: pip install pypdf reportlab"""import ioimport mathimport osimport tracebackfrom pathlib import Pathfrom pypdf import PdfReader, PdfWriterfrom reportlab.pdfgen import canvasfrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.ttfonts import TTFontfrom reportlab.lib.colors import Color# ==================== 配置区(改这里就行) ====================INPUT_DIR  = r"./pdfs"          # 待处理的 PDF 文件夹OUTPUT_DIR = r"./output"        # 输出文件夹(自动创建)WATERMARK_TEXT = "内部资料 严禁外传"      # 水印文字# 中文字体路径(按系统选一个)FONT_PATH = r"C:\Windows\Fonts\msyh.ttc"   # Windows: 微软雅黑# FONT_PATH = r"C:\Windows\Fonts\simhei.ttf"  # Windows: 黑体# FONT_PATH = "/System/Library/Fonts/PingFang.ttc"          # macOS# FONT_PATH = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc" # LinuxFONT_SIZE = 30        # 水印字号OPACITY   = 0.12      # 透明度 0~1,越小越淡ANGLE     = 30        # 旋转角度TILE      = True      # True=平铺整页  False=只在正中放一个COLOR     = (0.6, 0.6, 0.6)   # 灰色# ===========================================================FONT_NAME = "CNFont"def register_font():    """注册中文字体(只需一次)"""    if FONT_NAME in pdfmetrics.getRegisteredFontNames():        return    if not os.path.exists(FONT_PATH):        raise FileNotFoundError(            f"找不到字体文件:{FONT_PATH}\n"            f"请修改脚本开头的 FONT_PATH,换成你电脑上存在的中文字体"        )    pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))# 按页面尺寸缓存水印页,避免重复生成_wm_cache = {}def build_watermark_page(width, height, text):    """生成一页和原页面等大的水印 PDF"""    buf = io.BytesIO()    c = canvas.Canvas(buf, pagesize=(width, height))    c.setFont(FONT_NAME, FONT_SIZE)    c.setFillColor(Color(*COLOR))    c.setFillAlpha(OPACITY)    if TILE:        # ---- 平铺模式 ----        step_x = FONT_SIZE * (len(text) + 4)   # 横向间距(按文字长度估算)        step_y = FONT_SIZE * 6                 # 纵向间距        # 旋转后要覆盖整页,用对角线长度算范围        diag = math.hypot(width, height)        n_x = int(diag / step_x) + 2        n_y = int(diag / step_y) + 2        c.saveState()        c.translate(width / 2, height / 2)     # 原点移到页面中心        c.rotate(ANGLE)        for i in range(-n_x, n_x + 1):            for j in range(-n_y, n_y + 1):                c.drawCentredString(i * step_x, j * step_y, text)        c.restoreState()    else:        # ---- 居中单个 ----        c.saveState()        c.translate(width / 2, height / 2)        c.rotate(ANGLE)        c.drawCentredString(0, 0, text)        c.restoreState()    c.save()    buf.seek(0)    return PdfReader(buf).pages[0]def get_watermark(width, height, text):    """带缓存地获取水印页"""    key = (round(width, 1), round(height, 1), text)    if key not in _wm_cache:        _wm_cache[key] = build_watermark_page(width, height, text)    return _wm_cache[key]def add_watermark_to_pdf(src_path: Path, dst_path: Path):    """给单个 PDF 加水印并保存"""    reader = PdfReader(str(src_path))    # 处理加密 PDF(空密码的能直接解)    if reader.is_encrypted:        try:            reader.decrypt("")        except Exception:            raise RuntimeError("PDF 有密码,跳过")    writer = PdfWriter()    for page in reader.pages:        w = float(page.mediabox.width)        h = float(page.mediabox.height)        wm_page = get_watermark(w, h, WATERMARK_TEXT)        page.merge_page(wm_page)          # 把水印盖上去        writer.add_page(page)    # 尽量保留原元数据    try:        if reader.metadata:            writer.add_metadata(dict(reader.metadata))    except Exception:        pass    dst_path.parent.mkdir(parents=True, exist_ok=True)    with open(dst_path, "wb") as f:        writer.write(f)def main():    register_font()    in_dir = Path(INPUT_DIR)    out_dir = Path(OUTPUT_DIR)    if not in_dir.exists():        print(f"❌ 输入目录不存在:{in_dir.resolve()}")        return    pdfs = sorted(in_dir.rglob("*.pdf"))    if not pdfs:        print("❌ 没找到任何 PDF 文件")        return    print(f"📂 共找到 {len(pdfs)} 个 PDF,开始处理...\n")    ok = fail = 0    for i, src in enumerate(pdfs, 1):        rel = src.relative_to(in_dir)        dst = out_dir / rel        try:            add_watermark_to_pdf(src, dst)            ok += 1            print(f"[{i}/{len(pdfs)}] ✅ {rel}")        except Exception as e:            fail += 1            print(f"[{i}/{len(pdfs)}] ❌ {rel}  ->  {e}")            traceback.print_exc()    print(f"\n🎉 处理完成!成功 {ok} 个,失败 {fail} 个")    print(f"📁 输出目录:{out_dir.resolve()}")if __name__ == "__main__":    main()

六、怎么用

  1. 在脚本同目录下新建一个 pdfs 文件夹

  2. 把所有要加水的 PDF 丢进去(支持子文件夹,会自动递归)

  3. 运行:

python pdf_watermark.py
  1. 结果全部输出到 output 文件夹,目录结构和原来一模一样


七、常用参数速查

想要的效果改哪个参数
水印更淡 / 更明显OPACITY(0.05 很淡,0.3 很明显)
水印更大FONT_SIZE
水印更斜ANGLE(改成 45 更斜,0 是水平)
只要中间一个TILE = False
换颜色COLOR = (0.8, 0, 0) 就是红色
换文字WATERMARK_TEXT

八、踩坑指南

坑 1:中文变方块 / 乱码

99% 是字体路径不对。检查这几个位置:

  • Windows:C:\Windows\Fonts\msyh.ttc(微软雅黑)、simhei.ttf(黑体)、simsun.ttc(宋体)

  • macOS:/System/Library/Fonts/PingFang.ttc

  • Linux:先 fc-list :lang=zh 查一下有哪些中文字体

如果 .ttc 报错,换成 .ttf 的字体(比如黑体 simhei.ttf)就行。

坑 2:水印位置对不上

有些 PDF 页面尺寸不是标准 A4(比如扫描件、图纸),脚本是按 页面实际尺寸 生成水印的,所以理论上都能对齐。

如果发现水印跑到页面外了,多半是这个 PDF 有 CropBox 和 MediaBox 不一致,可以在代码里把 page.mediabox 换成 page.cropbox 试试。

坑 3:文件太大

加水印后文件会变大一点(每页多了一层内容流),一般增加 5%~20%,属于正常范围。

坑 4:加密 PDF

脚本已经自动尝试用空密码解密,如果设置了真密码会直接跳过并报错,不影响其他文件。


九、进阶玩法

如果你想让不懂代码的同事也能用,可以:

① 打包成 exe

pip install pyinstallerpyinstaller -F pdf_watermark.py

生成的 dist/pdf_watermark.exe 双击就能跑。

② 加个拖拽界面

用 tkinter + tkinterdnd2 做一个"把 PDF 拖进来就自动加水印"的小窗口,几十行代码的事。

③ 加图片水印(公司 Logo)

把 build_watermark_page 里画文字的代码换成:

c.drawImage("logo.png", x, y, width=100, height=100, mask="auto")

④ 加页码 / 页眉页脚

原理完全一样,只是画的内容从"水印文字"换成"第 X 页 / 共 Y 页"。


十、最后

这个脚本我前后用了两三年,处理过的 PDF 加起来得有几千份了。

核心逻辑其实特别简单:造一层透明的"纸",盖上去,完事。

真正省时间的不是代码多高级,而是把重复劳动交给机器。

相关学习资料