2026-10-05 · 喵窝资源铺
两个高频场景:① 一批 PDF 发给客户前要加打开密码(最好顺带禁止复制修改);② 拿到一批已知密码的加密 PDF要批量解密成明文(比如拿到自家公司的归档件)。Adobe Acrobat 收费、WPS 要一个个另存。这篇给一个 pypdf 的批量脚本:encrypt 加密码+限权限,decrypt 批量去密;坏文件不崩,全部记进报告。文中代码实跑验证,真实输出贴在下面。
| 需求 | 脚本的处理 |
|---|---|
| 加密+限权限 | encrypt 目录 输出 --user w123 --owner adm456:打开要密码,且只允许打印、禁止修改/复制/批注 |
| 仅限权限 | 只给 --owner 不给 --user:打开不要密码,但别人动不了内容(发公开件常用) |
| 批量解密 | decrypt 目录 输出 --password w123:输出去密后的明文副本,输出的 PDF is_encrypted=False |
| 烂文件不崩 | 损坏 PDF / 密码错的文件自动跳过并写进报告 |
坑1:网上老教程写permissions_flags(复数),pypdf 6.x 直接 TypeError。实测inspect.signature(PdfWriter.encrypt),现在的签名是encrypt(self, user_password, owner_password=None, ..., permissions_flag=..., algorithm=None)——参数是单数permissions_flag,而且user_password变成了必填位置参数。只想限权限不加密打开的,要传空串user_password=""。搜到的旧代码一个个抄都报unexpected keyword argument 'permissions_flags',这个坑版本相关,认准pypdf --version再抄。
坑2:验证加密别只看文件打不打开,要用decrypt()的返回值。PdfReader.decrypt(密码)返回 0 是密码错误、1 是成功。实测错误密码返回0,正确密码返回1且len(r.pages)正常。另外「只限权限、打开不要密码」的 PDF,reader.is_encrypted也是 True,解密脚本传空密码即可解开——这类文件在企业归档里非常常见。
# -*- coding: utf-8 -*-
"""批量 PDF 加密/解密:整目录处理,坏文件不崩。
用法:
python3 pdf_encrypt.py encrypt <输入目录> <输出目录> --user w123 --owner adm456
python3 pdf_encrypt.py encrypt <输入目录> <输出目录> --owner adm456
python3 pdf_encrypt.py decrypt <输入目录> <输出目录> --password w123
python3 pdf_encrypt.py decrypt <输入目录> <输出目录>
只依赖 pypdf(6.x 验证通过)。
"""
import os, sys
from pypdf import PdfReader, PdfWriter
from pypdf.constants import UserAccessPermissions as UAP
def walk_pdfs(d):
for root, _, files in os.walk(d):
for f in sorted(files):
if f.lower().endswith(".pdf"):
yield os.path.join(root, f)
def out_path(outdir, in_root, p):
dst = os.path.join(outdir, os.path.relpath(p, in_root))
os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
return dst
def parse_opts(args):
opts = {}
for a in args:
if a.startswith("--"):
k, _, v = a.partition("=")
if v == "":
i = args.index(a)
if i + 1 < len(args) and not args[i+1].startswith("--"):
v = args[i+1]
opts[k] = v
return opts
def main():
args = [x for x in sys.argv[1:]]
if len(args) < 3 or args[0] not in ("encrypt", "decrypt"):
print(__doc__); sys.exit(2)
mode, indir, outdir = args[0], args[1], args[2]
opts = parse_opts(args[3:])
user_pw = opts.get("--user")
owner_pw = opts.get("--owner")
password = opts.get("--password")
report = []
for p in walk_pdfs(indir):
name = os.path.basename(p)
try:
r = PdfReader(p)
if mode == "encrypt":
w = PdfWriter(clone_from=r)
if user_pw or owner_pw:
perm = UAP.PRINT
w.encrypt(
user_password=user_pw or "",
owner_password=owner_pw or user_pw or "",
permissions_flag=perm,
)
else:
if r.is_encrypted:
ok = r.decrypt(password or "")
if ok == 0:
raise ValueError("密码错误或未提供,解不开")
w = PdfWriter(clone_from=r)
dst = out_path(outdir, indir, p)
with open(dst, "wb") as f:
w.write(f)
report.append((name, "OK", dst))
except Exception as e:
report.append((name, f"[跳过] {e}", ""))
for name, st, dst in report:
print(f"{st}\t{name}" + (f"\t{dst}" if dst else ""))
bad = sum(1 for _, s, _ in report if s != "OK")
print(f"\n处理 {len(report)} 个,成功 {len(report)-bad},跳过 {bad}")
if __name__ == "__main__":
main()
造了 4 个测试文件:2 页的报告、1 页的便签、截断损坏的假 PDF、还有一个「只限权限、打开不要密码」的 owner_locked.pdf(模拟企业归档常见形态)。
加密 encrypt testdata_enc out_enc --user w123 --owner adm456:
EOF marker not found [跳过] Stream has ended unexpectedly broken.pdf OK memo1.pdf out_enc/memo1.pdf OK owner_locked.pdf out_enc/owner_locked.pdf OK report2.pdf out_enc/report2.pdf 处理 4 个,成功 3,跳过 1
复核加密结果:is_encrypted: True;错误密码 decrypt('wrong') 返回 0;正确密码 decrypt('w123') 返回 1、2 页正常读出。
解密 decrypt out_enc out_dec --password w123:
OK memo1.pdf out_dec/memo1.pdf OK owner_locked.pdf out_dec/owner_locked.pdf OK report2.pdf out_dec/report2.pdf 处理 3 个,成功 3,跳过 0
解密产物复核:is_encrypted: False,页数正常——拿到的就是干净明文副本。对没给密码的目录跑 decrypt,也能处理「只限权限」那类文件(传空密码解开)。