使用hashlib.sha256分块读取文件计算SHA-256哈希值实现去重,需以二进制模式打开、8KB分块、用defaultdict按哈希归组路径,异常捕获后排序保留首个路径,避免误删。

用 hashlib 计算文件的 SHA-256 哈希值
哈希去重的核心是把文件内容映射为唯一字符串,SHA-256 是目前最稳妥的选择——MD5 碰撞风险已不可忽视,而 hashlib.sha256() 在 Python 标准库中稳定、无需额外依赖。
关键点在于:不能直接 hashlib.sha256(file.read()).hexdigest(),大文件会爆内存。必须分块读取:
def get_file_hash(filepath):
h = hashlib.sha256()
with open(filepath, "rb") as f:
while chunk := f.read(8192): # 每次读 8KB
h.update(chunk)
return h.hexdigest()- 块大小选
8192是平衡 I/O 和 CPU 的常见值;太小(如 1024)会增加系统调用开销,太大(如 1MB)对 SSD 友好但对 HDD 不必要 - 务必用
"rb"模式打开,文本模式在换行符处理上不一致,会导致同一文件在不同系统上哈希不同 - Windows 上路径含中文时,
open()默认编码不影响二进制读取,放心用
遍历目录并按哈希值归组文件
单纯比对两两文件效率极低,O(n²);正确做法是一边扫描一边用哈希作字典 key,把相同哈希的路径存进列表:
from collections import defaultdict
hash_to_paths = defaultdict(list)
for root, _, files in os.walk("/path/to/dir"):
for name in files:
path = os.path.join(root, name)
try:
h = get_file_hash(path)
hash_to_paths[h].append(path)
except (OSError, IOError) as e:
# 权限不足、文件被占用、符号链接断链等
print(f"跳过 {path}: {e}")-
defaultdict(list)避免每次判断 key 是否存在,写起来干净 - 必须包住
get_file_hash()在try/except里——尤其是网络盘、挂载卷、临时文件,OSError比想象中频繁 - 不要用
os.scandir()替代os.walk()来“优化”,除非你明确需要深度控制;对普通去重场景,可读性和稳定性更重要
识别重复项并安全删除(保留最早那个)
哈希相同 ≠ 文件一定重复(极小概率碰撞),但实践中可忽略;真正要防的是误删——比如某个配置文件和日志文件哈希巧合相同(几乎不可能),但更现实的风险是:你删了源文件,只留下备份副本。
立即学习“Python免费学习笔记(深入)”;
- 永远先打印将要删除的路径,确认无误再执行
os.remove() - 按路径字符串排序后取第一个作为“保留项”,逻辑简单且可复现:
paths = sorted(hash_to_paths[h]),然后paths[1:]是待删列表 - 不要用
mtime或ctime判断新旧——NTFS 和 ext4 对时间戳处理不同,且 git checkout、unzip 等操作会重置时间 - 如果目标目录含硬链接,
os.stat().st_ino可辅助识别,但哈希去重本身已覆盖该场景,无需额外处理
处理大目录时的内存与速度权衡
当扫描几十万文件时,hash_to_paths 字典会吃掉几百 MB 内存——不是因为哈希值长,而是 Python 字符串和 list 对象的开销。这时候得做减法:
- 哈希值本身只需存一次,但路径字符串重复存储浪费大;可改用
tuple存路径(节省不多),或用array.array('u')存路径(过度优化,不推荐) - 更实际的方案:加个
--dry-run开关,先输出重复组统计,再分批处理;或用shelve把哈希表存到磁盘(慢但稳) - CPU 不是瓶颈,I/O 是——SSD 上单线程足够;别急着上
multiprocessing,Python 的 GIL 在文件读取这种 I/O-bound 场景下几乎没收益,反而增加进程启动和 IPC 开销
真正容易被忽略的是符号链接:默认 os.walk() 不跟随软链,但如果你手动 os.path.realpath() 解析了,就得小心循环链接导致无限递归——哈希计算前加个 os.path.islink() 判断并跳过,比事后崩溃强。


















