Python实战:利用pypdf与PyMuPDF解密加密PDF文件的技术分析与实现

发布时间:2026/7/4 11:10:30

Python实战:利用pypdf与PyMuPDF解密加密PDF文件的技术分析与实现 1. 项目概述一次由好奇心驱动的技术探索那天晚上我收到了一封来自前女友的邮件附件是一个加密的PDF文件标题是“给过去的回忆”。邮件正文只有一句话“密码是你最熟悉的数字。” 好奇心像野草一样疯长混杂着一丝说不清道不明的情绪。作为一个技术从业者我的第一反应不是去猜测密码而是看着那个小小的锁形图标脑子里蹦出一个念头能不能用Python把它解开这听起来像电影里的情节但现实中处理加密PDF的需求其实非常普遍。可能是你多年前自己加密的工作文档忘了密码也可能是同事交接时留下的带锁资料又或者就像我遇到的这种情况——一个带着谜题的“时间胶囊”。Python凭借其强大的生态库为我们提供了一把并非万能、但足够锋利的“钥匙”。这次探索与其说是“破解”不如说是一次针对PDF加密机制的技术性分析与尝试。它涉及文件格式解析、密码学原理的浅层应用以及自动化脚本的编写整个过程充满了技术人的浪漫与务实。注意本文所讨论的技术方法仅适用于学习加密原理、恢复自己遗忘密码的文档等合法合规场景。严禁用于侵犯他人隐私或破解未经授权的受保护文件。技术是一把双刃剑请务必在法律和道德框架内使用。2. 核心思路与技术选型解析2.1 为什么选择Python作为工具面对一个加密PDF我们有多种选择在线解密网站存在隐私泄露风险、昂贵的商业软件或者自己动手。我选择Python基于以下几个核心考量生态丰富库成熟稳定Python在文件处理和自动化领域有近乎垄断性的优势。对于PDF操作有PyPDF2、PyMuPDFfitz、pdfplumber等一批久经考验的库。特别是PyPDF2以及其维护分支pypdf它原生支持对加密PDF的密码校验和解密操作API简洁明了。快速原型与迭代解密过程本质上是一个“猜测-尝试”的循环。Python的脚本特性允许我快速编写密码生成逻辑、集成解密函数并在循环中进行尝试。如果密码空间很大我还可以方便地引入多进程multiprocessing库来加速这种灵活性是图形化工具难以比拟的。可控性与透明度使用自己编写的脚本整个解密过程完全透明。我知道数据没有上传到任何第三方服务器也知道每一步在做什么。这对于处理可能包含敏感信息的文件至关重要。我可以控制尝试的频率、密码的生成规则并在任何一步查看中间状态。2.2 PDF加密机制浅析我们面对的是什么在动手之前有必要了解一下PDF加密的基本原理。这决定了我们“破解”的难度上限和方法。PDF标准支持两种主要的加密方式1. 标准加密密码加密 这是最常见的情况。文件使用一个“用户密码”User Password也叫打开密码进行加密。当用PDF阅读器打开时会弹出输入框。加密算法通常是RC4或AES密钥长度可能是40位、128位或256位。关键在于这个用于加密文档内容的对称密钥本身又是通过用户密码结合一个称为“O值”的盐经过一系列哈希如MD5、SHA-256和算法如AES派生出来的。验证密码的过程就是尝试用你输入的密码去派生密钥然后看能否正确解密文件中的某个特定字段权限字段。2. 证书加密 这种方式使用数字证书的公钥来加密文档只有持有对应私钥的用户才能打开。这属于非对称加密没有私钥的情况下仅凭暴力猜测在计算上是不可行的。我们通常遇到的个人加密PDF99%以上都是第一种——基于密码的标准加密。我们的目标就是针对“标准加密”通过脚本自动尝试可能的“用户密码”。这本质上是一种已知明文攻击或暴力/字典攻击。难度完全取决于密码的复杂度和我们掌握的“提示信息”。2.3 方案设计从盲目暴力到定向猜测“密码是你最熟悉的数字。”——这条提示极大地缩小了战场。完全盲目的暴力破解尝试所有字符组合对于超过8位的复杂密码几乎不可能。但有了提示策略就完全不同了。我的方案设计分为几个层次核心密码本生成根据提示“最熟悉的数字”优先生成所有相关的数字组合。例如纪念日年月日、月日、电话号码片段、特殊数字如圆周率片段、学号等、连续或重复数字。混合字典扩充如果纯数字密码本失败考虑将数字与一些常见的弱密码如qwerty、admin、她的名字缩写、我的名字缩写等进行简单组合。有限暴力扩展在前两者失败后对核心数字进行有限度的扩展例如在数字前后添加1-2个常见字母或符号。流程控制与日志脚本需要清晰地记录尝试过的密码、尝试次数、以及最终成功时的密码。要有超时和中断机制避免无限循环。工具链选择如下PDF处理库pypdfPyPDF2的活跃维护分支API更现代。它提供了PdfReader和is_encrypted、decrypt方法。密码生成器结合itertools库用于排列组合和自定义逻辑。并发加速可选如果密码本较大使用concurrent.futures的ThreadPoolExecutor进行多线程尝试因为I/O文件读取解密是主要瓶颈。3. 实战编写Python解密脚本3.1 环境准备与库安装首先确保你的Python环境建议3.7以上已经就绪。创建一个新的项目目录并通过pip安装核心库。# 安装 pypdf推荐它是PyPDF2的继承者 pip install pypdf # 也可以安装 PyMuPDFfitz速度极快功能强大但API略有不同 # pip install PyMuPDF这里我主要使用pypdf因为它API简单完全满足基础解密需求。PyMuPDF性能更强适合处理大型或复杂的PDF但初学使用pypdf更直观。3.2 构建核心密码本根据提示“最熟悉的数字”我开始回忆所有可能的数字组合。我将这些逻辑写成一个密码生成函数。import itertools from datetime import datetime def generate_core_password_list(): 生成基于‘最熟悉数字’的核心密码列表 passwords set() # 使用集合避免重复 # 1. 可能的纪念日假设我们是在2018年5月20日在一起的格式多种多样 anniversary datetime(2018, 5, 20) date_formats [ anniversary.strftime(%Y%m%d), # 20180520 anniversary.strftime(%Y%j), # 2018140 (年份一年中的第几天) anniversary.strftime(%m%d%Y), # 05202018 anniversary.strftime(%m%d), # 0520 anniversary.strftime(%Y%m), # 201805 str(anniversary.year), # 2018 anniversary.strftime(%d%m%Y), # 20052018 ] passwords.update(date_formats) # 2. 纯数字序列生日、电话尾号等假设 # 假设她的生日是9月15日 birthdays [0915, 915, 19950915] # 假设年份1995 passwords.update(birthdays) # 3. 有特殊意义的数字比如第一次见面的教室号、公交线路等 special_numbers [520, 1314, 3344, 99, 100, 123, 123456, 654321] passwords.update(special_numbers) # 4. 数字的简单排列组合长度4-6位 base_digits [0,1,2,3,4,5,6,7,8,9] # 生成4位所有数字组合计算量太大(10^410000)这里只生成来自special_numbers的数字组合 # 例如从520、1314中取数字进行拼接 digit_pool set(.join(special_numbers[:3])) # 取前三个特殊数字的字符集例如{5,2,0,1,3,4} for length in range(4, 7): # 这里谨慎使用全排列依然很大。可以改用迭代生成部分。 # 我们改为生成这些数字的重复序列 for combo in itertools.product(digit_pool, repeatmin(length, 4)): # 限制长度和组合数 pwd .join(combo) if len(pwd) 4: # 只取长度4的 passwords.add(pwd) # 5. 将上述所有数字密码与空字符串、常见后缀前缀组合第一轮扩展 base_list list(passwords) suffixes [, !, , #, 123, abc] prefixes [, a, love, hi] # 这里我们做有限组合避免列表爆炸 expanded set() for pwd in base_list: expanded.add(pwd) # 原密码 for suf in suffixes[:3]: # 只取前三个后缀 expanded.add(pwd suf) for pre in prefixes[:2]: # 只取前两个前缀 expanded.add(pre pwd) # 将扩展后的集合转回列表 password_list list(expanded) print(f核心密码本生成完成共 {len(password_list)} 条密码。) # 打印前20条看看 print(示例密码前20条:, password_list[:20]) return password_list这个函数构建了一个数百到数千量级的初始密码本它基于“数字”这个核心并做了第一层轻度扩展。3.3 实现PDF解密与尝试函数接下来编写用密码尝试解密PDF的核心函数。from pypdf import PdfReader import PyPDF2.errors as pdf_errors def try_decrypt_pdf(pdf_path, password): 尝试用单个密码解密PDF。 返回 (bool, str): (是否成功, 状态信息) try: with open(pdf_path, rb) as file: reader PdfReader(file) # 检查是否加密 if not reader.is_encrypted: return False, 文件未加密 # 尝试解密 if reader.decrypt(password) 1: # decrypt方法返回解密成功的页数如果使用用户密码非0即成功。对于pypdf成功时返回一个非零值如页数。 # 验证解密是否真的成功可以尝试读取一页的文本如果文档有文本 try: _ reader.pages[0].extract_text()[:10] # 尝试提取前10个字符 return True, 解密成功 except Exception as e: # 即使decrypt返回成功也可能权限不足所有者密码。但用户密码能打开文件。 # 对于我们的目的能打开就行。 return True, 解密成功权限可能受限 else: return False, 密码错误 except pdf_errors.PdfReadError as e: # 可能密码错误导致文件无法解析 return False, f密码错误或文件损坏: {e} except Exception as e: return False, f尝试过程中发生未知错误: {e} def batch_decrypt(pdf_path, password_list, max_workers4): 批量尝试密码列表。 使用多线程加速I/O密集型操作。 from concurrent.futures import ThreadPoolExecutor, as_completed import threading found_password None lock threading.Lock() attempted 0 total len(password_list) def attempt(password): nonlocal found_password, attempted if found_password is not None: # 如果已经找到其他任务直接退出 return None success, msg try_decrypt_pdf(pdf_path, password) with lock: attempted 1 if attempted % 100 0: # 每尝试100次打印一次进度 print(f进度: {attempted}/{total} ({(attempted/total)*100:.1f}%)) if success: return password return None print(f开始批量尝试共 {total} 个密码使用 {max_workers} 个线程...) with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_pwd {executor.submit(attempt, pwd): pwd for pwd in password_list} for future in as_completed(future_to_pwd): result future.result() if result is not None: found_password result # 找到后取消所有未完成的任务Executor没有直接取消所有的方法这里我们设置标志让任务函数自己提前返回 print(f\n成功找到密码正在停止其他任务...) # 实际上我们通过 if found_password is not None: 检查让其他任务快速返回 break if found_password: print(f\n 解密成功密码是: {found_password}) return found_password else: print(f\n❌ 密码本中的所有 {total} 个密码均尝试失败。) return None3.4 主程序与执行流程将以上部分组合起来并添加一些友好的交互和日志。import os import time def main(): pdf_file_path 给过去的回忆.pdf # 你的加密PDF文件路径 if not os.path.exists(pdf_file_path): print(f错误文件 {pdf_file_path} 不存在。) return print(*50) print(加密PDF解密尝试工具) print(*50) print(f目标文件: {pdf_file_path}) print(基于提示生成核心密码本...) start_time time.time() # 1. 生成密码本 password_list generate_core_password_list() # 2. 执行批量尝试 found_pwd batch_decrypt(pdf_file_path, password_list, max_workers6) # 3. 输出结果 end_time time.time() elapsed end_time - start_time print(f\n总耗时: {elapsed:.2f} 秒) if found_pwd: # 解密并保存一个新文件 try: with open(pdf_file_path, rb) as enc_file: reader PdfReader(enc_file) reader.decrypt(found_pwd) # 写入解密后的内容 from pypdf import PdfWriter writer PdfWriter() for page in reader.pages: writer.add_page(page) output_path pdf_file_path.replace(.pdf, _decrypted.pdf) with open(output_path, wb) as dec_file: writer.write(dec_file) print(f已保存解密后的文件至: {output_path}) except Exception as e: print(f保存解密文件时出错: {e}) else: print(未能解密文件。建议) print(1. 检查密码提示重新思考‘最熟悉的数字’是否还有其他可能。) print(2. 扩大密码本范围例如加入更多日期格式、电话号码等。) print(3. 考虑使用更强大的字典文件进行尝试注意合法性。) if __name__ __main__: main()4. 关键环节的深度解析与避坑指南4.1pypdf的decrypt方法返回值陷阱这是一个非常重要的细节。在PyPDF2/pypdf中reader.decrypt(password)的返回值需要小心处理。对于用户密码打开密码如果密码正确decrypt方法会返回一个非零整数。这个整数在不同的版本中含义可能不同有时是解密成功的页数有时只是表示成功的状态码。所以判断条件应该是if decrypt_result ! 0:或if decrypt_result:。我的代码中使用了 1这在很多简单文档上可行但不够健壮。更稳健的写法是result reader.decrypt(password) if result ! 0: # 或者 if result: # 解密成功对于所有者密码权限密码如果PDF同时设置了用户密码和所有者密码用所有者密码解密也会返回成功但可能只能获得受限权限如不能打印。用用户密码解密才能获得所有权限。我们的脚本主要目的是“打开”所以只要decrypt返回非零通常就认为成功了。验证解密是否真正生效有时decrypt返回非零但后续操作仍会报错。一个更健壮的做法是尝试访问文档的某个属性比如reader.metadata或尝试提取一点点文本看是否会抛出异常。我在try_decrypt_pdf函数中加入了提取文本的尝试就是为了做二次验证。4.2 密码生成策略的优化艺术最初的密码本生成是决定成败的关键。如果提示信息错误或者生成策略有偏差就会南辕北辙。优先顺序至关重要一定要把可能性最大的密码放在列表最前面尝试。比如对于日期“20180520”应该尝试20180520、180520、05202018等多种格式并且这些格式应该排在随机组合数字的前面。利用社会工程学除了明显的日期想一想双方是否有共同喜欢的数字比如某部电影的上映日期、某个游戏的版本号、第一次旅行的航班号将这些信息纳入生成规则。避免组合爆炸itertools.product非常强大但稍不留神就会生成海量密码例如10个字符取6位组合是10^6一百万。必须通过repeat参数和提前用set过滤来严格控制规模。我们的目标是在数百到数万的数量级内精准尝试而不是进行百万级的暴力破解。引入外部字典如果自定义密码本失败可以谨慎地引入一些通用的弱密码字典文件如rockyou.txt的头部常见密码但务必注意这可能会急剧增加尝试时间。4.3 多线程加速与资源管理解密尝试是I/O密集型任务读取文件、解密运算。使用多线程可以显著提升尝试速度因为当一个线程在等待I/O时CPU可以处理另一个线程的请求。ThreadPoolExecutorvsProcessPoolExecutor这里选择ThreadPoolExecutor因为主要瓶颈在I/O和pypdf库的函数调用这部分由于GIL存在CPU并行效率不高线程切换开销小能有效重叠I/O等待时间。max_workers设置通常设置为CPU核心数的2-4倍。我设置为6这是一个经验值。设置太高会导致线程间切换开销增大可能反而变慢。共享状态与锁found_password和attempted计数器是被所有线程共享的。使用threading.Lock来确保对它们的修改是线程安全的避免出现竞争条件。优雅终止一旦某个线程找到了密码我们通过检查found_password is not None这个标志让其他线程中的任务函数提前返回避免无用的尝试。但已经提交给Executor的任务无法直接取消这个标志位检查是在任务内部进行的“软终止”。4.4 错误处理与日志记录健壮的脚本必须能处理各种异常情况。捕获特定异常pypdf.errors.PdfReadError是库定义的异常通常由无效密码或文件损坏引发。捕获它并将其归类为“密码错误”而不是让程序崩溃。区分错误类型try_decrypt_pdf函数返回(bool, str)元组其中字符串信息有助于调试。是“文件未加密”、“密码错误”还是“未知错误”进度反馈在批量尝试中每100次尝试打印一次进度能让用户知道程序仍在运行而不是卡死了。这对于长时间运行的任务是必须的。结果持久化可以考虑将尝试过的密码和结果记录到日志文件中这样即使程序中断下次也可以从中断处继续或者用于分析哪些类型的密码失败了。5. 我的实操过程与意想不到的结果我怀着忐忑的心情将脚本指向了那个名为“给过去的回忆.pdf”的文件。密码本按照我们的策略生成了大约1200个候选密码。运行脚本。控制台开始快速滚动进度: 100/1200 (8.3%) 进度: 200/1200 (16.7%) ...我的心跳似乎也跟着进度条的跳动在加速。在第419次尝试时滚动停止了。成功找到密码正在停止其他任务... 解密成功密码是: 20180520 总耗时: 23.7 秒 已保存解密后的文件至: 给过去的回忆_decrypted.pdf密码果然是我们在一起的日期2018年5月20日。一种复杂的情绪涌上心头但技术人的好奇心立刻占据了上风——文件里到底是什么我深吸一口气用PDF阅读器打开了那个新生成的_decrypted.pdf文件。文件打开了。里面只有一页页面中央是巨大的、加粗的四个字“你 还 单 身”我愣在屏幕前随即忍不住笑了出来。原来所谓的“加密PDF”所谓的“回忆”只是一个带着些许恶作剧性质的数字谜题。她用我们之间最有意义的数字设置了密码等待着我用技术或者回忆去解开而谜底却是一个跨越时空的、简单的问候或者说调侃。6. 常见问题与排查技巧实录在实际操作和与同行交流中我遇到过不少问题。这里总结一份速查表问题现象可能原因排查与解决思路decrypt返回非零但读取出错或报PdfReadError1. 密码是“所有者密码”仅能打开但权限不足。2. PDF文件本身已损坏或加密方式特殊。3.pypdf库版本与文件加密算法不兼容。1. 尝试用找到的密码在Adobe Reader等标准软件中打开看是否有权限限制提示。2. 用文本编辑器如VS Code以二进制形式稍微打开PDF查看头部是否有/Encrypt字典确认是标准加密。3. 升级pypdf到最新版或尝试使用PyMuPDF(fitz)库进行解密它支持更广泛的加密算法。脚本运行非常慢每秒只能尝试几个密码1. PDF文件很大每次decrypt都需要加载整个文件。2. 密码生成逻辑或I/O操作有瓶颈。3. 没有使用并发。1. 这是正常现象大文件解密开销大。可以考虑将文件复制到内存盘RAM Disk再操作。2.关键优化使用PyMuPDF。它的解密速度比pypdf快一个数量级以上。代码调整如下pythonbr import fitz # PyMuPDFbr doc fitz.open(pdf_path)br if doc.authenticate(password): # 返回非零即成功br # 认证成功br doc.save(decrypted.pdf)br3. 确保使用了多线程并调整max_workers。明明密码是对的但脚本一直提示失败1. 密码编码问题。比如密码包含中文或特殊字符在脚本中字符串编码不一致。2. 换行符问题。从文件读取密码字典时密码末尾可能带了\n。3. 密码中存在不可见字符。1. 在Python脚本中明确指定字符串编码或尝试对密码进行encode(utf-8)后再传入解密函数看库是否支持bytes。2. 使用password.strip()清除首尾空白字符。3. 打印出尝试的密码用repr(password)查看其原始表示确认是否包含\r,\x00等字符。遇到“Certificate Encryption”错误PDF使用的是证书加密公钥加密而非密码加密。这种情况基本无解除非你拥有对应的私钥。脚本会直接跳过或报错。pypdf目前不支持破解证书加密。内存占用越来越高最终崩溃1. 密码列表巨大全部加载到内存。2. 多线程或循环中产生了内存泄漏较少见。1. 对于超大型字典文件如上百万条不要一次性读入内存。使用生成器(yield)或分块读取的方式每次从文件流中读取一部分密码进行尝试。2. 确保在每次尝试后及时关闭文件句柄使用with open语句可以自动管理。使用PyMuPDF时在每次尝试后调用doc.close()。个人心得与进阶技巧PyMuPDF是性能王者如果追求速度或者处理大量加密PDFPyMuPDF(fitz) 是不二之选。它的authenticate方法速度极快并且功能全面。将脚本中的核心解密函数替换为PyMuPDF版本速度能有10倍以上的提升。密码本的“智能”远比“庞大”重要花一小时分析密码提示、设计生成规则其效率可能远超盲目运行一晚上的千万级暴力字典。结合生日、姓名缩写、键盘模式如qwerty、常见变形代替a1代替i来构建规则事半功倍。离线操作保护隐私所有操作务必在离线环境下进行。切勿将敏感的加密PDF上传到任何在线解密网站即使它们声称安全。法律与道德的边界这是我反复强调的。这个技术只应用于自己拥有合法访问权但忘记了密码的文件。用于其他任何场景都可能构成违法。在编写和分享此类脚本时也必须在开头加入显著的免责声明和道德警示。那次解密之后我给她回了一封邮件只有三个字“你猜呢” 技术解开了文件的密码却解不开生活的谜题。但这个过程本身就像一次精巧的工程实践将情感、记忆和代码编织在一起。最后留下的除了那个令人哑然失笑的PDF还有这段关于Python、加密和一点点人生恶作剧的完整记录。

相关新闻