我给自己写了一个本地重复文件查找器:先找准,再决定删不删
目录
- 缘起:硬盘空间到底被谁吃掉了
- 需求拆解:找重复文件不等于自动删除
- 技术选型:标准库已经足够
- 核心实现一:用文件大小缩小范围
- 核心实现二:分段哈希与完整确认
- UI 定制:把风险放在操作之前
- 三个扫描文件时容易忽略的坑
- 结尾:整理文件最重要的是可确认
1. 缘起:硬盘空间到底被谁吃掉了
我的硬盘总是在不知不觉中变满。真正占空间的往往不是正在使用的项目,而是散落在不同目录里的副本:下载过两次的安装包、从手机重复导出的照片、改过目录名后又完整复制一份的项目备份。
手动寻找重复文件几乎不现实:
- 文件名相同,不代表内容相同;
- 文件名不同,也可能是同一个文件;
- 照片和视频数量太多,不可能逐个打开比较;
- 一旦跨越多个目录,人很难记住哪个版本已经看过。
我试过一些重复文件清理软件,但“扫描完成后一键删除”的设计反而让我不放心。对我来说,工具可以帮忙找,最终删不删必须由人确认。
因此我给这个工具定下了第一条原则:只判断内容是否相同,不根据文件名猜测;默认只展示结果,不自动删除任何文件。
2. 需求拆解:找重复文件不等于自动删除
把需求写成可检查的清单后,工具的边界就清楚了:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 找出真正相同的文件 | 最终使用完整 SHA-256 校验 |
| 扫描别太慢 | 先按大小分组,再做分段哈希 |
| 大文件别把内存撑满 | 流式读取,每次只读固定大小数据块 |
| 看懂重复在哪里 | 按组展示路径、大小和修改时间 |
| 别误删 | 默认无删除按钮,只支持打开所在目录 |
| 下次继续看 | 可导出 JSON 或 CSV 扫描报告 |
我没有加入“相似图片识别”。两张压缩程度不同但画面相同的照片,需要感知哈希或图像模型判断,这和“字节完全相同”是两类问题。把它硬塞进第一个版本,只会让结果变得难以解释。
心得:文件整理工具必须先定义“重复”的含义。我的定义很简单——文件内容逐字节一致。
3. 技术选型:标准库已经足够
工具最终使用 Python 编写,主要能力全部来自标准库:
| 能力 | 方案 |
|---|---|
| 遍历目录 | pathlib.Path.rglob() |
| 读取文件信息 | Path.stat() |
| 内容摘要 | hashlib.sha256() |
| 并发计算 | concurrent.futures |
| 导出报告 | json 与 csv |
| 简单界面 | tkinter |
这里没有数据库。一次扫描的结果本质上是一组路径和摘要,保存在内存中即可;需要留档时再导出成普通文件。
我也没有一开始就做多进程。重复文件扫描通常受磁盘读取速度限制,同时让太多任务读取同一块机械硬盘,反而会因为频繁寻道变慢。默认使用少量线程已经足够,线程数可以根据存储设备再调整。
心得:性能优化要先判断瓶颈在哪里。磁盘任务不是线程越多越快。
4. 核心实现一:用文件大小缩小范围
如果两个文件内容完全相同,它们的字节数一定相同。因此第一步根本不用读取内容,只需要按文件大小分组。
from collections import defaultdict
from pathlib import Path
def group_by_size(root: Path):
groups = defaultdict(list)
for path in root.rglob("*"):
if not path.is_file():
continue
try:
groups[path.stat().st_size].append(path)
except OSError:
continue
return {
size: paths
for size, paths in groups.items()
if len(paths) > 1
}
这一步过滤效果非常明显。一个包含几万个文件的目录里,真正大小完全一致的文件通常只占一小部分。只有进入候选组的文件,才值得继续读取内容。
我还加了两个简单过滤条件:
- 跳过大小为 0 的空文件,因为它们内容天然相同,却没有清理价值;
- 允许设置最小文件大小,例如只检查 1 MB 以上文件,把时间花在真正占空间的对象上。
扫描时界面会分别显示“已发现文件数”和“待校验候选数”。这样用户能知道程序正在缩小范围,而不是误以为它卡在某个目录。
心得:能用元数据排除的对象,就不要急着读取文件内容。
5. 核心实现二:分段哈希与完整确认
大小相同只是候选条件。为了减少不必要的完整读取,我先计算文件头部和尾部的分段哈希,再对仍然一致的候选计算完整 SHA-256。
import hashlib
SAMPLE_SIZE = 64 * 1024
def sample_hash(path):
size = path.stat().st_size
digest = hashlib.sha256()
with path.open("rb") as file:
digest.update(file.read(SAMPLE_SIZE))
if size > SAMPLE_SIZE:
file.seek(max(0, size - SAMPLE_SIZE))
digest.update(file.read(SAMPLE_SIZE))
return digest.hexdigest()
完整哈希采用流式读取,避免把大文件一次性放进内存:
def full_hash(path, chunk_size=1024 * 1024):
digest = hashlib.sha256()
with path.open("rb") as file:
while chunk := file.read(chunk_size):
digest.update(chunk)
return digest.hexdigest()
最终的处理顺序是:
- 按文件大小分组;
- 对同大小文件计算分段哈希;
- 对分段哈希相同的文件计算完整哈希;
- 只有完整哈希一致,才归入同一个重复组。
分段哈希不是最终证据,它只是又一层便宜的过滤。真正显示给用户的重复组,必须经过完整摘要确认。
心得:高效扫描不是降低准确性,而是把昂贵校验留给最有可能重复的文件。
6. UI 定制:把风险放在操作之前
界面被我分成三个区域:顶部选择扫描目录和最小文件大小,中间显示进度,底部按组展示重复结果。
每个重复组会显示:
- 文件数量和单个文件大小;
- 这组副本理论上可释放的空间;
- 每个文件的完整路径、修改时间和只读状态;
- “打开文件”和“打开所在目录”两个安全操作。
我故意没有在第一版加入“一键删除重复项”。扫描结果可能包含用户主动保留的不同目录备份,也可能包含某些软件依赖的副本。字节相同只能证明内容相同,不能证明其中一个没有用途。
导出的报告会保存扫描时间、根目录、文件大小和完整哈希。用户可以先查看报告,再使用熟悉的文件管理方式处理。工具本身不替用户做不可逆决定。
心得:越接近删除操作,界面越应该提供证据,而不是提供捷径。
7. 三个扫描文件时容易忽略的坑
坑一:扫描过程中,文件可能正在变化。 日志、数据库和下载中的文件会边扫描边增长。如果分段哈希和完整哈希之间文件大小发生变化,本次结果就不再可信。我的做法是在完整校验前后各读取一次文件信息,只要大小或修改时间变化,就把该文件标记为“扫描期间已变化”并跳过。
坑二:符号链接可能造成重复遍历。 目录链接有机会把扫描带回已经访问过的位置,甚至形成循环。工具默认不跟随目录符号链接,并记录跳过数量。是否跟随必须由用户明确开启。
坑三:访问失败不等于文件不存在。 权限不足、文件被占用或路径过长都可能让读取失败。程序不应该悄悄把这些对象当作“非重复”,而要在扫描结束后列出失败路径和原因,让结果的覆盖范围清清楚楚。
另外,哈希碰撞在理论上存在。对于普通本地整理,SHA-256 已经足够可靠;如果处理的是必须逐字节证明一致的高价值数据,还可以在哈希相同后再做二进制逐块比较。
8. 结尾:整理文件最重要的是可确认
写完这个工具后,我没有立刻多出大量可用空间,反而先看清了自己的文件习惯:下载目录里堆着重复安装包,照片在不同备份目录中交叉出现,旧项目压缩包又被复制进了新归档。
重复文件查找器真正有价值的地方,不是替我按下删除键,而是把原本藏在硬盘各处的关系整理出来:
- 哪些文件内容完全相同;
- 它们分别位于什么位置;
- 每组副本占用了多少空间;
- 哪些路径因权限或变化没有完成校验。
有了这些信息,删除才是一个可以判断的动作,而不是一次赌博。
如果你也想写一个重复文件查找器,建议从“按大小分组 + 完整哈希”开始。数据量不大时,这已经足够。只有确认扫描确实慢,再增加分段哈希和有限并发,不要一开始就把流程设计得过于复杂。
文件整理的目标不是让目录看起来干净,而是让每一次处理都有依据、每一个结果都能解释。
- 点赞
- 收藏
- 关注作者
评论(0)