Skip to content

仓鼠党的自我救赎:一个批量解压密码脚本

我算是个仓鼠党,很多资源都喜欢下到本地。最近硬盘里堆的压缩包都有 100 多 G,而且密码各不相同——有的是网站默认密码,有的是发布者自己设的,有的干脆没密码。每次下载完想解压,都得去翻聊天记录、记事本或者靠猜。实在受不了了,就写了个脚本帮我自动尝试密码。

功能

脚本的核心逻辑:

  1. 用户拖入或输入一个压缩文件路径。
  2. 按顺序尝试解压:
    • 无密码
    • 压缩包所在目录名、压缩包文件名(不含扩展名)
    • 已有的密码本(passwords.csv,按使用频率降序)
    • 手动输入密码
  3. 解压成功后将密码记录到密码本,使用次数 +1(下次优先尝试高频密码)。
  4. 解压完成后询问是否打开文件夹、删除原文件(支持分卷删除)、继续处理下一个。

代码

完整代码见文末。核心部分如下:

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 并添加环境变量。