Python pycryptodome、shutil 目录同步与 tqdm 进度条、PyInstaller 打包

pycryptodome:No module named ‘Crypto’

安装了 pycryptodome 但运行时报错:

ModuleNotFoundError: No module named 'Crypto'

原因:pycryptodome 安装后的导入路径是 Crypto(大写C),和旧版 pycrypto 同名,但两者不兼容。虚拟环境里如果同时装了两个就会冲突。

解决:

pip uninstall pycrypto pycryptodome
pip install pycryptodome

导入方式不变:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

如果想用 Cryptodome 命名空间(与 pycrypto 完全隔离),改装 pycryptodomex

pip install pycryptodomex
from Cryptodome.Cipher import AES

虚拟环境里确认安装:

pip list | grep -i crypto
# pycryptodome   3.20.0

shutil 目录同步加 tqdm 进度条

shutil.copytree 不支持进度回调,需要手动遍历:

import shutil
from pathlib import Path
from tqdm import tqdm

def sync_dir(src: Path, dst: Path):
    files = [f for f in src.rglob("*") if f.is_file()]
    dst.mkdir(parents=True, exist_ok=True)

    for src_file in tqdm(files, desc="复制中", unit="file"):
        rel = src_file.relative_to(src)
        dst_file = dst / rel
        dst_file.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src_file, dst_file)

shutil.copy2 会保留原文件的修改时间和权限位。

覆盖前备份,保留最近 N 个

from datetime import datetime

def backup_and_replace(src: Path, dst: Path, keep=5):
    if dst.exists():
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup = dst.parent / f"{dst.name}_bak_{ts}"
        dst.rename(backup)

        # 清理旧备份,只保留最近 keep 个
        baks = sorted(dst.parent.glob(f"{dst.name}_bak_*"))
        for old in baks[:-keep]:
            shutil.rmtree(old)

    sync_dir(src, dst)

PyInstaller 打包后路径检测

-F 打包成单文件 exe 后,__file__ 不再可用,需要用 sys.executable

import sys
from pathlib import Path

if getattr(sys, "frozen", False):
    # 打包环境:exe 所在目录
    BASE_DIR = Path(sys.executable).parent
else:
    # 开发环境:脚本所在目录
    BASE_DIR = Path(__file__).parent

CONFIG_PATH = BASE_DIR / "config.json"

sys.frozen 在 PyInstaller 打包环境下为 True,普通 Python 解释器下不存在(getattr 返回 False)。

打包命令:

pyinstaller -F -n mytool --noconsole main.py
  • -F:单文件
  • -n mytool:输出名称
  • --noconsole:不弹出控制台窗口(GUI 工具用)

完整示例:插件目录同步工具

import sys
import json
import shutil
from pathlib import Path
from datetime import datetime
from tqdm import tqdm

BASE_DIR = Path(sys.executable).parent if getattr(sys, "frozen", False) else Path(__file__).parent
CONFIG = json.loads((BASE_DIR / "config.json").read_text(encoding="utf-8"))

SRC = Path(CONFIG["src"])
DST = Path(CONFIG["dst"])

def backup(dst: Path, keep=5):
    if not dst.exists():
        return
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    dst.rename(dst.parent / f"{dst.name}_bak_{ts}")
    for old in sorted(dst.parent.glob(f"{dst.name}_bak_*"))[:-keep]:
        shutil.rmtree(old)

def copy_with_progress(src: Path, dst: Path):
    files = [f for f in src.rglob("*") if f.is_file()]
    dst.mkdir(parents=True, exist_ok=True)
    for f in tqdm(files, unit="file"):
        rel = f.relative_to(src)
        out = dst / rel
        out.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(f, out)

backup(DST)
copy_with_progress(SRC, DST)
print("同步完成")

config.json

{
  "src": "C:/Users/用户名/AppData/Roaming/adspower_global/cwd_global/chrome/Default",
  "dst": "D:/backup/chrome_profile"
}