仓鼠党的自我救赎:一个批量解压密码脚本
我算是个仓鼠党,很多资源都喜欢下到本地。最近硬盘里堆的压缩包都有 100 多 G,而且密码各不相同——有的是网站默认密码,有的是发布者自己设的,有的干脆没密码。每次下载完想解压,都得去翻聊天记录、记事本或者靠猜。实在受不了了,就写了个脚本帮我自动尝试密码。
功能
脚本的核心逻辑:
- 用户拖入或输入一个压缩文件路径。
- 按顺序尝试解压:
- 无密码
- 压缩包所在目录名、压缩包文件名(不含扩展名)
- 已有的密码本(
passwords.csv,按使用频率降序) - 手动输入密码
- 解压成功后将密码记录到密码本,使用次数 +1(下次优先尝试高频密码)。
- 解压完成后询问是否打开文件夹、删除原文件(支持分卷删除)、继续处理下一个。
代码
完整代码见文末。核心部分如下:
python
# 密码尝试顺序
def try_extract(archive_path, output_dir, password):
# 调用 7z 命令行解压
cmd = ['7z', 'x', archive_path, f'-o{output_dir}', '-aoa']
if password:
cmd.append(f'-p{password}')
# 执行并返回成功与否密码本格式为 CSV:
csv
password,usage_count
Matyux,15
114514,1加载时按 usage_count 降序排序,高频优先。
运行日志
脚本跑起来的样子:
log
[2026-04-24 05:02:10] 文件: D:\Users\Downloads\BaiDuCloud\[UserFolder]\example_file_1.rar | 密码: SkyBlue92
[2026-04-24 05:02:42] 文件: D:\Users\Downloads\BaiDuCloud\[UserFolder]\example_file_2.7z | 密码: Mat
[2026-04-24 05:02:57] 文件: D:\Users\Downloads\BaiDuCloud\[UserFolder]\sub_folder\example_file_3.zip | 密码: a7k2m9
[2026-04-24 05:04:11] 文件: D:\Users\Downloads\BaiDuCloud\OtherFolder\example_file_4.7z.001 | 密码: 不要在线解压!踩过的坑
1. 分卷删除只删了第一个
最初版本里,删除原文件时只删了 .001,剩下的 .002、.003 还在硬盘里,导致再次解压时 7z 报错“文件不完整”。后来加了 find_volume_files 函数,检测到分卷后一次性删除所有后续卷。
2. 7z 中文版进度条不显示
脚本尝试用正则 (\d{1,3})% 匹配 7z 输出的百分比,但中文版 7z 输出的是“已完成 45%”,没有单独的 % 符号(或者格式不同)。目前这个 bug 还没修,下一个版本会改。
3. 密码本 CSV 格式不够健壮
如果密码本身包含逗号,直接写 CSV 会被拆成两列。解决办法是给字段加双引号或者改用别的分隔符。不过目前我的密码里还没有逗号,所以没触发。
4. 命令依赖是给自己埋的坑
脚本硬编码了 7z 命令,要求系统已安装 7-Zip 并加入 PATH。本来就是给自己用的,没考虑通用性。如果拿给别人用,对方大概率会报错“找不到 7z”。
下一个版本的规划
最近在构思 v2.0,准备加入命令系统(类似 CLI 工具),支持:
auto openfile true/false:解压后自动打开文件夹auto deletefile true/false:自动删除原压缩包auto output <路径>:统一解压目录list passwords:查看高频密码delete logs:清空日志
还准备修复进度条 bug,完善手动输入密码的重试逻辑,并支持更多压缩格式(.rar、.7z、.zip 等)。
目前这个版本虽然糙,但已经帮我解压了几百 GB 的资源,密码本积累了二十多条常用密码,算是仓鼠党的自救成功。
完整代码
python
import os
import sys
import subprocess
import csv
import datetime
import re
import glob
# ==================== 配置区 ====================
PASSWORD_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "passwords.csv")
LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "extract_log.txt")
# ===============================================
def load_passwords(filepath):
passwords = {}
if not os.path.isfile(filepath):
print(f"密码本不存在({filepath}),将使用空密码列表。")
return passwords
try:
with open(filepath, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if not row or len(row) < 2:
continue
row = [cell.strip() for cell in row]
pwd = row[0]
try:
cnt = int(row[1])
except ValueError:
continue
if pwd not in passwords:
passwords[pwd] = cnt
print(f"已加载 {len(passwords)} 条密码记录(含空密码)。")
except Exception as e:
print(f"读取密码本失败:{e},将使用空密码列表。")
passwords = {}
return passwords
def save_passwords(filepath, passwords):
try:
os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
with open(filepath, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
for pwd, cnt in passwords.items():
writer.writerow([pwd, cnt])
print(f"密码本已保存到 {filepath}")
except Exception as e:
print(f"保存密码本失败:{e}")
def sort_passwords(passwords):
sorted_items = sorted(passwords.items(), key=lambda x: x[1], reverse=True)
return [pwd for pwd, _ in sorted_items]
def find_volume_files(first_file):
"""
如果是分卷 .001 文件,寻找同目录下所有 .002, .003 等后续分卷
返回列表 [first_file, ...],如果不是分卷则返回空列表
"""
dirname = os.path.dirname(first_file)
basename = os.path.basename(first_file)
# 匹配 *.001 模式(可能带额外扩展名,比如 .7z.001 或 .rar.001)
if not basename.endswith('.001'):
return []
prefix = basename[:-4] # 去掉 .001
volumes = [first_file]
# 从002开始尝试,最大尝试到999
for i in range(2, 1000):
vol_name = f"{prefix}.{i:03d}" # 如 file.7z.002 或 file.002
vol_path = os.path.join(dirname, vol_name)
if os.path.isfile(vol_path):
volumes.append(vol_path)
else:
# 也尝试不带其他扩展名的纯数字后缀
# 如果前缀已经包含扩展名 (如 file.7z.001 -> prefix = file.7z.)
# 则上面的构造会生成 file.7z.002,正确
# 如果前缀是 file. (即 file.001),则尝试 file.002
# 两种都覆盖了
break
return volumes
def extract_with_progress(archive_path, output_dir, password):
"""
解压并实时显示进度条。
password 为空字符串代表无密码。
返回 True/False。
"""
cmd = ['7z', 'x', archive_path, f'-o{output_dir}', '-aoa', '-bsp1'] # -bsp1 将进度输出到 stdout
if password != '':
cmd.append(f'-p{password}')
try:
# 使用 Popen 以便逐行读取输出
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # 合并到 stdout 方便统一解析(因为 -bsp1 可能输出到 stdout)
text=True,
startupinfo=startupinfo,
encoding='utf-8',
errors='replace',
bufsize=1,
universal_newlines=True
)
# 正则匹配 7z 的进度百分比,例如 " 45%" 或 "45%"
progress_pattern = re.compile(r'(\d{1,3})%')
last_progress = -1
for line in process.stdout:
match = progress_pattern.search(line)
if match:
percent = int(match.group(1))
if percent != last_progress:
print(f"\r解压进度: {percent}%", end='', flush=True)
last_progress = percent
# 你也可以在这里输出其他调试信息,但生产环境不输出
process.wait()
# 解压完成后清除进度行
if last_progress >= 0:
print('\r' + ' ' * 20 + '\r', end='', flush=True) # 清除进度行
return process.returncode == 0
except FileNotFoundError:
print("错误:找不到 7z 命令,请确认已安装 7-Zip 并添加至环境变量。")
sys.exit(1)
except Exception as e:
print(f"\n解压异常:{e}")
return False
def write_log(archive_path, password):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
pwd_display = password if password != '' else '(无密码)'
log_entry = f"[{timestamp}] 文件: {archive_path} | 密码: {pwd_display}\n"
try:
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(log_entry)
except Exception as e:
print(f"写入日志失败:{e}")
def interactive_loop(passwords):
while True:
user_input = input("\n请拖入或输入压缩文件路径(输入 exit 退出): ").strip()
if user_input.lower() == 'exit':
break
# 路径处理:去除首尾引号,并规范化
file_path = user_input.strip('"')
file_path = os.path.normpath(file_path)
if not os.path.isfile(file_path):
print(f"文件不存在:{file_path}")
continue
base = os.path.splitext(file_path)[0]
# 如果是 .001 分卷,解压文件夹名应该基于基础前缀(去掉末尾的数字点数字)
if file_path.endswith('.001'):
# 获取不带 .001 的路径作为文件夹名基础,例如 file.7z.001 -> file
# 简单处理:去掉最后4个字符 '.001'
base = file_path[:-4]
output_dir = base
print(f"解压目标文件夹:{output_dir}")
# ------ 构建尝试密码列表 ------
# 1. 自动提取压缩文件所在目录的名字作为密码优先尝试
dir_name = os.path.basename(os.path.dirname(file_path))
extra_pwd = dir_name if dir_name else ''
# 去重:如果 extra_pwd 与已知密码重复则不重复添加;且非空
trial_list = []
if extra_pwd and extra_pwd not in passwords:
trial_list.append(extra_pwd)
print(f"附加尝试目录名密码: {extra_pwd}")
# 2. 加入排序后的已知密码(去重:排除已经加过的)
sorted_pwd_list = sort_passwords(passwords)
for pwd in sorted_pwd_list:
if pwd not in trial_list:
trial_list.append(pwd)
total = len(trial_list)
success = False
used_password = None
# 遍历尝试,显示进度
for idx, pwd in enumerate(trial_list, 1):
pwd_label = pwd if pwd != '' else '(无密码)'
# 刷新显示当前尝试(同一行)
print(f"\r尝试密码:{pwd_label}({idx}/{total})", end='', flush=True)
if extract_with_progress(file_path, output_dir, pwd):
# 成功后换行
print(f"\n解压成功!使用的密码:{pwd_label}")
passwords[pwd] = passwords.get(pwd, 0) + 1
success = True
used_password = pwd
break
else:
# 所有密码尝试失败,换行结束进度显示
print()
if not success:
print("密码本中的所有密码(含目录名)均无法解压该文件。")
while True:
choice = input("请输入新密码尝试解压(直接回车表示无密码,输入 'skip' 跳过): ").strip()
if choice.lower() == 'skip':
print("已跳过此文件。")
break
else:
new_pwd = choice
pwd_label = new_pwd if new_pwd != '' else '(无密码)'
# 尝试新密码,不显示进度索引
print(f"尝试新密码:{pwd_label}")
if extract_with_progress(file_path, output_dir, new_pwd):
print(f"解压成功!新密码已记录:{pwd_label}")
passwords[new_pwd] = passwords.get(new_pwd, 0) + 1
success = True
used_password = new_pwd
break
else:
print("密码错误,请重试或输入 'skip' 跳过。")
if success:
write_log(file_path, used_password)
# ------- 后续操作循环 -------
file_deleted = False
while True:
print("\n后续操作:")
if not file_deleted:
print(" O - 打开解压后的文件夹")
print(" D - 删除原压缩文件")
else:
print(" O - 打开解压后的文件夹")
print(" (原压缩文件已删除)")
print(" C - 继续处理下一个文件")
print(" E - 退出程序")
opt = input("请选择: ").strip().lower()
if opt == 'o':
if os.path.exists(output_dir):
os.startfile(output_dir)
else:
print("文件夹不存在,可能已被移动或删除。")
elif opt == 'd':
if file_deleted:
print("原压缩文件已被删除,无需重复操作。")
else:
# 检查是否为分卷并询问
volumes = find_volume_files(file_path)
if len(volumes) > 1:
print("检测到以下分卷文件:")
for v in volumes:
print(f" {v}")
confirm = input("是否同时删除所有分卷?(y/n): ").strip().lower()
if confirm == 'y':
for v in volumes:
try:
os.remove(v)
print(f"已删除:{v}")
except Exception as e:
print(f"删除失败 {v}: {e}")
file_deleted = True
else:
# 仅删除第一个
try:
os.remove(file_path)
print(f"已删除:{file_path}")
file_deleted = True
except Exception as e:
print(f"删除失败:{e}")
else:
try:
os.remove(file_path)
print(f"已删除压缩文件:{file_path}")
file_deleted = True
except Exception as e:
print(f"删除失败:{e}")
elif opt == 'c':
break
elif opt == 'e':
return
else:
print("输入无效,请重新选择。")
def main():
passwords = load_passwords(PASSWORD_FILE)
try:
interactive_loop(passwords)
except KeyboardInterrupt:
print("\n用户中断。")
save_passwords(PASSWORD_FILE, passwords)
print("程序退出。")
if __name__ == "__main__":
main()附:代码仅作个人存档,不保证通用性。需要自行安装 7-Zip 并添加环境变量。