硬盘用了六年,D 盘塞了十几万个文件。同一张图存了四五份,同一个安装包下过三次还在。靠眼睛找重复不现实,所以我写了个脚本。
先说结论
真正靠谱的重复判定只有一步:先比大小,再比哈希。光比文件大小会漏掉大量「大小刚好一样」的误判,光比文件名则完全没有意义——同一个文件被改名三次是常有的事。
坑一:硬链接被当成重复
第一版脚本跑完,报出两万多个「重复文件」。我差点一键全删,幸好先看了看报告——里面大量是同一个文件的不同硬链接。
硬链接在文件系统里只是同一份数据的另一个名字,删掉其中一个,数据还在。但我的脚本把它们当成了「两份一样的内容要删一份」,逻辑上没错,实际却是在破坏目录结构。
判断方法:同一设备号 + 同一 inode 号,就是同一个文件的多个名字,不算重复。Python 里用 os.stat() 拿 st_dev 和 st_ino 两个值比一下就行。
坑二:大小相同不代表内容相同
我用「文件大小」做第一道筛选,这是标准做法,能砍掉 99% 的比对量。但筛完之后必须做完整哈希,不能省。
原因很朴素:一份 4KB 的文本和一个 4KB 的图标,大小完全一样,内容毫无关系。我第一批测试里就有一堆大小正好是 1024 字节的空模板文件被标记成重复。
哈希算法我选的是 BLAKE2b,不是 MD5。不是因为 MD5 会被碰撞——日常文件去重根本不用担心这个——而是 BLAKE2b 在现代 CPU 上带 SSE 加速,比 MD5 快一倍多。对十几万个文件来说,这个差别是十几分钟。
坑三:删除顺序把源文件删了
这是最危险的一个。我的逻辑是「每组重复文件保留第一个,删除其余」。问题在于——如果这一组里的「第一个」正好是硬链接指向的那个副本,而「其余」里包含了原始数据,删完数据就真的没了。
现在我的做法是:脚本只生成报告,绝不自动删除。报告是一份 CSV,三列:保留路径、待删路径、文件大小。我自己看一眼,确认无误再执行删除。
多花五分钟,换掉一次不可挽回的数据丢失,很划算。
完整脚本骨架
import os, hashlib
from collections import defaultdict
def walk(root):
for dirpath, _, filenames in os.walk(root):
for fn in filenames:
yield os.path.join(dirpath, fn)
def key_of(path):
st = os.stat(path)
return (st.st_size, st.st_dev, st.st_ino)
# 第一遍:按大小分组,只有同大小的才值得算哈希
by_size = defaultdict(list)
for p in walk(ROOT):
try:
by_size[os.path.getsize(p)].append(p)
except OSError:
continue
# 第二遍:同大小里算哈希
def digest(path, chunk=1 << 20):
h = hashlib.blake2b(digest_size=16)
with open(path, 'rb') as f:
while True:
b = f.read(chunk)
if not b:
break
h.update(b)
return h.hexdigest()
report = []
for size, paths in by_size.items():
if len(paths) < 2:
continue
by_hash = defaultdict(list)
for p in paths:
try:
by_hash[digest(p)].append(p)
except OSError:
continue
for h, same in by_hash.items():
if len(same) < 2:
continue
# 排除硬链接:同 dev+ino 的算同一个文件
seen_ino = set()
keep, drop = None, []
for p in same:
st = os.stat(p)
ident = (st.st_dev, st.st_ino)
if ident in seen_ino:
continue # 硬链接副本,跳过
seen_ino.add(ident)
if keep is None:
keep = p
else:
drop.append(p)
if drop:
report.append((keep, drop, size))
跑完的结果
十五万八千个文件,扫了大概十一分钟。报告里真实重复 3,412 组,可回收空间 47.8 GB。硬链接误判 1,900 多个,被 inode 判断挡掉了。
最后我手工核对了前两百组,全对,然后才批量删。删完 D 盘从 91% 降到 41%。
两个补充建议
一是先把报告导出来看,别信自动删除。任何号称「一键清理重复文件」的工具,只要没有保留策略和预览,都不该用在有真实数据的盘上。
二是给哈希结果加缓存。文件没改过,哈希就不会变。我在脚本旁边放了个小 JSON,记录「路径 + 修改时间 + 大小 → 哈希」,第二次跑直接从缓存读,十一分钟变成两分钟。
这套东西没有什么高深技术,全是常识加耐心。真正让它有用的,是把「自动删除」这个诱人的按钮拿掉。
资源库 » 用一段一百行的脚本清理重复文件:我踩过的三个坑