简介:面向被密码锁定的 PDF 文档而准备的密码清除与解锁工具,适用于需要恢复已授权文件编辑、打印或内容提取权限的办公人群,也可作为日常文档处理时的应急辅助方案。压缩包共 10 个文件,约 556KB,体量轻便;其中包含可执行主程序、运行所需动态库与配置数据,并配有帮助网页、说明文本及界面皮肤和引导图示,兼顾功能运行与上手查阅。已有 254 人学习下载,说明其在同类小工具中具备一定实用参考价值。读者可获得一套可直接解压使用的 PDF 密码处理工具,借助随附说明了解基本用法与常见问题,快速应对因密码限制导致的文档无法打开或权限受限场景,减少格式转换与重复查找工具的时间成本。
1. 从一份打印键变灰的 PDF 说起:清除密码前先看锁在哪
上个月帮同事整理一批归档材料,PDF 双击能打开,打印按钮却是灰的,选中文字复制出来是一串乱码。搜 pdf 清除 密码 的人,十有八九卡在这一步:不知道该拆哪把锁,于是随手找在线站点上传,把还没脱敏的合同传到了别人的服务器上。这份资源是一套本地跑的清除工具包,用脚本和常见命令行工具组合,覆盖四件事:属主密码(权限锁)移除、已知打开口令后的解密重存、目录级批处理、结果校验。它不联网、不上传、可批量,适合经常和归档、合同、报表打交道的运维、行政支持和做数据清洗的开发者。要找「无中生有打开陌生文件」的办法,这篇可以直接关掉,边界我会在第 5 章讲透。
2. 属主密码与用户密码:/Encrypt 字典里的两把锁怎么分
2.1 两把锁在文件结构里的位置不一样
PDF 的加密信息不在正文里,而在文件尾部的 trailer 指向一个/Encrypt字典。这个字典里有两个关键条目:/O(Owner,属主)和/U(User,用户)。前者管的是「你有没有权限改这个文件、能不能打印、能不能复制」,后者管的是「你能不能打开看」。很多工具把两件事混为一谈,结果就是明明去掉了打印限制,打开还是要口令。
权限位存在/P这个整数里,按位编码打印、复制、注释、表单填写、页面提取、辅助功能等开关,常看到一个负数——那是按位取反后的有符号表示,不是错误。算法版本由/V和/R决定,密钥长度在/Length里。常见对应关系大致是这样:
| R 值 | 典型算法 | 密钥长度 | 常见出没场景 |
|---|---|---|---|
| 2 | RC4 | 40 bit | 早期内部系统导出的报表 |
| 3 | RC4 | 128 bit | 老版本办公软件默认导出 |
| 4 | RC4 或 AES-128 | 128 bit | 目前存量最多的一档 |
| 5 | AES-256 | 256 bit | 新版规范,密钥派生慢 |
| 6 | AES-256 | 256 bit | 与 R5 同代,口令校验方式有调整 |
看清这张表,后面的选型就有依据了:R2 到 R4 里只设了属主密码的文件,绝大多数工具能直接重写;R5/R6 且设了打开口令的,只能老老实实给口令,没有捷径。
2.2 一条命令判类型,不要凭感觉选方案
我一般不会上来就跑解密,而是先探测。qpdf 的--show-encryption是这件工具里最容易被低估的参数,它只读不写,输出足够判断下一步:
# 只读探测,不产生任何输出文件 qpdf --show-encryption ./in/contract_2023.pdf只设权限锁的文件,输出大致是这样:
R = 4 P = -3904 User password = <empty> Supplied password = <empty>User password = <empty>是关键信号:打开不需要口令,--decrypt时不传任何密码就能过。如果这里显示非空,说明文件设了打开口令,--decrypt之前必须把口令带上,否则 qpdf 会直接退出并返回错误码 2。另外还有一类文件输出File is not encrypted,那是干净的,根本不用处理,脚本里应该直接跳过。
2.3 工具选型:为什么我把在线站点从流程里删掉了
在线清除站点的体验确实好:拖进去、点一下、下载。但它有三个绕不过去的问题。第一是材料出域,合同、发票、体检报告这类文件一旦上传,你就失去了对它的控制,这与合规要求直接冲突。第二是不可批量,几十上百个文件靠手点,时间成本远超装一次 qpdf。第三是结果不可复现,同一份文件不同站点处理出来的页数、书签、表单可能都不一样,出了问题没法回滚。
本地方案我一般这么搭配:qpdf 打底,负责单文件的解密与结构重写,稳定、参数少、退出码明确;pikepdf 负责脚本化,它是 Python 绑定,能遍历目录、捕获异常、统计页数,把批处理串起来;pdfcpu 作为备用,遇到 qpdf 处理异常的个别文件再切过去交叉验证。三者都不需要联网,也都不依赖图形界面,塞进定时任务或者归档脚本里很自然。
需要提醒的是,工具再多也只解决「有口令或有权限锁」的情况。设了打开口令又忘了,那就不是工具问题,是密码学问题——这条线在第 5 章会单独讲。
3. 跑通清除流程:qpdf 与 pikepdf 的单文件、批量四步
3.1 环境准备与依赖安装
先把依赖装齐。qpdf 走系统包管理器,pikepdf 走 pip,两个都装上之后,命令行和脚本两条路就都通了:
# macOS brew install qpdf poppler # Debian / Ubuntu sudo apt install qpdf poppler-utils # Python 侧,pikepdf 自带 qpdf 的编译产物,一般不用额外配置 pip install pikepdfpoppler-utils 是给后面的pdftotext校验用的,不装也能解密,但少了最直接的一条验证手段。装完跑一次qpdf --version确认路径没问题,Windows 下如果提示找不到命令,多半是没加进 PATH,用绝对路径或者改用python -m pikepdf都行。
3.2 单文件:属主密码用一条命令清掉
权限锁是最简单的一类。核心就一个参数--decrypt,它的含义是「以解密方式重写输出文件」,不是去猜密码,而是把已经能打开的内容重新写成一份不带加密字典的 PDF:
# 仅权限锁:不需要提供任何口令 qpdf --decrypt ./in/contract_2023.pdf ./out/contract_2023.pdf # 已知打开口令:把口令一起传进去,同样走 --decrypt qpdf --password='这里填你的口令' --decrypt ./in/report.pdf ./out/report.pdf输入和输出必须是两个不同路径,qpdf 会先读后写,写之前会把输出文件截断,路径相同的话原始文件就没了。执行成功返回 0 且无输出,失败会打印到 stderr 并返回非 0,脚本里判断$?就够了。解密后的文件是一棵全新的对象树,trailer 里不再写/Encrypt,权限位随之消失,打印和复制自然就放开了。
3.3 批量:pikepdf 遍历目录并分流处理
单文件靠命令行,目录级就得靠脚本。下面这段是我常用的骨架,重点是每个文件独立 try,一个失败不影响后面的,同时把失败原因记下来方便二次处理:
from pathlib import Path import pikepdf SRC = Path("./in") DST = Path("./out") DST.mkdir(exist_ok=True) def unlock_one(src: Path, dst: Path, password: str = "") -> str: """处理单个文件,返回 ok / need_password / failed 三类结果。""" try: # 空口令能打开,说明只设了权限锁;打不开会抛 PasswordError pdf = pikepdf.open(src, password=password) except pikepdf.PasswordError: return "need_password" except Exception as e: return f"failed: {type(e).__name__}" try: # encryption=False 是关键:显式声明不写加密字典,别依赖默认值 pdf.save(dst, encryption=False) pages = len(pdf.pages) pdf.close() return f"ok pages={pages}" except Exception as e: pdf.close() return f"failed: {type(e).__name__}: {e}" for f in sorted(SRC.rglob("*.pdf")): target = DST / f.relative_to(SRC) target.parent.mkdir(parents=True, exist_ok=True) print(f.relative_to(SRC), unlock_one(f, target))逻辑上有三处值得说清。pikepdf.open的password默认空串,对只设权限锁的文件空串就能过,所以同一个函数既能处理权限锁也能处理已知口令的文件,不需要分支。pdf.save(dst, encryption=False)这一步才是真正的清除动作——很多人打开之后直接save,结果权限位原样带了过去,白忙一场。len(pdf.pages)顺手把页数记下来,和源文件对比就是最廉价的完整性检查。
口令不要硬编码在脚本里。实际用法一般是python unlock.py "口令",从sys.argv读,或者从环境变量读,避免脚本进版本库时把口令一起带进去。
3.4 三条校验:加密标记、文本层、页数
处理完不校验,等于没做。我固定跑三条,都很短:
# 校验一:加密字典是否还在,期望看到 File is not encrypted qpdf --show-encryption ./out/contract_2023.pdf # 校验二:文本层能不能提取出来(扫描件这里会是空的,属正常) pdftotext -f 1 -l 1 ./out/contract_2023.pdf - | head -c 200 # 校验三:页数是否和源文件一致,防止重写过程丢页 qpdf --show-npages ./in/contract_2023.pdf qpdf --show-npages ./out/contract_2023.pdf第一条是硬指标,输出不是File is not encrypted就说明解密没生效,可能是口令没传对,也可能是文件被别的进程占用导致写了一半。第二条区分类型:文字型 PDF 应该能提取出可读中文或英文,扫描件提取为空是正常的,别误判成失败。第三条最容易被忽略,但从经验看,重写过程丢页的概率不高却真实存在,尤其是文件本身结构有损坏的时候。
4. 参数取舍:大文件、扫描件与元数据保留的四个决策点
4.1 对象流与线性化对体积和耗时的影响
qpdf 重写时会重新组织对象流(object stream)和交叉引用表,这直接影响输出体积。默认行为是尽量保留原有结构,一般不用动。但有几个参数容易被乱用,值得单独列出来:
| 参数 | 作用 | 我的建议 |
|---|---|---|
--object-streams=preserve | 保持原有对象流结构 | 默认值,绝大多数场景用它 |
--object-streams=disable | 拆开对象流,结构可人眼阅读 | 只在排查结构问题时用,体积会涨 |
--stream-data=uncompress | 解压内容流 | 不要用,8MB 变几十 MB 很常见 |
--linearize | 生成线性化文件,首屏加载快 | 对外分发可选,耗时增加一到两成 |
--qdf | 输出为可读的 QDF 格式 | 纯调试用途,别进正式流程 |
耗时方面,文字型 PDF 的瓶颈在解压重压数据流和磁盘 IO,一份 300MB 左右的文件单次重写大概几秒到十几秒,量级和机器磁盘关系很大。批量跑的时候我一般限制并发到 4 以内,机械盘上再降到 2,否则 IO 打满反而更慢。
4.2 文字型与扫描件:校验方式必须分开
这两类文件的失败表现完全相反。文字型 PDF 解密失败时,pdftotext提取为空或者乱码;扫描件本来就是图片,提取永远为空。如果脚本统一用「文本非空」当成功判据,扫描件会被全量误判成失败。
我的做法是先判类型再校验:用pdfimages -list看图片对象占比,或者直接用 pikepdf 统计每页的/XObject里/Subtype为/Image的数量。图片占绝对多数的,校验就退回到页数加文件大小两个指标;文字型的才上文本提取。扫描件真正需要的是 OCR,那和解密是两件独立的事,别指望解密工具顺手把文字层补上。
4.3 元数据、书签、表单与数字签名的保留取舍
解密重写会重建对象树,大部分结构会原样保留,但有几类东西会变。书签和页面标签一般没问题,AcroForm 表单字段通常也能保留,但如果你之前填过表单,重写后建议人工抽检一两个字段。XMP 元数据和文档信息字典里的作者、创建时间会跟着走,不过部分工具会顺手把Producer改掉,如果归档有元数据要求,处理前先导出原始信息比对。
数字签名是另一回事。带签名的 PDF 一旦被重写,签名必然失效,这是密码学保证,不是工具缺陷。碰到带签名的合同,正确做法是联系签署方重新出具,而不是硬解。这条边界我在流程里写死了:检测到/AcroForm里存在/SigFlags且大于 0 的,直接跳过并进人工队列。
5. 避坑记录:五类翻车现场的现象、原因与解法
5.1 命令提示成功,文件却打不开或变成 0 字节
现象:批量跑完,日志全是 ok,抽查发现部分输出文件 0 字节或者打不开。
原因:输出路径和输入路径撞了。qpdf 和多数重写型工具都是先截断输出再写入,路径相同时源文件先被清空,后面读到的就是空内容。另一种情况是脚本里dst用了相对路径,工作目录切换后写到了意料之外的位置。
解决:脚本入口处做一次Path(src).resolve() == Path(dst).resolve()的断言,相等直接抛错退出。输出目录用绝对路径拼,别依赖当前工作目录。已经出问题的文件只能从原始备份重来,所以处理前先复制一份in.bak是值得的。
5.2 解密后体积暴涨,归档系统拒收
现象:8MB 的文件处理完变成 40MB,上传归档系统直接被拦。
原因:命令行里顺手加了--stream-data=uncompress或者--qdf。这两个参数会把压缩的内容流全部展开,用于调试很爽,用于正式流程就是灾难。
解决:正式流程只保留--decrypt,别加任何 stream 相关参数。已经生成的大文件重新用默认参数跑一遍即可,源文件没坏就不影响。我在批处理脚本里对这两个参数做了黑名单拦截,防止有人复制粘贴调试命令进生产。
5.3 批量跑一半崩掉,后面的文件全没处理
现象:一百个文件处理到第 37 个脚本抛异常退出,剩下 63 个原封不动。
原因:pikepdf.open抛的PasswordError没被捕获,或者把所有文件放在一个 try 里。前者在有打开口令的文件上必现,后者一个坏文件拖垮整批。
解决:每个文件独立 try,异常一律转成字符串记进结果表,主循环不中断。跑完输出一份失败清单,人工按need_password和failed分两类处理:前者补口令重跑,后者单独看具体异常类型。
5.4 密码清掉了,复制文字还是乱码
现象:--show-encryption已经显示未加密,打印也放开了,但选中文字复制出来仍是乱码。
原因:文档根本没有文本层,是扫描件;或者 PDF 内嵌字体缺少ToUnicode映射表,字符编码对不上。这两种都跟加密无关,解密工具帮不上忙。
解决:先pdftotext验证,提取为空就走 OCR 流程,提取出乱码则考虑重新生成 PDF 或做字体替换。别在这类文件上反复折腾解密参数,那是拿错工具干错活。
5.5 忘了打开口令,跑了一晚上没结果
现象:一份多年前自己加密的合同,只记得大概,想着工具能「破解」,结果挂机一整晚毫无进展。
原因:R5/R6 的 AES-256 口令派生本身就是为了抗暴力尝试设计的,单机每秒能试的次数极其有限,弱口令之外的组合基本没戏。所谓「破解工具」在这种场景下能做的,只是比你手动快一点地试常见组合。
解决:认命,找回原始口令,或者联系文件出具方重发。真正该做的是事后补救——从那以后我给任何 PDF 加密都会把口令记进密码管理器,文件名里不带任何口令线索但留一个可检索的标签,省得下次再翻车。
6. 接进归档流水线:先探测再分流的批处理写法
前面几步都是单点技能,真正省时间的是把它们串成一个分流器:进来一个目录,先判断每个文件属于哪一类,再决定走哪条路。无脑对所有文件跑--decrypt的问题在于,干净文件被白白重写一遍,体积和哈希都变了,归档系统那边的去重逻辑会直接失效。
分流的判据其实就三个状态:干净、只有权限锁、有打开口令。用 pikepdf 判断比解析命令行输出更省事:
import pikepdf from pathlib import Path def classify(path: Path, password: str = "") -> str: """clean / owner_only / user_pw / locked / broken""" try: pdf = pikepdf.open(path, password=password) except pikepdf.PasswordError: return "locked" # 有打开口令且当前口令不对,进人工队列 except Exception: return "broken" # 结构损坏,先走修复再看 try: if not pdf.is_encrypted: return "clean" # 没加密,直接跳过,别重写 # 空口令就能打开且文件仍标记为加密 -> 只设了权限锁 return "owner_only" if password == "" else "user_pw" finally: pdf.close()这段的判断顺序有讲究:先尝试打开,打不开才是locked,这一步本身就把「口令错」和「文件坏」区分开了。打开之后再看is_encrypted,能区分干净文件和权限锁文件——有加密标记但空口令可开,就是典型的属主密码场景。分流结果直接映射到动作:clean跳过并原样复制,owner_only走 qpdf 重写,user_pw带着口令走 pikepdf 重存,locked和broken落到人工清单。
跑批的时候还有两个细节值得固化。一是输出目录结构要和源目录镜像,用relative_to拼,别把上百个同名文件全倒进一个平铺目录。二是每次跑完生成一份 CSV,字段至少包含源路径、分类、页数、输出大小、结果,这份表现在是我排查问题的第一手材料——哪一批文件出了异常,翻表比翻日志快得多。
我自己踩得最深的一次,是把解密步骤直接塞进了一个每夜跑的归档任务里,没做分类,结果连续三天把整批干净文件的修改时间全刷了一遍,增量备份系统第二天开始全量重传。从那以后我每次改这类流水线,都强制先拿二十个样本文件跑一遍 dry-run,把分类结果和动作列表打出来对一遍,确认没有误伤再放开全量。慢这十分钟,比事后回滚三天的备份划算。希望帮到你。
本文还有配套的精品资源,点击获取