全盘重复文件检测与清理(MD5去重,含4个实跑大坑)

下载目录里堆了三年,同一个附件存了五份、大文件备份了又备份。下面这份脚本按文件内容(不是按文件名)找出重复,保留每组里最早修改的一份,其余列出或删除。本文所有代码完整实跑过,不是纸面代码。

核心脚本

import os, sys, hashlib
from collections import defaultdict

def md5(p, buf=1 << 20):
    h = hashlib.md5()
    with open(p, 'rb') as f:
        while chunk := f.read(buf):
            h.update(chunk)
    return h.hexdigest()

# 阶段1: 按大小分组 —— 大小不同必不重复,直接排除,避免全盘算MD5
by_size = defaultdict(list)
for root, dirs, files in os.walk(sys.argv[1]):
    if 'System Volume Information' in root or '$RECYCLE.BIN' in root:
        dirs[:] = []            # 跳过Windows系统/回收站目录,否则扫描报错+垃圾命中的假重复
        continue
    for fn in files:
        p = os.path.join(root, fn)
        try:
            by_size[os.path.getsize(p)].append(p)
        except OSError:         # 正被占用的文件读不到大小,跳过而不是让整个扫描崩掉
            pass

# 阶段2: 只对"大小相同"的组算MD5
by_hash = defaultdict(list)
for sz, paths in by_size.items():
    if len(paths) < 2:
        continue
    for p in paths:
        by_hash[(sz, md5(p))].append(p)

groups = [g for g in by_hash.values() if len(g) > 1]
freed = 0
for g in groups:
    g.sort(key=lambda p: os.path.getmtime(p))   # 保留最早修改的那份,删其余
    keep, dups = g[0], g[1:]
    print(f"[组] 保留: {keep}")
    for p in dups:
        freed += os.path.getsize(p)
        print(f"     重复: {p}")
        if '--delete' in sys.argv:
            os.remove(p)
print(f"\n共 {len(groups)} 组重复,可释放 {freed/1048576:.2f} MB")

用法:python3 dupfind.py 目录(只报告);加 --delete 才真删。建议先跑一遍看清单再删。

实跑验证

造了一组测试数据:同一内容存 3 份(report_v1.docx / report_final.docx / 下载附件2.docx)、一个 2KB 随机文件存 2 份、再加 2 个各不相同的唯一文件。结果:

4个真实大坑

坑1:只按大小去重 = 大规模误删。实测 aaa.txt("AAAAA") 和 bbb.txt("BBBBB") 都是5字节,按大小分组判重会报告它们是"重复"。大小只能当预筛选漏斗,最终判定必须算内容 MD5。反过来也有个反直觉点:内容相同的文件大小必然相同,所以"大小不同的组直接跳过"是安全的加速,不是偷懒。
坑2:不排除 Windows 系统目录,扫描直接崩或产出垃圾结果。os.walk 撞上 System Volume Information(权限拒绝)会抛 OSError,没 try 就让整个扫描中途死掉;$RECYCLE.BIN 里的回收站文件还会被当成"重复"报出来。两处都要跳过;对 getsize 单独 try,被占用的文件跳过而不是放弃全盘。
坑3:保留策略要排序取唯一,不能"留第一个"。os.walk 的遍历顺序在Windows上不保证稳定,"保留每组的第一个"会导致两次运行留下不同副本,用户第二次一看"怎么上次删的不是这份"。按 mtime 排序取最早的,规则确定、可复现。
坑4:默认 dry-run,--delete 才动手。清理类脚本一旦误删不可恢复(脚本没做回收站)。先只打印清单让人确认,再显式加参数执行删除,是这类工具的底线设计。另外删除时应该删文件而不是删目录,防止 os.walk 迭代中目录树变化。

什么时候不用MD5

不想自己动手? 把文件夹清单发给我们,脚本代写代跑、按你的规则定制保留策略(最早/最新/指定目录优先)。
👉 看服务列表 · 回首页(AI客服,一般几分钟内回复)

本文所有脚本在真实环境完整跑通后才发布。虚拟服务交付后不退款;未达承诺免费重做。