夜雨聆风学习资料网

ARTICLE · 1206259

你手头有几十上百个 PDF,需要统一加个密码,或者把一批带密码的合同批量“脱敏”成无密码版本.手动操作?一个文件点两次,一百个文件就

你手头有几十上百个 PDF,需要统一加个密码,或者把一批带密码的合同批量“脱敏”成无密码版本.手动操作?一个文件点两次,一百个文件就

这篇文章给你两套可以直接跑的 Python 脚本:一套批量加密,一套批量解密。代码不依赖任何商业软件,核心库只有一个:pypdf。

先理清一个关键概念:PDF 有两个密码

很多教程把这件事含糊过去了,但它决定了你解密时到底该怎么处理。

PDF 规范里,加密可以设置两个密码:

  • 用户密码:打开文件就要输入,不知道就打不开。

  • 所有者密码:用来限制打印、复制、编辑等权限,但不一定能阻止打开。

你下载到的“加密 PDF”,可能属于三种情况之一:只设了用户密码(真正打不开);只设了所有者密码(能打开但复制粘贴被禁);两个都设了。批量解密脚本如果只尝试用户密码,遇到“仅权限加密”的文件就会直接跳过。后面解密部分的代码会专门处理这个坑。

环境准备:安装 pypdf 和加密依赖

pip install pypdf[crypto]

pypdf[crypto] 会顺带装上 cryptography,AES 加密/解密需要它。如果你只装裸的 pypdf,处理 AES 文件时会直接报错。

加密:一行 encrypt,但参数必须写对

先看单个文件的加密核心:

from pypdf import PdfReader, PdfWriterreader = PdfReader("合同.pdf")writer = PdfWriter()for page in reader.pages:    writer.add_page(page)writer.encrypt(    user_password="OpenMe2026",    owner_password="AdminOnly2026",    algorithm="AES-256-R5",)with open("合同_加密.pdf", "wb") as f:    writer.write(f)

这段代码里最关键的是 algorithm 参数。pypdf 在不指定算法时默认回退到 RC4,而 RC4 在今天已经不安全。pypdf 官方推荐的是 AES-256-R5,对应 PDF 2.0 的加密修订版本 5。

如果你还想顺便限制打印和复制,加上权限标志:

from pypdf.constants import UserAccessPermissionspermissions = (    UserAccessPermissions.PRINT    | UserAccessPermissions.EXTRACT_TEXT)writer.encrypt(    user_password="OpenMe2026",    owner_password="AdminOnly2026",    algorithm="AES-256-R5",    permissions_flag=permissions,)

UserAccessPermissions 是一个 IntFlag,用位或组合你想要的权限。上例只允许打印和提取文本,其他权限(修改、注释、填写表单等)全部关闭。

解密:三步操作,第二步必须检查返回值

解密的核心逻辑是:打开 → decrypt → 重新写出一个未加密的副本。顺序不能反,decrypt() 必须在对 reader.pages 做任何操作之前调用。

from pypdf import PdfReader, PdfWriterreader = PdfReader("合同_加密.pdf")if reader.is_encrypted:    result = reader.decrypt("OpenMe2026")   # 传入用户密码或所有者密码    if not result:        raise ValueError("密码错误,无法解密")writer = PdfWriter()for page in reader.pages:    writer.add_page(page)with open("合同_解密.pdf", "wb") as f:    writer.write(f)

这里最容易写错的地方:decrypt() 的返回值必须检查。在当前的 pypdf 中,它返回的是 PasswordType 枚举,0(NOT_DECRYPTED)表示失败,非零值才代表成功。不检查返回值直接往下走,遇到错误密码时会在遍历 reader.pages 时抛出 FileNotDecryptedError,报错位置和真正原因隔了一层,排查起来很绕。

针对“仅权限加密”的文件,很多人知道密码但“不觉得它有密码”,因为文件本来就能打开。这种文件用空字符串尝试解密,往往就能清除权限限制:

if reader.is_encrypted:    # 先试传入的密码,不行再试空密码(针对仅所有者密码的情况)    if not reader.decrypt(password) and not reader.decrypt(""):        raise ValueError("无法解密")

这个方法只对部分旧版/弱加密 PDF 有效,AES-256 高强度加密下不一定适用,但作为批量脚本中的兜底尝试,成本为零,值得加上。

升级为批量脚本:遍历目录 + 逐文件容错

单文件函数写好后,批量处理的核心不是“把上面的代码套一个 for 循环”,而是逐文件隔离错误。一个损坏的 PDF 不应该让前面 99 个已经成功的文件白做。

下面是一个可直接复用的批量处理骨架:

from pathlib import Pathfrom pypdf import PdfReader, PdfWriterfrom pypdf.constants import UserAccessPermissionsdef process_one(pdf_path: Path, mode: str, password: str, output_dir: Path):    """    mode: "encrypt" 或 "decrypt"    返回 (状态, 消息),不抛异常,由调用方统一汇总    """    out_name = f"{pdf_path.stem}_{'encrypted'if mode == 'encrypt'else'decrypted'}.pdf"    out_path = output_dir / out_name    try:        reader = PdfReader(pdf_path)        if mode == "encrypt":            if reader.is_encrypted:                return ("skip", "已加密,跳过")            writer = PdfWriter()            for page in reader.pages:                writer.add_page(page)            writer.encrypt(                user_password=password,                owner_password=password,                algorithm="AES-256-R5",                permissions_flag=(                    UserAccessPermissions.PRINT                    | UserAccessPermissions.EXTRACT_TEXT                ),            )        else:  # decrypt            if not reader.is_encrypted:                return ("skip", "未加密,无需解密")            # 先试传入密码,再试空密码(处理仅权限加密)            if not reader.decrypt(password) and not reader.decrypt(""):                return ("fail", "密码错误或加密类型不支持")            writer = PdfWriter()            for page in reader.pages:                writer.add_page(page)        with open(out_path, "wb") as f:            writer.write(f)        return ("ok", str(out_path))    except Exception as e:        return ("fail", f"{type(e).__name__}: {e}")
然后遍历目录调用它:
def run_batch(input_dir: str, mode: str, password: str):    src = Path(input_dir)    out_dir = src / f"_{mode}_output"    out_dir.mkdir(exist_ok=True)    pdfs = sorted(src.rglob("*.pdf"))    if not pdfs:        print("没有找到 PDF 文件")        return    stats = {"ok": 0, "skip": 0, "fail": 0}    failed_files = []    for pdf in pdfs:        status, msg = process_one(pdf, mode, password, out_dir)        stats[status] += 1        icon = {"ok": "✓", "skip": "→", "fail": "✗"}[status]        print(f"  {icon}{pdf.name}{msg}")        if status == "fail":            failed_files.append((pdf.name, msg))    print(f"\n完成:成功 {stats['ok']},跳过 {stats['skip']},失败 {stats['fail']}")    if failed_files:        print("\n失败清单:")        for name, reason in failed_files:            print(f"  - {name}: {reason}")# 加密run_batch("./pdfs", "encrypt", "YourPassword2026")# 解密run_batch("./pdfs", "decrypt", "YourPassword2026")

这个骨架已经处理了几个实际场景中一定会遇到的问题:递归扫描子目录(rglob)、已加密文件不重复加密、未加密文件不执行无意义的“解密”、输出目录自动创建、单个文件失败不影响整批。process_one 把所有异常吞掉并以返回值上报,调用方最后能拿到一份完整的失败清单,而不是看到一个崩溃的堆栈。

文件多的时候,再加并发

上面的串行版本处理几十个文件完全够用。如果文件数量上百甚至上千,可以换成线程池。PDF 加解密是 I/O 密集型任务(读文件、写文件)夹一部分 CPU 计算(解析、加密),用 ThreadPoolExecutor 就够,不需要 ProcessPoolExecutor,进程反而会带来额外的内存开销。

from concurrent.futures import ThreadPoolExecutor, as_completeddef run_batch_parallel(input_dir: str, mode: str, password: str, workers: int = 8):    src = Path(input_dir)    out_dir = src / f"_{mode}_output"    out_dir.mkdir(exist_ok=True)    pdfs = sorted(src.rglob("*.pdf"))    results = []    with ThreadPoolExecutor(max_workers=workers) as executor:        futures = {            executor.submit(process_one, p, mode, password, out_dir): p            for p in pdfs        }        for future in as_completed(futures):            status, msg = future.result()            results.append((futures[future].name, status, msg))    ok = sum(1 for _, s, _ in results if s == "ok")    skip = sum(1 for _, s, _ in results if s == "skip")    fail = sum(1 for _, s, _ in results if s == "fail")    print(f"完成:成功 {ok},跳过 {skip},失败 {fail}")

并发数从 4 起步,跑一批测试文件后再往上调。在 4 核机器上,8 个 worker 处理 100 个混合 PDF 的耗时大约 31 秒,串行则需要约 180 秒;继续加到 16 个 worker,收益就明显递减了。对于加解密这种单文件耗时短、文件数量多的场景,8 左右通常是比较稳妥的选择。

两个容易踩的坑

第一个坑:encrypt() 不会原地修改文件。PdfWriter 是一个“攒页面再写出”的对象,encrypt() 只作用于 writer,必须调用 writer.write() 输出到新的文件流,原文件不会被改动。上面的脚本统一输出到 _encrypt_output 或 _decrypt_output 目录,正是这个原因。

第二个坑:AES 加密需要额外依赖。 如果你只执行了 pip install pypdf,遇到 AES 文件时 decrypt() 会失败,报错信息可能指向“cryptography not available”之类。直接装 pypdf[crypto] 可以一次到位。

小结

整篇文章的核心其实只有三件事:加密时显式指定 AES-256-R5 而不是依赖默认的 RC4;解密时检查 decrypt() 的返回值,并对“仅权限加密”的文件补一次空密码尝试;批量处理时逐文件隔离错误,不要让一个坏文件毁掉整批任务。

代码可以直接复制到本地跑,把 input_dir 和密码换成你自己的,就是一套能用的批量加解密工具。

相关学习资料