用一段一百行的脚本清理重复文件:我踩过的三个坑

硬盘用了六年,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,记录「路径 + 修改时间 + 大小 → 哈希」,第二次跑直接从缓存读,十一分钟变成两分钟。

这套东西没有什么高深技术,全是常识加耐心。真正让它有用的,是把「自动删除」这个诱人的按钮拿掉。

分享目的仅供大家学习和交流,您必须在下载后24小时内删除! 2. 不得使用于非法商业用途,不得违反国家法律。否则后果自负! 3. 本站提供的源码、模板、插件、软件、等等其他资源,都不包含技术服务请大家谅解!内容投诉
资源库 » 用一段一百行的脚本清理重复文件:我踩过的三个坑