你是不是也遇到过这种情况:领导丢过来一个文件夹,里面几十个PDF,要求把身份证号、手机号、银行账号全部打码,下午就要交。单份文件用编辑器涂抹还能应付,一旦文件数量上来,手动操作就成了纯粹的体力活,而且特别容易漏。我前段时间接了个这样的需求,最后直接做出来一个“PDF批量加马赛克,抹除敏感信息绿色版”的小工具,双击运行、指定输入输出目录,剩下交给脚本处理,实测下来几十份文件几分钟搞定,比手工涂得还整齐。
这个需求在招投标、合同管理、银行流水处理、病历脱敏这些场景里太常见了。但真正做起来你会发现,单份打码谁都会,难的是“批量”和“保证不漏”。这篇文章就把我从选型、脚本实现、打包绿色版到踩坑排查的完整过程记录下来,给正在被同类需求折磨的朋友一个可以直接参考的方案。
1. 先说清楚:这个“批量打码”到底要解决什么问题
1.1 单份打码简单,难的是“批量”和“不漏”
用Adobe Acrobat或者WPS PDF给一份PDF打码,操作本身不复杂。打开文件,找到敏感区域,框选,涂黑或者添加马赛克效果,保存。单份文件这么做完全没问题,效率取决于你的手速。
但一旦变成四十份、八十份、一百份,问题立刻就暴露了。
第一个问题是效率。每份文件都要打开、定位、涂抹、放大检查、保存,循环往复。一份文件算三分钟,四十份就是两个小时。如果文件页数多、敏感信息位置分散,时间还要翻倍。更关键的是,这个时间成本是纯重复劳动堆出来的,毫无技术含量,但你又不得不做。
第二个问题是“漏”。手动操作最容易出错的地方就是漏涂。尤其当文件内容相似但不完全相同时,眼睛一花就看漏了。投标文件里漏一处身份证号,合同里漏一个银行账号,这种过失的后果不需要我多解释。特别是走正规流程的文件,一点疏漏都可能引发麻烦。
第三个问题是“一致性”。手工涂出来的方块大小不一、位置歪斜,放到一起对比着看,观感很差。审核方拿到手,一眼就能看出这个文档是手工处理的,有些区域涂得遮遮掩掩,反而显得不专业。用脚本统一处理,所有遮盖矩形都是精确到坐标的,输出效果整齐划一。
1.2 批量打码的典型使用场景
根据我实际遇到的情况,适合用这套方案处理的文件主要有下面几类:
- 招投标文件:法定代表人身份证复印件、授权书、银行资信证明中的账号信息。这类文件往往每个标段一套,文件结构高度相似。
- 合同扫描件:双方手机号、银行账号、联系地址、邮箱。合同一签就是几十份,扫描存档后要提供给第三方时,必须逐份脱敏。
- 银行流水、征信报告:姓名、证件号、账号,很多是从网银导出的固定模板,敏感字段位置相对固定。
- 历史档案发布:旧档案扫描成PDF后要放到内网或者对外公开,必须把相关人员信息抹掉。
这些场景的共同特点:文件数量多、敏感字段可以按规则定位(比如固定页眉、固定位置出现“身份证号”字样)、文件格式相对结构化。只要满足这三个条件,就有条件做批量自动化处理。
1.3 什么情况不适合固定规则批量打码
如果遇到的是“几百份PDF,每份敏感区域位置完全随机,内容也毫无规律”,那就不适合用固定坐标批量打码,需要先解决“区域自动定位”的问题。
比如一份扫描件,有的页面身份证号在左上角,有的在右下角,有的还是倾斜的,这种情况单纯套固定矩形坐标肯定不行,得走OCR识别加动态定位的路子,复杂度会高一个台阶。这篇文章里的核心逻辑能帮你解决其中一大半场景,碰上完全无规律的,可以在自动定位的基础上再叠加一层人工抽检。
2. 方案选型:为什么最后选了脚本而不是编辑器
2.1 桌面编辑器的涂抹能力与边界
先说结论:主流PDF编辑器在“人工处理单份文件”这个场景下体验都还行,但批量自动化基本都瘸腿。
我试过的几个工具,简单说一下感受:
| 工具 | 涂抹/脱敏能力 | 批量能力 | 备注 |
|---|---|---|---|
| Adobe Acrobat Pro | 支持“标记为敏感信息”,能彻底删除内容 | Action脚本配置繁琐,做批量坐标绘制不直观 | 功能最强,但太重 |
| PDF-XChange Editor | 矩形填充、高亮操作方便,轻量绿色版多 | 没有原生批量涂抹入口,只能一个个点 | 适合少量文件 |
| WPS PDF | 操作简单,中文支持好 | 批量功能依赖会员和模板,限制多 | 免费版不够用 |
| 福昕PDF | 涂抹功能基础,能做注释块 | 批量功能集中在批量转换,不覆盖涂抹 | 场景有限 |
如果你处理的文件只有三五份,用PDF-XChange或者Adobe手动涂完全没问题。但文件量一上来,“人工逐张涂”这个动作就必须被脚本替换掉。
2.2 脚本方案的核心优势
我最后选择的是Python + PyMuPDF组合。说几个它比手工编辑器强的地方:
第一是“可重复”。第一次调好参数之后,第二次、第三次处理同类文件,直接复用配置文件,不用重新画。比如这次处理这套投标文件的授权书,下个月又来一批同样结构的,配置文件和脚本都不用改,跑一遍就行。
第二是“可审计”。脚本执行完可以输出一个日志,记录哪些文件处理了、哪些文件失败了、每个文件打了几个遮罩。这在批量处理几十上百份文件时非常关键,回头要检查或者写处理说明,直接翻日志就行。
第三是“可控精度”。区域坐标精确到point,不会出现手抖涂歪的情况。同一批文件的所有遮罩矩形大小、位置、颜色全部一致,格式上非常规范。
2.3 在线工具的隐私风险
有朋友推荐过在线PDF编辑器,上传后自动识别敏感信息打码。我从来不用这类工具处理正经文件,原因特别简单:你正在脱敏的文件,本身就是敏感信息。把它们上传到第三方服务器,等于先把隐私信息交出去,再让别人帮你打码。这逻辑上就说不通。
而且在线工具生成的PDF经常带水印,有的还有文件数量、大小限制,处理速度也不稳定。脱敏这件事,工具必须是本地化的。
2.4 绿色版的真正含义
标题里写了“绿色版”,顺手说明一下我的理解。很多朋友看到“绿色版”会去搜现成的免安装工具,但实际找下来,真正能做到“指定输入目录、输出目录,自动批量打码”的现成工具少之又少。大多数绿色版PDF编辑器还停留在“人工逐张涂”的阶段,并没有解决批量问题。
所以我这里的“绿色版”,指的是用PyInstaller把脚本打包成免安装exe,不写入注册表、不依赖系统Python环境,拷到任何Windows电脑上双击就能跑。需要换电脑使用时,整个文件夹拷走就行,这才是我理解的“绿色”。
3. 核心实操:从代码到可用的批量打码脚本
3.1 环境与基础代码
用Python操作PDF,我首选PyMuPDF(即fitz)。原因有三个:它能对PDF页面做坐标级操作,能搜索文本块位置,还能执行真正的内容删除,API设计直白,适合写单文件脚本。
安装命令:
pip install PyMuPDF第一版脚本非常简单:遍历输入目录里所有PDF,遍历每一页,用draw_rect在指定坐标画一个黑色矩形,覆盖在敏感信息上方,然后保存到输出目录。
import fitz # PyMuPDF import os input_dir = r"D:\temp\pdf_in" output_dir = r"D:\temp\pdf_out" os.makedirs(output_dir, exist_ok=True) # 需要遮盖的矩形区域,格式为 (x0, y0, x1, y1),单位是 pt(磅) mask_rects = [ (50, 100, 250, 130), (400, 500, 600, 530), ] for file in os.listdir(input_dir): if not file.lower().endswith(".pdf"): continue pdf_path = os.path.join(input_dir, file) doc = fitz.open(pdf_path) for page in doc: page_rect = page.rect for rect in mask_rects: r = fitz.Rect(rect) if r.x1 <= page_rect.x0 or r.y1 <= page_rect.y0: continue page.draw_rect(r, color=(0, 0, 0), fill=(0, 0, 0)) output_path = os.path.join(output_dir, file) doc.save(output_path, garbage=4, deflate=True) doc.close() print("处理完成")这段代码能跑,但只能实现“画覆盖层”。什么意思?它是在原本的文字之上画了一个黑色矩形,看起来是遮住了,但底下的文字层还在。用阅读器全选、复制,或者用PDF转Word工具一还原,敏感信息立刻露出来。这是批处理打码中最要命的一个细节,后面会专门讲。
3.2 让遮盖真正“删除内容”而不是“盖在上面”
PyMuPDF里有两个核心方法:add_redact_annot和apply_redactions。
page.add_redact_annot(rect):把指定区域标记为需要删除的区域。page.apply_redactions():真正执行删除操作,把区域内的文本、图像对象从PDF数据里移除,而不是仅仅画一个矩形盖在上面。
这两个方法配合使用,才能叫“抹除敏感信息”。正确的代码逻辑应该是:
import fitz import os input_dir = r"D:\temp\pdf_in" output_dir = r"D:\temp\pdf_out" os.makedirs(output_dir, exist_ok=True) mask_rects = [ (50, 100, 250, 130), (400, 500, 600, 530), ] for file in os.listdir(input_dir): if not file.lower().endswith(".pdf"): continue doc = fitz.open(os.path.join(input_dir, file)) for page in doc: for rect in mask_rects: r = fitz.Rect(rect) page.add_redact_annot(r, fill=(0, 0, 0)) # 用黑色填充删除区域 page.apply_redactions() # 真正执行删除 doc.save(os.path.join(output_dir, file), garbage=4, deflate=True) doc.close() print("处理完成")这里有三个细节值得注意:
一是garbage=4和deflate=True。garbage=4表示保存时做最彻底的对象回收,把被移除的内容从文件结构里清干净;deflate=True则对输出做压缩,避免文件体积失控。
二是fill=(0,0,0)表示删除后留下的区域用黑色方块填充。如果想要“马赛克”那种模糊网格效果,可以改用灰色,或者干脆把像素化处理放到后面做。
三是 redaction 的边界一定要精确,稍微大一点没关系,但不能小。区域小了,边缘会残留半截字符,等于没删干净。
3.3 从固定坐标到“按内容自动定位”
固定坐标在真实场景里不够用,因为每份PDF页面大小、排版、内容位置都可能不同。我的做法是:先用page.search_for()查找关键字,拿到文字块的坐标框,再把这些坐标框作为遮罩区域。
比如,要遮掉“身份证号”后面跟着的内容,可以先搜索“身份证号”这个关键词,拿到它的位置,再向右延伸一定宽度。
def find_and_redact(page, keyword="身份证号", extend=150): rects = page.search_for(keyword) for r in rects: # 从关键字位置向右扩展,覆盖后面的内容 redact_rect = fitz.Rect(r.x0, r.y0, r.x1 + extend, r.y1) page.add_redact_annot(redact_rect, fill=(0, 0, 0))更进一步,可以用正则表达式直接在PDF文本里匹配身份证号、手机号、银行账号的常见模式,把匹配到的文本块全部定位出来,实现全自动打码。
import re import fitz patterns = [ (re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)"), "手机号"), # 粗略手机号 (re.compile(r"\d{17}[\dXx]"), "身份证号"), # 粗略身份证号 ] doc = fitz.open(pdf_path) for page in doc: for pattern, tag in patterns: matches = page.search_for(pattern) # PyMuPDF 支持正则搜索 for r in matches: page.add_redact_annot(r, fill=(0, 0, 0)) page.apply_redactions()这里有个前提:文档必须有文本层。如果是纯扫描图片,search_for()找不到任何内容,需要走后面说的“扫描件图片处理”方案。
3.4 怎么快速拿到要遮盖的坐标
很多朋友会卡在一件事上:我怎么知道敏感信息在页面上的坐标是多少?
几个实操办法:
第一个办法,用PyMuPDF输出所有文本块坐标。写个临时小脚本,把每一页的文字块和它的bbox打印出来,对照定位。
doc = fitz.open("sample.pdf") for page_no, page in enumerate(doc): for block in page.get_text("blocks"): x0, y0, x1, y1, text = block[0], block[1], block[2], block[3], block[4] print(f"第{page_no+1}页 坐标({x0:.1f},{y0:.1f})-({x1:.1f},{y1:.1f}): {text.strip()[:20]}")第二个办法,如果能提前从别人那里拿到模板文件,直接量尺寸就行。比如页面是A4大小210mm x 297mm,转成pt就是595.27 x 841.89。一个目标区域位于页面顶部居中位置,换算一下就能得到坐标。
第三个办法,把PDF页面渲染成PNG图片,用绘图软件打开,直接看像素或者用标尺量,然后再换算成pt单位。
这几个方法各有利弊,我用得最多的是第一种,因为它最直接,几秒钟就能看到结果。
3.5 批量配置化:把参数和代码分开
为了让工具能给别人用,也方便自己换场景复用,我把参数抽到了配置文件里。配置文件用JSON,结构类似这样:
{ "input_dir": "./input", "output_dir": "./output", "mask_color": [0, 0, 0], "page_areas": [ { "pages": [1], "rects": [ [50, 100, 250, 130], [400, 500, 600, 530] ] }, { "pages": "all", "rects": [ [100, 50, 300, 70] ] } ], "keywords": ["身份证号", "手机号码"] }pages字段既可以是具体页码,也可以写成"all",表示所有页面。这样一套配置就能对应一类文件,同一个脚本、不同配置就能处理不同项目。
主程序只需要读配置、遍历目录、按配置执行redaction、输出结果,逻辑非常干净。
3.6 扫描件图片的马赛克处理
扫描件PDF没有文本层,敏感信息全部是图片里的像素。这类文件用 redaction 只能整块删除区域内的图片对象,但那样会直接挖掉一块图,看起来不自然。想要真正实现“马赛克”效果,需要换一种思路:
- 用
page.get_pixmap(dpi=150)把页面渲染成图片; - 用PIL定位敏感区域,对该区域做像素化处理;
- 把处理后的图片重新合成回PDF。
像素化代码示意:
from PIL import Image img = Image.open("page.png") crop = img.crop((x0, y0, x1, y1)) small = crop.resize((w // 8, h // 8), resample=Image.NEAREST) pixelated = small.resize((w, h), resample=Image.NEAREST) img.paste(pixelated, (x0, y0)) img.save("page_masked.png")这种方式生成的就是真正的“马赛克”视觉:敏感区域变成模糊的色块网格,而不是一整块黑色。缺点是需要先渲染再合成,比矢量遮盖慢一些。大批量扫描件处理时,建议用多进程并行,每个进程打开独立的PDF文档,避免线程间资源竞争。
4. 打包成绿色版工具:过程与细节
4.1 PyInstaller打包
脚本写好后,我用PyInstaller打包成单文件exe,这样目标电脑不需要装Python,直接就能运行。
pip install pyinstaller pyinstaller -F -w -n pdf_mosaic main.py参数说明:
-F:打包成单文件。-w:运行时不显示黑色命令行窗口。-n:指定生成exe的名称。
这个版本适合“双击后弹窗选择路径”的交互方式。如果后续想要更复杂的命令行操作,去掉-w参数就行。
打包完会在dist目录下生成一个pdf_mosaic.exe,这个exe就是绿色版工具的核心。
4.2 批处理封装的用法
绿色版的简洁用法,我用一个run.bat来封装:
@echo off cd /d "%~dp0" pdf_mosaic.exe --config config.json pause这样双击bat文件,它就会读取同目录下的config.json,处理./input文件夹里的PDF,输出到./output。
如果不方便改配置文件,也支持命令行直接传参:
pdf_mosaic.exe --input "D:\资料" --output "D:\资料_已脱敏"整个发布目录结构大致是:
pdf_mosaic/ ├── pdf_mosaic.exe ├── config.json ├── run.bat └── README.txt这个文件夹整体拷给别人,或者拷到别的电脑,都可以直接使用。
4.3 关于绿色版体积的一点经验
PyInstaller打的单文件exe因为内置了Python解释器,体积通常在10到20MB之间。有些朋友嫌大,可以尝试用UPX压缩,能压掉30%左右,但个别杀毒软件对UPX压缩过的exe会误报,需要斟酌。
另外,PyInstaller产物有时会被Windows SmartScreen或者杀毒软件拦截,所以我会把源码和打包命令写进README,放在release目录里。万一哪天exe被误删了,随时可以重新打包,不至于断档。
5. 实操中遇到的坑与完整排查链路
这一章是重点中的重点。我把自己踩过的坑按“现象—排查—根因—解决”的方式写下来,照着这个思路排雷,比直接看结论有用得多。
5.1 最经典的坑:遮盖后文字还能复制
现象:用draw_rect处理完,肉眼看上去黑条盖住了文字。但用Adobe Acrobat打开,全选、粘贴,居然还能把遮盖区域下面的文字复制出来。
排查链路:
- 先怀疑是预览器的缓存问题,把文件拷到另一台电脑、换另一个阅读器打开,问题依旧。
- 用
fitz重新打开文件,执行page.get_text(),果然能看到被“遮住”的文本。说明文字对象根本没有被删除。 - 打开PDF原始对象结构,发现
draw_rect只是往页面里追加了一个矩形路径对象,和原本的文字层是平行的,两者没有归属关系。 - 改用
apply_redactions()后,再提取文本,干干净净,验证通过。
这个坑是所有做“PDF马赛克”的项目都绕不开的。原因在于“看起来遮住了”和“实际上被抹除了”是两码事。只有执行了redaction,才能把选中区域的内容彻底移除。
5.2 元数据也会出卖你
现象:处理完的PDF发送出去之后,对方打开文档属性,里面居然还有公司名字和创建人信息。
排查链路:
- 右键PDF → 属性 → 详细信息,看到“作者”一栏有公司中文名。
- 用解析工具读文档信息,确认这些是系统元数据,不是正文内容。
- 回看代码,发现只处理了页面内容,没有清理
doc.set_metadata()。 - 在保存前加一行:
doc.set_metadata({"author": "", "creator": "", "producer": "", "title": ""})这类数据不处理,谈不上彻底脱敏。PDF元数据里会记录文档创建者、创建时间、处理软件版本,这些都可能反推溯源。
5.3 坐标单位不对,遮罩全部错位
现象:一套坐标在某些文件上正常,换到另一批文件,遮盖区域全部偏到左上角或者右下角。
排查链路:
- 打开PDF页面属性,发现页面尺寸不一致。有的是A4,有的是Letter,还有竖版横版混排。
- 排查代码,发现矩形坐标是写死的point值,跟页面尺寸强相关。
- 改用相对坐标:把坐标定义为页面宽高的百分比,再做换算。
例如,想遮盖水平方向中间区域、垂直方向20%到30%的区块:
w = page.rect.width h = page.rect.height rect = fitz.Rect(w * 0.25, h * 0.20, w * 0.75, h * 0.30)后续所有坐标都从“百分比”换算成“绝对point”,再传给redaction,这样即使页面尺寸不同,也能保持相对位置正确。
5.4 扫描件里“删了文字层,图片里还露着”
现象:一个扫描件PDF,正文是图片,额外附带OCR文字层。按文本层把“手机号”匹配出来删了,结果用看图软件打开扫描原图,手机号还在图片像素里。
排查链路:
- 意识到扫描PDF本质是“图片+OCR文字层”两层结构,删除文本层只是去掉了可搜索的字符,图像像素没有动。
- 对这类文件,要么用redact_annot处理整个图片对象区域(连图带字一起抹掉),要么渲染图片后做马赛克/像素化再贴回去。
- 最终选择“按区域渲染-像素化-替换图片”的方式,保留整张扫描图的真实感,只把敏感区域做模糊。
这种处理方式适合“外观上不希望看起来像删过内容”的场合。如果只要信息不出现、不太在意外观,直接用黑色矩形盖住然后redact掉图片区域也可以。
5.5 加密和权限问题
现象:某份PDF是加密的,脚本打开直接报错:File has password。
排查链路:
- 先确认这份文档是不是自己有权处理的文件,密码是否已知。
- 如果能输入密码,用
fitz.open(路径, password="xxxx")打开。 - 打开后先另存为一个临时文件,再走正常批量处理流程。
- 遇到权限限制但知道密码的情况,另存为新文件通常就能解除编辑限制。
这里需要明确说明:这套工具解决的是“处理自己手头有权处理的文件”的场景,不是用来破解他人文档的。遇到权限锁死、无法获取密码的文件,正确做法是先联系文档所有者确认授权,而不是想办法绕过保护。
5.6 文件体积异常增大或缩小
如果处理后PDF体积比原文件大很多,通常是冗余对象没清干净,保存时加上garbage=4, deflate=True可以大幅回收。如果体积小到异常,可能是图片被过度压缩或者内容被误删,需要检查是否有关键页面丢失。
实测下来,正常处理一批文本型PDF后,体积通常会比原文件略小,因为被删除的隐藏对象都被清掉了。如果个别文件体积不降反增,优先怀疑这个文件内部有大量图片或者复杂字体,属正常现象,不用太紧张。
6. 验证一遍才算完成:脱敏效果自查方法
6.1 文本层提取验证
处理完一批文件后,我不会直接打包送出去,会先跑一个验证脚本:提取所有输出PDF的文本,搜索敏感关键词,确认完全搜不到。
import fitz doc = fitz.open(output_path) full_text = "".join(page.get_text() for page in doc) for keyword in ["身份证号", "手机号", "1371234"]: if keyword in full_text: print(f"警告:{keyword} 仍存在")这一步非常关键。如果搜到了,说明redaction没生效,或者源文件本身不是文本层,需要回到上一步重新处理。
6.2 图片层抽查
如果文件里有扫描图片,文本提取查不到不代表图片像素里没有。可以用pdfimages工具把PDF里所有图片导出,对重点页面做肉眼抽查。这个方法不需要全部检查,挑几份关键文件、几页敏感页面看一下即可,但这一步不能省。
6.3 元数据检查
用PyMuPDF读取处理后的PDF元数据,确认作者、创建人、公司名都被清空,避免前面提到的二次泄露。
meta = fitz.open(output_path).metadata print(meta)正常输出应该是{"format": "PDF", "title": "", "author": "", ...}这类空白值。
6.4 生成处理记录方便追溯
比较实用的一个习惯是让脚本输出一个CSV日志,记录每份文件处理了哪几页、每个区域用了哪个规则,这样后续定位漏处理或者误处理都会非常方便。对正规项目来说,这也是一份可提交的处理记录。
我用一个列表收集每次处理的结果,最后统一写CSV:
import csv with open("process_log.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["文件名", "页数", "执行动作", "状态"]) writer.writerows(log_entries)整体流程跑下来,从选型、写脚本、打包绿色版到最终验证,其实就一个原则:把“人工逐份处理”变成“规则定义+脚本自动执行”。批量只是个动作,真正的核心是规则要对、流程要稳、验证要严。
我现在的习惯是处理任何文件之前,先把原始PDF备份到一个单独的文件夹,原始文件永远不动,输出目录单独命名。万一遮罩区域定错了,或者源文件本身有多个版本,也不会因为一次误操作把原始数据弄丢。脱敏这个动作,宁可保守,也不要激进。
如果你需要的只是一个能跑的绿色版小工具,照着上面的代码打包就能直接用。如果后续有更复杂的自动定位、OCR识别、批量像素化需求,把redaction和配置文件里的规则字段继续扩展就行。希望这篇记录能帮你少踩几个坑。