ARTICLE · 1156020
用 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():returnif 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) + 2n_y = int(diag / step_y) + 2c.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:passdst_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()}")returnpdfs = sorted(in_dir.rglob("*.pdf"))if not pdfs:print("❌ 没找到任何 PDF 文件")returnprint(f"📂 共找到 {len(pdfs)} 个 PDF,开始处理...\n")ok = fail = 0for i, src in enumerate(pdfs, 1):rel = src.relative_to(in_dir)dst = out_dir / reltry:add_watermark_to_pdf(src, dst)ok += 1print(f"[{i}/{len(pdfs)}] ✅ {rel}")except Exception as e:fail += 1print(f"[{i}/{len(pdfs)}] ❌ {rel} -> {e}")traceback.print_exc()print(f"\n🎉 处理完成!成功 {ok} 个,失败 {fail} 个")print(f"📁 输出目录:{out_dir.resolve()}")if __name__ == "__main__":main()
六、怎么用
在脚本同目录下新建一个
pdfs文件夹把所有要加水的 PDF 丢进去(支持子文件夹,会自动递归)
运行:
python pdf_watermark.py结果全部输出到
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.ttcLinux:先
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 加起来得有几千份了。
核心逻辑其实特别简单:造一层透明的"纸",盖上去,完事。
真正省时间的不是代码多高级,而是把重复劳动交给机器。