下载目录里堆了三年,同一个附件存了五份、大文件备份了又备份。下面这份脚本按文件内容(不是按文件名)找出重复,保留每组里最早修改的一份,其余列出或删除。本文所有代码完整实跑过,不是纸面代码。
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 个各不相同的唯一文件。结果:
unique1.txt/unique2.txt 零误伤;--delete 后剩 4 个文件全部是每组保留的那份;diff 完全一致(幂等);aaa.txt("AAAAA") 和 bbb.txt("BBBBB") 都是5字节,按大小分组判重会报告它们是"重复"。大小只能当预筛选漏斗,最终判定必须算内容 MD5。反过来也有个反直觉点:内容相同的文件大小必然相同,所以"大小不同的组直接跳过"是安全的加速,不是偷懒。os.walk 撞上 System Volume Information(权限拒绝)会抛 OSError,没 try 就让整个扫描中途死掉;$RECYCLE.BIN 里的回收站文件还会被当成"重复"报出来。两处都要跳过;对 getsize 单独 try,被占用的文件跳过而不是放弃全盘。os.walk 的遍历顺序在Windows上不保证稳定,"保留每组的第一个"会导致两次运行留下不同副本,用户第二次一看"怎么上次删的不是这份"。按 mtime 排序取最早的,规则确定、可复现。--delete 才动手。清理类脚本一旦误删不可恢复(脚本没做回收站)。先只打印清单让人确认,再显式加参数执行删除,是这类工具的底线设计。另外删除时应该删文件而不是删目录,防止 os.walk 迭代中目录树变化。_final/(1)这些尾巴),脚本可以再加提示;du 排序,别绕道去重。本文所有脚本在真实环境完整跑通后才发布。虚拟服务交付后不退款;未达承诺免费重做。