
1. 需求分析与方案选型1.1 为什么选择Python做PDF水印PDF水印这事儿听起来好像很简单——找个工具打开文件加个水印保存完事儿。但实际工作中一旦涉及批量处理事情就变得非常烦躁。几十个、上百个PDF文件每个都要加同样的水印手动操作不仅慢而且容易漏更别提不同文件还要区分内部资料客户预览版已归档这种不同样式的水印。我最早也用过WPS、Adobe Acrobat这些带图形界面的工具处理一两个文件倒没什么问题文件一多就完全是折磨。后来切换到Python来做这套批量水印方案核心原因有几点Python生态里针对PDF的库非常成熟处理水印这种需求不用自己撸PDF底层格式直接用现成库组合就行。脚本化操作天然适合批量任务一个循环跑下来几百个文件也就是几十秒到几分钟的事情。可复现性强同样的脚本换一批文件继续跑效果完全一致不会像手动操作那样时好时坏。后续要扩展功能比如按文件名自动调整水印内容、按目录分类输出只是加几行代码的问题。市面上能实现PDF水印的方案不少我实际用过或者调研过的有这么几类方案优点缺点适用场景WPS/Acrobat手动添加操作直观无需代码批量效率极低样式定制受限临时处理一两个文件在线水印工具简单快捷文件上传有隐私风险批量要收费不涉密、文件少PyPDF2/pypdf直接操作纯Python方案轻量对中文支持差样式控制弱简单标记场景reportlab PyPDF2组合样式可控支持中文批量能力强需要理解两个库的配合逻辑本文场景功能与灵活性的平衡最终我选的是最后一种组合方案reportlab负责生成水印页面PyPDF2负责把水印页面合并到目标PDF的每一页上。后面会详细讲组合方式和踩坑记录这里先说结论——这套方案在批量场景下最稳。1.2 两条技术路线的取舍在确定reportlab PyPDF2之前其实还有一条路可以走直接用PyMuPDF也就是fitz库一步到位。这个库是真的强打开PDF直接往页面上画文字、画线条不需要先做个水印PDF再合并那么绕。我为什么没选它做主力主要原因是字体管理和批量稳定性问题。PyMuPDF的中文渲染依赖系统字体很多时候画出来中文就是乱码或者方块。得先自己找到系统里的中文字体文件路径再用page.insert_text()配合fontfile参数来指定。这里面有个大坑不同机器的字体路径不一样Windows是C:/Windows/Fonts/simhei.ttfLinux是/usr/share/fonts/...一轮部署下来光是适配字体路径就够烦的。reportlab这边就好很多——它自己带了一套字体注册机制用TTFont注册一个中文字体文件之后不管文件放哪台机器跑只要路径不丢渲染效果就稳定。批量场景最怕的就是单页没问题整体出幺蛾子所以稳定优先。至于PyPDF2的版本问题我用的是新版的pypdfPyPDF2的延续项目pip install pypdf就是它API更干净对PDF格式的兼容性也更好。文章里后面写到的代码如果你用的是老版PyPDF22.x之前个别方法名可能对不上建议直接统一用pypdf。2. 环境准备与基础依赖2.1 安装Python与依赖库这一步是基础中的基础但也最常见到卡壳的地方。Python环境如果没装好后面所有代码都是空中楼阁。如果你是第一次配Python环境我建议按下面的顺序来操作去Python官网下载安装包Windows系统安装时务必勾选Add Python to PATH这一步漏了后面在命令行跑pip命令全部报不是内部或外部命令。打开命令提示符或PowerShell先跑下面这行确认Python装好了python --version能显示版本号比如Python 3.12.x就说明成功了。如果没有重启一下命令行再试还不行就说明PATH没配上需要手动加环境变量。安装必要的第三方库直接一行命令搞定pip install reportlab pypdfreportlab用于生成水印PDFpypdf用于合并PDF页面。装有旧版的可以先pip install --upgrade reportlab pypdf更新到新版避免一些老版本API差异的坑。整个依赖环境就这两个核心库不需要额外装别的。这也是我觉得这套方案好的原因之一——依赖少出问题的地方就少。2.2 中文字体文件的准备既然是给中文文档加水印字体这事就绕不开。reportlab默认的字体是Helvetica这类西文字体直接写中文进去会报错或者显示成空白。处理方法是注册一个中文字体用TTFont加载字体文件。字体文件哪来最简单的是直接用系统自带的Windows系统C:/Windows/Fonts/simhei.ttf黑体、C:/Windows/Fonts/simsun.ttc宋体、C:/Windows/Fonts/msyh.ttc微软雅黑macOS系统/System/Library/Fonts/PingFang.ttc苹方、/Library/Fonts/Arial Unicode.ttfLinux系统/usr/share/fonts/truetype/wqy/wqy-microhei.ttc文泉驿微米黑没装的话要apt install fonts-wqy-microhei之类的命令装一下我实际测试下来黑体simhei.ttf做水印效果最好——笔画粗水印看起来清楚倾斜摆放时也容易辨认。微软雅黑也行但笔画细一些在某些PDF背景上可能不够突出。如果你的PDF里某些页面背景比较花需要更醒目的水印黑体是首选。注意.ttc是字体集合文件reportlab的TTFont对.ttc的支持有时候不太稳定不同版本表现不一致。如果加载.ttc报错优先去找.ttf格式的字体文件用或者转成.ttf再用。字体准备、注册、生成水印的完整代码下面逐步来写。3. 水印生成reportlab从零搭建3.1 使用canvas生成单页水印PDF整个方案的核心思路先用reportlab生成一个和PDF页面尺寸一致的水印PDF水印PDF的内容就是我们要的平铺水印然后用pypdf把这个水印PDF作为透明层叠加到目标PDF的每一页上。为什么中间要隔一个水印PDF而不是直接在reportlab里画完就输出图片因为pypdf的merge_page方法接收的对手是页面对象页面对象最方便的来源就是一个现成的PDF文件。同时这样设计还有一个附加好处同一个水印PDF可以被反复用来处理不同目录下的文件生成一次万物皆用。先写生成水印PDF的基础代码from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont # 注册中文字体 pdfmetrics.registerFont(TTFont(SimHei, C:/Windows/Fonts/simhei.ttf)) # A4页面尺寸单位是磅point1磅约等于0.35毫米 PAGE_W, PAGE_H A4 # 595.32 x 841.92 # 创建一个空白的PDF画布 watermark_pdf watermark.pdf c canvas.Canvas(watermark_pdf, pagesizeA4) # 设置水印文字样式 c.setFont(SimHei, 42) # 字号42磅 c.setFillAlpha(0.15) # 透明度15%让水印不影响正文阅读 c.setFillColorRGB(0.5, 0.5, 0.5) # 灰色 # 在页面中心写一行水印 c.saveState() c.translate(PAGE_W / 2, PAGE_H / 2) # 把原点移到页面中心 c.rotate(45) # 绕中心旋转45度 c.drawCentredString(0, 0, 内部资料) # 居中绘制文字 c.restoreState() # 保存水印PDF c.showPage() c.save()这段代码跑完会生成一个watermark.pdf文件打开看就是一页A4纸正中心倾斜45度一行灰色内部资料水印文字。几个关键点的解释canvas.Canvas创建的是PDF绘图环境操作方式和Windows画图软件类似——移动画笔、设置颜色、写字、保存。translate把坐标系原点移到了页面中心这样无论画什么内容都以中心为参考点来定位写旋转水印特别方便。saveState()和restoreState()必须成对出现它们的作用是记住当前状态和恢复之前记住的状态。如果不加这两行旋转之后坐标系就歪了后面的绘制都会受影响。水印的文字方向、字号、颜色、透明度都是可以调参的后面会给你一个参数速查表。3.2 多行多列平铺水印的实现实际工作中那种只有一行水印的效果往往不够用。合同、图纸、报表这类多页文档水印稀疏了容易被裁剪掉或者截取到无字区域。行业里更常见的要求是整页平铺水印——多行多列均匀铺满整张纸不管从哪一段截屏都能看到水印痕迹。多行多列的实现思路也不复杂算出列间距和行间距然后嵌套循环逐个位置画水印即可。这里给出一套完整、可实际用的平铺水印生成代码def create_watermark_pdf(output_path, text, font_pathC:/Windows/Fonts/simhei.ttf): 生成多行多列平铺水印的PDF :param output_path: 输出水印PDF路径 :param text: 水印文字内容 :param font_path: 中文字体文件路径 pdfmetrics.registerFont(TTFont(WaterMarkFont, font_path)) c canvas.Canvas(output_path, pagesizeA4) page_w, page_h A4 # 水印样式参数 font_size 24 # 字号多行多列场景字号不宜过大 opacity 0.12 # 透明度 row_gap 160 # 行间距磅 col_gap 320 # 列间距磅 angle 45 # 旋转角度 c.setFont(WaterMarkFont, font_size) c.setFillAlpha(opacity) c.setFillColorRGB(0.5, 0.5, 0.5) # 计算水印文字宽度用于居中偏移修正 text_width pdfmetrics.stringWidth(text, WaterMarkFont, font_size) # 横向列循环 x col_gap / 2 while x page_w: # 纵向行循环 y page_h - row_gap / 2 while y 0: # 对每个水印位置旋转并居中绘制 c.saveState() c.translate(x, y) c.rotate(angle) c.drawCentredString(0, 0, text) c.restoreState() y - row_gap x col_gap c.showPage() c.save()这里面最核心的是一个先旋转后画字的组合把坐标原点移动到目标位置 → 旋转坐标系 → 在(0,0)点居中画字。这样画出来的水印每个都是独立旋转45度且围绕自身中心旋转不会出现整体排版偏移。计算行数和列数没有用固定公式而是靠while循环来判断当前坐标还在页面范围内。这样可以保证不管你的row_gap和col_gap怎么调水印总能覆盖到页面边缘不会出现末尾缺一半的情况。列间距和行间距的默认值是我调的比较舒服的参数A4纸上大概是4列×6行的密度你也可以按自己的审美调整。3.3 水印样式参数速查水印效果好不好看很大程度上取决于参数调得合不合适。我把常用的几个参数整理成了表格方便你按需取用参数推荐值效果说明font_size20-30平铺场景推荐24左右单行居中推荐40-50opacity0.08-0.20数值越小越淡超过0.2会明显影响阅读row_gap120-180行间距越小水印越密集col_gap250-350列间距越小水印越密集angle30-6045度最经典30度偏平缓60度更陡颜色灰(0.5,0.5,0.5)灰色最不抢眼也可以用浅红做机密水印参数没什么标准答案全是经验值。我的习惯是先随便生成一页用PDF阅读器放大看效果再回头调参数。就这个生成→看→调的循环比什么参数公式都管用。4. 核心功能实现批量给PDF加水印4.1 单文件水印合并的原理与代码生成好水印PDF之后接下来要做的就是把水印合并到目标PDF的每一页。这一步用pypdf来实现原理可以理解成两张纸叠在一起先把水印PDF读成一个页面对象然后循环目标PDF的每一页调用merge_page把它盖上去。这个merge_page方法做的事情本质上是把两个页面的内容流合并成一个新的内容流。屏幕上看起来是同一页里既有原来的内容又有水印层的内容实际上是两份PDF绘制指令叠加后的结果。这也是为什么水印PDF本身的页面背景必须是透明的reportlab生成的PDF默认就是透明背景不需要额外处理。直接上代码from pypdf import PdfReader, PdfWriter def add_watermark_to_pdf(input_pdf, output_pdf, watermark_pdf): 给单个PDF文件添加水印 :param input_pdf: 输入PDF路径 :param output_pdf: 输出PDF路径 :param watermark_pdf: 水印PDF路径 # 读取目标PDF和水印PDF reader PdfReader(input_pdf) watermark PdfReader(watermark_pdf).pages[0] # 创建PDF写入器 writer PdfWriter() # 遍历每一页合并水印 for page in reader.pages: page.merge_page(watermark) # 第1页水印合并到第1页 writer.add_page(page) # 写出结果 with open(output_pdf, wb) as f: writer.write(f) print(f已处理: {input_pdf} - {output_pdf})这段代码只需要注意一个点水印PDF只取pages[0]第一页因为reportlab生成的水印PDF本来就只有一页。如果水印PDF有多个页面你甚至可以按目标页序号循环取不同页的水印——比如第1页用机密第2页用内部资料不过这个场景比较少见就不展开了。4.2 批量处理目录下的所有PDF单个文件的加水印实现之后批量就只是加了一层循环结构。但批量处理里有一个很关键的细节——输出文件的组织方式。我见过不少人在批量处理时直接把原文件覆盖掉这是大忌。万一处理出来的PDF有问题比如水印位置不对、颜色太深影响阅读原文件已经被覆盖了想反悔都来不及。稳妥的做法是原文件保持不动处理结果输出到一个新的目录或者带后缀的新文件。下面是一套同时支持单文件和整目录的封装代码import os from pathlib import Path def add_watermark_batch(input_path, output_dirNone, watermark_pdfwatermark.pdf): 批量给PDF添加水印 :param input_path: PDF文件路径 或 目录路径 :param output_dir: 输出目录None则自动生成在原目录下创建watermarked子目录 :param watermark_pdf: 水印PDF路径 # 判断输入是文件还是目录 if os.path.isfile(input_path): pdf_files [input_path] elif os.path.isdir(input_path): pdf_files list(sorted(Path(input_path).glob(*.pdf))) else: print(f路径不存在: {input_path}) return if not pdf_files: print(未找到任何PDF文件) return # 输出目录处理 if output_dir is None: if os.path.isfile(input_path): output_dir os.path.dirname(input_path) else: output_dir os.path.join(input_path, watermarked) os.makedirs(output_dir, exist_okTrue) # 逐个处理 for i, pdf_file in enumerate(pdf_files, 1): filename os.path.basename(pdf_file) output_path os.path.join(output_dir, filename) print(f[{i}/{len(pdf_files)}] 正在处理: {filename}) try: add_watermark_to_pdf( str(pdf_file), output_path, watermark_pdf ) except Exception as e: print(f处理 {filename} 时出错了: {e}) continue print(全部处理完成)这套函数的关键点支持传单个文件路径、也支持传目录路径两种调用方式都兼容。默认在输入目录下创建一个watermarked子目录所有结果统一输出到那里和原文件隔离。逐个文件处理并打日志批量过程中任何一个文件出错都不会中断整个流程而是打印错误后跳过继续下一个。这在批量60个文件、其中两三个损坏的场景下是真的救命——不然一个文件坏了后面全部白跑。4.3 等比例缩放给不同尺寸PDF加适配水印前面处理A4尺寸的PDF没问题但现实很残酷——很多PDF不是A4。我在实际项目里碰到过有A3幅面的图纸扫描件、有B5的书页扫描件甚至还有自定义尺寸的报表文件。如果水印PDF固定按A4尺寸生成合并到其他尺寸的页面上时水印就不是居中平铺的状态——要么偏在一角要么只覆盖一部分页面。问题的原因很简单pypdf的merge_page是按坐标原点对齐的两个页面尺寸不同水印层只覆盖了和A4等大的区域。解决方案也不复杂在合并之前先读取目标页面的尺寸然后把水印PDF等比缩放到和目标页面一致。先确定目标页面尺寸再用add_transformation方法做缩放矩阵变换from pypdf import PdfReader, PdfWriter, Transformation def add_watermark_to_pdf(input_pdf, output_pdf, watermark_pdf): 给单个PDF添加水印自动适配目标页面尺寸 reader PdfReader(input_pdf) watermark PdfReader(watermark_pdf).pages[0] # 获取水印原始尺寸 wm_w float(watermark.mediabox.width) wm_h float(watermark.mediabox.height) writer PdfWriter() for page in reader.pages: # 获取目标页面尺寸 page_w float(page.mediabox.width) page_h float(page.mediabox.height) # 计算缩放比例以宽度为基准适配 scale_x page_w / wm_w scale_y page_h / wm_h # 选择一个能完全覆盖页面的缩放比例 # 如果只是简单等比缩放用 width 比例即可 watermark_scaled watermark if abs(wm_w - page_w) 1 or abs(wm_h - page_h) 1: watermark_scaled watermark.transfer_transformation( Transformation().scale(scale_x, scale_y) ) page.merge_page(watermark_scaled) writer.add_page(page) with open(output_pdf, wb) as f: writer.write(f)这里有个细节值得注意我是按宽度和高度分别计算缩放比例然后分别应用到x轴和y轴上。如果原PDF的宽高比和水印PDF不一样比如水印是A4竖版目标是A4横版这种非等比缩放会把水印拉伸变形。解决办法是取max(scale_x, scale_y)来等比缩放缩放后超出边界的部分让页面裁剪掉这样水印不会被拉伸只是边缘可能会被裁掉一点点——但是因为我们的水印设计的时候多行多列铺满了整页裁掉一点边缘完全不影响效果。# 等比例缩放保证水印不变形 scale max(scale_x, scale_y) watermark_scaled watermark.transfer_transformation( Transformation().scale(scale, scale) )优先用这种等比例方式非等比的方式在绝大多数文档水印场景下看起来会有点奇怪。4.4 保存原PDF的元数据与书签深层一点的批处理需求里有一个坑值得提pypdf的PdfWriter在默认情况下不会保留原PDF的元数据比如作者、标题、创建时间这些信息也不会保留书签目录结构。如果你处理的是正式文档处理完之后用PDF阅读器打开发现文件标题变成了untitled书签栏空了观感会很糟糕。规避方法是在写入之前把原PDF的元数据复制过去同时手工把书签大纲重新挂到writer里。下面这段代码是把元数据和书签都保留的完整写法from pypdf import PdfReader, PdfWriter, Transformation def add_watermark_preserve_meta(input_pdf, output_pdf, watermark_pdf): reader PdfReader(input_pdf) watermark PdfReader(watermark_pdf).pages[0] # 提取原文件书签大纲 outline reader.outline writer PdfWriter() # 复制每一页并合并水印省略了尺寸适配的代码 for page in reader.pages: page.merge_page(watermark) writer.add_page(page) # 复制元数据 writer.add_metadata(reader.metadata or {}) # 重建书签结构 if outline: writer.add_outline_item(outline[0], 0) # 先添加顶级条目 for i in range(1, len(outline)): # 这里简化处理只示例复杂层级需要用add_outline_item逐一处理 pass with open(output_pdf, wb) as f: writer.write(f)书签这块pypdf的API在不同版本里略有不同老版本用的是addOutlines新版本是add_outline_item。如果你发现书签添加失败多半是版本问题。查看版本号可以用pip show pypdf注意完整保留书签层级结构在pypdf里需要遍历原PDF的outline树代码比较繁琐。如果只是给自己看的文档不保留书签问题不大但如果是给客户或同事交付的成品建议还是做一下元数据和书签的保留专业度差别很大。5. 命令行工具化从脚本到可用工具的封装5.1 支持命令行参数调用写好的脚本如果每次都去改代码里的路径那用起来还是不够痛快。更好的做法是把脚本改造成一个命令行工具指定输入、输出、水印文字一条命令跑完。标准库里的argparse就够用了不需要额外装click或者fire。一个完整的命令行入口长这样import argparse def main(): parser argparse.ArgumentParser(description为PDF批量添加水印) parser.add_argument(input, help输入PDF文件或目录) parser.add_argument(-o, --output, defaultNone, help输出目录默认自动生成) parser.add_argument(-t, --text, default内部资料, help水印文字内容) parser.add_argument(-f, --font, defaultC:/Windows/Fonts/simhei.ttf, help中文字体文件路径) parser.add_argument(--font-size, typeint, default24, help水印字号多行平铺场景建议20-30) parser.add_argument(--opacity, typefloat, default0.12, help水印透明度0-1之间) parser.add_argument(--angle, typefloat, default45, help水印旋转角度) parser.add_argument(--temp-watermark, default_tmp_watermark.pdf, help临时水印PDF路径) args parser.parse_args() # 1. 生成水印PDF create_watermark_pdf( args.temp_watermark, args.text, font_pathargs.font, font_sizeargs.font_size, opacityargs.opacity, angleargs.angle, ) # 2. 批量添加水印 add_watermark_batch( args.input, output_dirargs.output, watermark_pdfargs.temp_watermark, ) # 3. 清理临时文件 if os.path.exists(args.temp_watermark): os.remove(args.temp_watermark) if __name__ __main__: main()封装完之后的使用方式# 给单个文件加水印 python pdf_watermark.py ./report.pdf -t 内部资料 # 给整个目录下的PDF加水印自定义透明度和字号 python pdf_watermark.py ./docs/ -o ./docs_watermarked/ -t 机密 --opacity 0.2 --font-size 30 # 指定中文字体文件 python pdf_watermark.py ./docs/ -f /usr/share/fonts/truetype/wqy/wqy-microhei.ttc这样一条命令就能跑完整套流程不管是自己日常用还是配合其他自动化脚本调用都很方便。5.2 断点续跑与输出文件管理批量处理几十个文件的时候中途很可能因为某个文件损坏、加密、或者路径异常导致脚本中断。前面批量函数里的try/except已经保证了单个文件失败不影响整个流程但是如果真的跑到第30个文件进程崩了比如内存不够或者被系统杀掉已经处理完的20个文件不会丢剩下的重跑一遍就行了。输出目录里会混着之前已经处理好的文件和新处理的文件怎么区分我习惯在每次运行时加一个时间戳子目录或者根据需求选择覆盖模式import time def main(): # ... 省略argparse部分 if args.output is None: time_str time.strftime(%Y%m%d_%H%M%S) args.output os.path.join(args.input, fwatermarked_{time_str})加时间戳的另一个好处是可以放心对比多次处理的效果——同一批文件第一次用透明度0.1处理第二次用透明度0.15处理两个输出目录并存对比看效果非常直观。6. 常见问题与排坑记录6.1 中文水印显示为方块或乱码这是出现频率最高的问题。排查思路很固定确认字体注册成功——如果registerFont那一步就报了KeyError或者FileNotFoundError检查字体文件路径是否正确特别是.ttc字体报错的概率比.ttf大。确认设置字体用的名字和你注册的名字一致——注意大小写和键盘符号TTFont(SimHei, ...)之后c.setFont(SimHei, 42)必须用同一个字符串。我曾经手滑注册的是SimHei后面写成了Simhei找了半天才定位到这个低级错误。确认系统字体文件本身没问题——有些下载的字体文件其实是损坏的注册时能通过但渲染不出来。换个字体文件试试就知道是不是这个原因。排查口诀先看注册报不报错再看名字匹不匹配最后换字体文件验证。6.2 合并之后水印位置偏移或只覆盖部分页面这个问题的根源就是前面聊过的页面尺寸不一致。比如原PDF是A4竖版595 x 842磅但某些扫描件可能是Legal尺寸612 x 1008磅甚至是一些印刷品扫描后带着切割标记的怪尺寸。水印PDF如果始终按A4生成合进去之后就会偏。解决方案就是我前面写的等比例缩放逻辑。但如果你遇到了水印缩放之后糊成一团的情况可能是缩放比例太大导致水印线条被拉伸模糊。这时可以试着生成水印PDF时把一个更大的页面作为基准比如直接用目标尺寸生成让水印原始像素密度足够高缩放时就不容易失真。6.3 加了水印后PDF文件体积剧增处理前10MB的PDF加水印后变成100MB——我遇到过好几次基本都是水印PDF本身有问题。如果你在水印PDF里用了大尺寸的嵌入图片比如把图片水印当背景体积自然会爆炸。但如果你用的是文字水印体积变大还有一个常见原因reportlab默认嵌入的是Type0字体且嵌入方式不够优化每页都引用一份字体子集。优化方法有两个方向水印PDF只生成一次重复利用——这也是我反复强调先生成水印PDF再批量合并的原因之一。如果你每一个文件都重新生成一次水印PDF并且水印PDF还被嵌入了字体子集那体积损失就是N倍。合并后用pypdf的compress_content_streams做一次压缩from pypdf import PdfReader, PdfWriter def compress_pdf(input_pdf, output_pdf): reader PdfReader(input_pdf) writer PdfWriter() for page in reader.pages: page.compress_content_streams() # 压缩内容流 writer.add_page(page) with open(output_pdf, wb) as f: writer.write(f)加了压缩后体积能从明显膨胀降到略微增大而且压缩对肉眼可见的质量几乎没影响。6.4 加密PDF处理失败如果你的PDF文件是从客户那边拿来的很可能带有打开密码或者编辑限制。pypdf读取加密PDF会直接抛异常需要先解密。from pypdf import PdfReader reader PdfReader(encrypted.pdf) if reader.is_encrypted: try: reader.decrypt() # 如果文件有密码在这里填密码 except Exception as e: print(f解密失败: {e})注意两个细节第一有些PDF虽然打开不用密码但设置了限制编辑权限这种算is_encryptedTrue但密码为空字符串可以解决第二decrypt方法在高版本pypdf中返回的是密码等级值1代表有密码不是布尔值别把返回值当布尔判断导致逻辑错误。6.5 多文件处理速度慢批量处理性能方面如果文件数量多几百个每个文件都要重新打开、遍历、合并、写出耗时确实上去了。我实测过一个100页、10MB左右的PDF在我的笔记本上加水印大约耗时1.5秒。如果是300个这样的文件大概需要7-8分钟。性能优化的方向水印PDF提前生成并复用不要在循环里重复生成这个前面已经强调过了。原PDF很大时pypdf是逐页读取并处理内存占用不会成为瓶颈。但如果单个PDF超过300页建议分段处理防止写文件时内存溢出。用多进程并行处理每个进程处理一批文件能用满多核CPU。标准的multiprocessing.Pool就能实现import multiprocessing as mp def worker(args): pdf_file, output_dir, watermark_pdf args filename os.path.basename(pdf_file) output_path os.path.join(output_dir, filename) add_watermark_to_pdf(pdf_file, output_path, watermark_pdf) # 在批量函数里用进程池 with mp.Pool(processes4) as pool: tasks [(str(f), output_dir, watermark_pdf) for f in pdf_files] pool.map(worker, tasks)要注意的是mp.Pool在Windows上必须放在if __name__ __main__保护块里否则会无限递归启动子进程。7. 场景扩展这个方案还能怎么用水印功能做完之后你会发现这套PDF处理管道可以延伸出不少变体加的需求往往只是改改水印内容或者处理逻辑不用大动干戈。7.1 按文件名动态设置水印内容有些场景下不同文件要打不同水印。比如一个目录下混着合同和报价单两类文件要求合同打合同专用报价单打报价仅供参考。实现思路是在批量循环里根据文件名关键词选择不同的水印文字然后重新生成水印PDF再来合并def get_watermark_text(filename): if 合同 in filename: return 合同专用 elif 报价 in filename: return 报价仅供参考 else: return 内部资料然后在批量函数里把先生成水印PDF这步挪到循环内部每个文件根据文件名生成对应水印再合并。代价是性能上每次都要重新生成水印PDF会慢一些但胜在动态灵活。7.2 时间戳水印或页脚水印如果需要给每页加上打印时间或者第X页/共Y页这种动态页脚水印比上面的方案更进了一步——水印PDF的每一页内容都不同不能再复用一个单页水印。做法是直接读目标PDF的页数在生成水印PDF时生成多页每一页对应一个页码或者时间戳。核心代码如下# 先从reader拿到总页数 reader PdfReader(input_pdf) total_pages len(reader.pages) # 生成多页水印PDF c canvas.Canvas(page_num_watermark.pdf, pagesizeA4) for i in range(1, total_pages 1): c.setFont(SimHei, 12) c.drawString(30, 30, f第 {i} 页 / 共 {total_pages} 页) c.showPage() # 结束当前页开启下一页 c.save()合并时注意水印PDF的页数要和目标PDF一致merge_page时要取水印PDF对应的那一页。7.3 批量重命名配合水印归档水印处理完往往还牵扯到归档命名的问题。结合热搜词里很多人提到的批量重命名可以顺手把这一步也整合进流程处理完的文件按规律重命名成合同编号_客户名_日期.pdf之类的格式再统一输出到归档目录。重命名的逻辑和工具千奇百怪有的是从文件内容里提取关键信息有的是根据已有的Excel映射表批量替换。核心思路就是在批量处理的循环里处理好水印后顺手做一个重命名一步到位。代码就是多一个文件名映射表的事rename_map { 20230101_001.pdf: 合同_张先生_20230101.pdf, 20230101_002.pdf: 报价单_李女士_20230101.pdf, } # 处理水印时检查映射表有则重命名无则保持原名写在最后的一点个人体会这套PDF批量水印方案我从最早手动操作WPS到后来写脚本半自动再到现在封装成命令行工具前前后后迭代了好几版。最大的体会是工具没有最好的只有最趁手的。reportlab pypdf这套组合也许不是速度最快的也不是功能最全的但它胜在轻量、可控、不依赖网络服务数据安全上有保障。对刚接触Python的人我的建议是别急着整套方案一步到位先跑通单文件加水印的最小流程再逐步扩展成批量、再封装成命令行工具。每一步都验证过再往前走出了问题也好定位。配色参数也别迷信什么标准值多调几次找到你自己最满意的效果就是对的。如果后续你在实际使用中碰到什么奇怪的PDF兼容性问题也可以多查查对应库的文档和issue区——PDF这个格式说复杂也复杂同一个文件在不同工具里表现不一致的情况时有发生不慌耐心排查就行。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。