朴素方案的账

「按内容找重复」最直白的实现:对每个文件全量算一遍 SHA-256,哈希相同即重复。数学上站得住——除非碰上蓄意构造的碰撞,两个不同文件哈希相同的概率小于宇宙原子数分之一。但工程上算不过来:一万个文件平均 20MB,就是 200GB 的磁盘读取。而典型的重复场景(连拍的相似照片、反复下载的安装包、备份套备份的目录)里,99% 的文件根本没有「同内容竞争者」——算的哈希绝大部分是白算。

解法是漏斗:每一级用最便宜的特征淘汰最多的文件,昂贵的全量哈希只留给最后一小撮。

第一级:大小分桶,零 IO

字节数不同的两个文件不可能内容相同——这是不读一个字节就能用的判据。把所有文件按 size 塞进哈希表,只保留桶内 ≥2 个的组。真实目录里这一步就能砍掉 80%–95% 的文件:文件系统里同名不同大小、单份存在的文件占绝对多数。淘汰是零成本的(size 在选文件时就有),收益却最大——漏斗设计的黄金第一级。

唯一要留神的是「大小相同」不等于「内容相同」:稀疏文件、全零文件天然同尺寸。所以第一级只能淘汰、不能确认。

第二级:首尾各 64KB 的快哈希

同大小的候选组内,下一步读多少?全读还是采样?关键洞察是文件的同与不同分布极不均匀:头部集中了格式签名与元数据(JPEG 的 FFD8FF、MP4 的 ftyp box),尾部集中了索引与收尾结构(ZIP 的中央目录、ID3v1 的 TAG 尾)。两头各读 64KB 算两遍独立的 SHA-256,拼接成复合键——同大小且首尾 128KB 全同的文件,是重复的先验概率已经极高;而任何一头不同,直接出局。

为什么「首+尾」优于「连续读前 128KB」?一类顽固的伪重复恰好坑在只读头部:同系列照片连拍,EXIF 头部高度雷同;同模板导出的 PDF,前几十 KB 的字体与对象流如出一辙。尾部通常是内容的收尾,差异密度更高。64KB 这个量级则兼顾两头:大到覆盖绝大多数头部结构,小到一个 50GB 的视频也只读它万分之一。

这一级的诚实边界在读取语义上:随机访问文件的任意区间(Blob.slice / File.slice),浏览器不必把整个文件读入内存——这也是整条流水线能在网页里跑起来的前提,内存占用与文件总数和分段大小相关,与文件总大小无关。

第三级:全量 SHA-256,以及不装确定的勇气

快哈希仍然相同的组,才做全量 SHA-256 确认。此时候选组通常只剩几十个文件,全量读取的总开销可控。WebCrypto 的 crypto.subtle.digest('SHA-256', bytes) 由系统原生实现,比任何 JS 手写的哈希都快一个量级。

真正的工程分寸在第三级的例外上:超过 100MB 的文件跳过全量校验,按「大小 + 快哈希」标记为「疑似重复(未全量校验)」,而不是混在确定结果里充数。理论上同大小且首尾 128KB 相同的大文件仍可能中段不同(比如同一蓝光原盘两种压制的中段差异),把「几乎确定」说成「确定」是查重工具的诚信红线——宁可多标一个状态,不给用户删错文件的底气。处理结果按组展示:每组一个 8 位短哈希指纹、文件数、浪费字节数((份数−1)×单份大小),并给出「保留最新一份」的建议(按修改时间排序),清理清单可导出复核后再动手。

哈希不是校验和

顺带辨析一个常被混用的概念:CRC32 与 SHA-256 都是「内容指纹」,但设计目标不同。CRC32 是校验和,32 位、防意外损坏(传输误码),构造碰撞轻而易举——gzip 尾部那 4 字节就是它(参见DEFLATE 解剖);SHA-256 是密码学哈希,构造碰撞在算力上不可行。查重用 SHA-256 而非 CRC32,防的正是「两个不同文件恰好同 CRC」的尴尬。而判断「这堆文件是什么格式」,用的又是另一套「头部魔数比对」的手艺,文件取证那篇里拆过。

🔧 重复文件查找 把三级漏斗整个跑在浏览器里:选文件或整个文件夹,分组、快哈希、全量确认逐步推进,全程不上传。它的兄弟们——格式识别、加密压缩、目录树生成——都收在文件处理流水线专题里,本地完成,文件不出设备。