用Rust手写100行高性能文件去重工具,比Python快10倍!

大家好,今天给大家分享一个非常实用的 Rust 实战小项目。不知道你有没有遇到过这种情况:电脑用久了,下载文件夹、照片库里面堆了一大堆重复的文件,占了好几十 G 的空间,想清理一下,找了半天工具要么带广告,要么跑起来慢的要死。之前我用 Python 写过一个简单的去重脚本,小目录还行,一碰到那种几万个文件、几十 G 的大目录,跑起来要等半天,多线程还因为 GIL 发挥不了作用。后来我用 Rust 重写了一个,只用了 100 行代码,不仅速度快了 10 倍,还能编译成单文件,Windows、Mac、Linux 都能跑,不用装任何依赖,今天就把这个实现分享给大家。

核心思路:分阶段过滤,把 IO 做到极致

很多人写文件去重,上来就遍历所有文件,挨个算全文件哈希,这其实非常浪费性能。尤其是大文件,读整个文件的 IO 成本太高了。

我们的思路是分阶段过滤,像筛子一样,一步一步把不可能重复的文件过滤掉,只对真正有嫌疑的文件做昂贵的哈希计算:

  1. 第一阶段:按文件大小分组 大小不一样的文件,内容绝对不可能一样!这一步就能过滤掉 90% 以上的文件,完全不用碰文件内容。

  2. 第二阶段:部分哈希过滤 对大小相同的文件,我们只读取前 4KB 的内容算哈希。如果连前 4KB 都不一样,那整个文件肯定不一样。这一步又能过滤掉大部分,而且不用读整个大文件,比如 10G 的视频,前 4KB 不对就直接跳过,省了 99.9% 的 IO。

  3. 第三阶段:全文件哈希确认 最后剩下的候选文件,我们再计算整个文件的哈希,确认是不是真的重复。这一步我们用多线程并行处理,把多核 CPU 跑满。

就靠这三步,我们把整个流程的 IO 和 CPU 开销降到了最低,速度直接拉满。

项目初始化:30 秒搭好环境

首先,我们用 Cargo 新建一个项目,然后加几个非常实用的依赖:

cargo new rust-dedup cd rust-dedup

然后编辑 Cargo.toml,加上这几个依赖:

[dependencies] walkdir = "2.4" # 递归遍历目录,比标准库好用太多 rayon = "1.10" # 无痛并行,一行代码就能把循环改成多线程 xxhash-rust = { version = "0.8", features = ["xxh3"] } # 最快的非加密哈希,比MD5快5倍 anyhow = "1.0" # 简单的错误处理,不用写一堆Result

这几个都是 Rust 生态里非常成熟的库,不用自己造轮子。

代码实现:100 行搞定核心功能

接下来就是核心代码了,我把每一步都加了注释,非常好懂。

第一步:遍历目录,按大小分组

首先我们遍历目标目录下的所有文件,把它们按文件大小存到一个哈希表里面,大小相同的文件放到同一个组里。

use anyhow::Result; use walkdir::WalkDir; use rayon::prelude::*; use xxhash_rust::xxh3::xxh3_64; use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::Path; fn main() -> Result<()> { // 从命令行参数获取目标目录 let args: Vec<String> = std::env::args().collect(); if args.len() < 2 { eprintln!("Usage: {} <directory>", args[0]); std::process::exit(1); } let root = Path::new(&args[1]); println!("正在扫描目录: {}", root.display()); // 第一阶段:按文件大小分组 let mut size_map: HashMap<u64, Vec<_>> = HashMap::new(); for entry in WalkDir::new(root) { let entry = entry?; if !entry.file_type().is_file() { continue; // 跳过目录和链接 } let meta = entry.metadata()?; let size = meta.len(); size_map.entry(size).or_default().push(entry.path().to_path_buf()); } // 过滤掉只有单个文件的组,这些肯定没有重复 let candidates: Vec<_> = size_map .into_iter() .filter(|(_, files)| files.len() >= 2) .flat_map(|(_, files)| files) .collect(); println!("第一阶段完成,找到{}个候选文件", candidates.len()); // ... 后面的步骤 }

第二步:部分哈希,快速过滤

接下来,我们对这些候选文件,计算前 4KB 的哈希,再分组,过滤掉单个的。

// 第二阶段:按前4KB的哈希分组 let mut partial_hash_map: HashMap<u64, Vec<_>> = HashMap::new(); const PARTIAL_SIZE: usize = 4096; // 读前4KB for path in candidates { let mut file = File::open(&path)?; let mut buffer = [0; PARTIAL_SIZE]; let n = file.read(&mut buffer)?; // 计算前4KB的xxh3哈希 let hash = xxh3_64(&buffer[..n]); partial_hash_map.entry(hash).or_default().push(path); } // 再次过滤掉单个的 let candidates: Vec<_> = partial_hash_map .into_iter() .filter(|(_, files)| files.len() >= 2) .flat_map(|(_, files)| files) .collect(); println!("第二阶段完成,剩下{}个待确认文件", candidates.len());

第三步:全文件哈希,并行确认

最后,剩下的这些文件,我们计算整个文件的哈希,这一步我们用 Rayon 的并行迭代器,自动把任务分到多个线程,跑满 CPU。

// 第三阶段:计算全文件哈希,并行处理 let full_hash_map: HashMap<u64, Vec<_>> = candidates .into_par_iter() // 这里就是并行的关键!一行代码自动多线程 .filter_map(|path| -> Option<(u64, std::path::PathBuf)> { // 计算整个文件的哈希 let mut file = match File::open(&path) { Ok(f) => f, Err(_) => return None, }; let mut hasher = xxhash_rust::xxh3::Xxh3::new(); let mut buffer = [0; 8192]; loop { let n = match file.read(&mut buffer) { Ok(n) => n, Err(_) => break, }; if n == 0 { break; } hasher.update(&buffer[..n]); } Some((hasher.digest(), path)) }) .fold(HashMap::new(), |mut map, (hash, path)| { map.entry(hash).or_default().push(path); map }) .reduce(HashMap::new, |mut a, b| { for (k, v) in b { a.entry(k).or_default().extend(v); } a });

最后:输出结果

最后,我们把重复的文件组打印出来,用户就可以看到哪些文件是重复的了。

// 输出结果 println!("\n找到的重复文件组:"); let mut dup_count = 0; for (hash, files) in full_hash_map { if files.len() >= 2 { println!("\n--- 重复组 (哈希: {:x}) ---", hash); for path in files { println!(" {}", path.display()); dup_count += 1; } } } println!("\n扫描完成,共找到{}个重复文件", dup_count); Ok(()) }

完整代码

整个代码加起来也就 100 行左右,完整的代码在这里,你可以直接复制过去用:

use anyhow::Result; use walkdir::WalkDir; use rayon::prelude::*; use xxhash_rust::xxh3::{xxh3_64, Xxh3}; use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::Path; fn main() -> Result<()> { let args: Vec<String> = std::env::args().collect(); if args.len() < 2 { eprintln!("Usage: {} <directory>", args[0]); std::process::exit(1); } let root = Path::new(&args[1]); println!("正在扫描目录: {}", root.display()); // 第一阶段:按文件大小分组 let mut size_map: HashMap<u64, Vec<_>> = HashMap::new(); for entry in WalkDir::new(root) { let entry = entry?; if !entry.file_type().is_file() { continue; } let meta = entry.metadata()?; let size = meta.len(); size_map.entry(size).or_default().push(entry.path().to_path_buf()); } let candidates: Vec<_> = size_map .into_iter() .filter(|(_, files)| files.len() >= 2) .flat_map(|(_, files)| files) .collect(); println!("第一阶段完成,找到{}个候选文件", candidates.len()); // 第二阶段:按前4KB哈希分组 let mut partial_hash_map: HashMap<u64, Vec<_>> = HashMap::new(); const PARTIAL_SIZE: usize = 4096; for path in candidates { let mut file = File::open(&path)?; let mut buffer = [0; PARTIAL_SIZE]; let n = file.read(&mut buffer)?; let hash = xxh3_64(&buffer[..n]); partial_hash_map.entry(hash).or_default().push(path); } let candidates: Vec<_> = partial_hash_map .into_iter() .filter(|(_, files)| files.len() >= 2) .flat_map(|(_, files)| files) .collect(); println!("第二阶段完成,剩下{}个待确认文件", candidates.len()); // 第三阶段:全文件哈希,并行处理 let full_hash_map: HashMap<u64, Vec<_>> = candidates .into_par_iter() .filter_map(|path| -> Option<(u64, std::path::PathBuf)> { let mut file = File::open(&path).ok()?; let mut hasher = Xxh3::new(); let mut buffer = [0; 8192]; loop { let n = file.read(&mut buffer).ok()?; if n == 0 { break; } hasher.update(&buffer[..n]); } Some((hasher.digest(), path)) }) .fold(HashMap::new(), |mut map, (hash, path)| { map.entry(hash).or_default().push(path); map }) .reduce(HashMap::new(), |mut a, b| { for (k, v) in b { a.entry(k).or_default().extend(v); } a }); // 输出结果 println!("\n找到的重复文件组:"); let mut dup_count = 0; for (hash, files) in full_hash_map { if files.len() >= 2 { println!("\n--- 重复组 (哈希: {:x}) ---", hash); for path in files { println!(" {}", path.display()); dup_count += 1; } } } println!("\n扫描完成,共找到{}个重复文件", dup_count); Ok(()) }

性能测试:真的快了 10 倍?

我拿我自己的下载目录做了个测试,里面有 12000 多个文件,总大小 48GB,对比速度可以看到,在大目录的场景下,Rust 的版本只用了 4 秒就跑完了,而我之前的 Python 单线程版本,足足跑了 45 秒,快了 10 倍还多!

而且最爽的是,编译之后它就是一个不到 2MB 的单文件,你可以直接放到 PATH 里,随时调用,不用装 Python,不用装依赖,Windows、Mac、Linux 都能跑。

扩展与总结

这个小工具其实还有很多可以扩展的地方,比如:

  • 加上删除重复文件的功能

  • 支持把重复文件替换成硬链接,节省空间

  • 加上 GUI 界面,用 Tauri 做个跨平台的桌面应用

  • 支持忽略特定的文件或者目录

其实这就是 Rust 最吸引人的地方:用很少的代码,就能写出性能媲美 C++,但是比 Python 还安全好用的工具

不用再担心内存泄漏,不用再担心数据竞争,写并行代码就跟写串行代码一样简单,编译完直接到处跑,这才是真正的生产力工具。

如果你也有那种小工具的需求,不妨试试用 Rust 写一个,你会打开新世界的大门。

更多推荐