ARTICLE · 1206259
你手头有几十上百个 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_nametry: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: # decryptif 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 文件")returnstats = {"ok": 0, "skip": 0, "fail": 0}failed_files = []for pdf in pdfs:status, msg = process_one(pdf, mode, password, out_dir)stats[status] += 1icon = {"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): pfor 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 和密码换成你自己的,就是一套能用的批量加解密工具。















































































