<-- Home
|--rust
Walking folders in Rust中文件遍历方法
本文通过实际代码演示和性能测试,对比了Rust中三种不同的并行文件遍历方法,分析了它们在处理大规模文件系统时的性能表现。
WalkDir库详解
什么是WalkDir?
walkdir
是Rust生态系统中最流行的文件系统遍历库,提供了高效、安全的目录树遍历功能。它的设计理念是简单易用,同时提供强大的定制选项。
核心特性
- 跨平台兼容: 支持Windows、Linux、macOS等主流操作系统
- 符号链接处理: 智能处理符号链接,避免无限循环
- 深度控制: 可限制遍历深度,避免过深的目录结构
- 错误处理: 优雅处理权限错误和损坏的文件系统条目
- 内存高效: 迭代器模式,不会一次性加载所有文件到内存
- 排序支持: 可按文件名排序遍历
- 过滤功能: 支持自定义过滤条件
基本使用示例
导入库:
1use walkdir::WalkDir;
1. 最简单的遍历
1fn simple_walk() {
2 for entry in WalkDir::new(".") {
3 match entry {
4 Ok(entry) => println!("{}", entry.path().display()),
5 Err(e) => eprintln!("Error: {}", e),
6 }
7 }
8}
2. 限制深度遍历
1fn limited_depth_walk() {
2 for entry in WalkDir::new(".").max_depth(2) {
3 if let Ok(entry) = entry {
4 println!("{}[depth: {}] {}",
5 " ".repeat(entry.depth()),
6 entry.depth(),
7 entry.file_name().to_string_lossy()
8 );
9 }
10 }
11}
3. 过滤特定文件类型
1fn filter_rust_files() {
2 for entry in WalkDir::new(".")
3 .into_iter()
4 .filter_map(|e| e.ok())
5 .filter(|e| {
6 e.path().extension()
7 .and_then(|ext| ext.to_str())
8 .map(|ext| ext == "rs")
9 .unwrap_or(false)
10 })
11 {
12 println!("Rust file: {}", entry.path().display());
13 }
14}
4. 高级配置示例
1fn advanced_walk() {
2 let walker = WalkDir::new(".")
3 .max_depth(5) // 最大深度5层
4 .follow_links(false) // 不跟随符号链接
5 .sort_by_file_name() // 按文件名排序
6 .into_iter();
7
8 for entry in walker {
9 match entry {
10 Ok(entry) => {
11 if entry.file_type().is_file() {
12 println!("📄 File: {}", entry.path().display());
13 } else if entry.file_type().is_dir() {
14 println!("📁 Dir: {}", entry.path().display());
15 }
16 }
17 Err(err) => {
18 // 处理权限错误等问题
19 if let Some(path) = err.path() {
20 eprintln!("⚠️ Error accessing {}: {}", path.display(), err);
21 }
22 }
23 }
24 }
25}
5. 自定义过滤器
1use walkdir::{WalkDir, DirEntry};
2
3fn is_hidden(entry: &DirEntry) -> bool {
4 entry.file_name()
5 .to_str()
6 .map(|s| s.starts_with('.'))
7 .unwrap_or(false)
8}
9
10fn skip_hidden_files() {
11 let walker = WalkDir::new(".")
12 .into_iter()
13 .filter_entry(|e| !is_hidden(e)); // 跳过隐藏文件
14
15 for entry in walker {
16 if let Ok(entry) = entry {
17 println!("{}", entry.path().display());
18 }
19 }
20}
性能优化技巧
1. 早期过滤
1// ❌ 低效:收集后过滤
2let all_entries: Vec<_> = WalkDir::new(".").into_iter().collect();
3let rust_files: Vec<_> = all_entries.into_iter()
4 .filter_map(|e| e.ok())
5 .filter(|e| is_rust_file(e))
6 .collect();
7
8// ✅ 高效:遍历时过滤
9let rust_files: Vec<_> = WalkDir::new(".")
10 .into_iter()
11 .filter_map(|e| e.ok())
12 .filter(|e| is_rust_file(e))
13 .collect();
2. 合理设置深度限制
1// 避免遍历过深的目录结构
2WalkDir::new(".").max_depth(10) // 根据实际需求设置
3. 使用 filter_entry 跳过整个子树
1WalkDir::new(".")
2 .into_iter()
3 .filter_entry(|e| {
4 // 跳过 target 和 node_modules 目录
5 !e.file_name().to_str().map(|s| s == "target" || s == "node_modules").unwrap_or(false)
6 })
常见使用模式
- 代码分析工具: 遍历项目找出所有源代码文件
- 文件搜索: 按条件查找特定文件
- 目录统计: 计算目录大小、文件数量等
- 文件同步: 比较两个目录树的差异
- 清理工具: 找出并删除临时文件、缓存等
WalkDir的设计使得这些常见任务都能简洁高效地实现,是Rust文件系统操作的基础工具。
实际应用抛砖引玉
通过上面的例子,我们整一个列出所有Rust文件,并进行基本统计的程序。
首先,便利所有文件,这里前面也提到可以进行早期过滤,我们要比较后面的两种并行方式,所以把过滤的功能放在并行/异步中间处理,当然,我们也比较了早期过滤的情况。
首先是不过滤的代码:
1// 收集所有文件,然后并行过滤和处理
2let entries: Vec<_> = WalkDir::new(dir)
3 .into_iter()
4 .filter_map(|e| e.ok())
5 .collect();
测试环境和数据规模
- 测试目录: D盘根目录(Windows系统)
- 文件总数: 957,055个文件和目录
- Rust文件数: 3,398个
- 总代码行数: 1,515,715行
- Rust文件总大小: 59,521,881字节(约56.8MB)
- 编译模式: Release模式(–release)
方法1:同步并行遍历 (walkdir + rayon)
1// 收集所有文件,然后并行过滤和处理
2let rust_file_info: Vec<_> = entries
3 .par_iter()
4 .filter(|entry| is_rust_file(entry)) // 并行过滤
5 .map(|entry| get_rust_file_info_sync(entry)) // 并行处理
6 .collect();
- 文件遍历: 3.684s (收集957,055个文件)
- 并行处理: 28.27ms ⚡
- 总耗时: ~3.71s
方法2:异步并行遍历 (tokio)
1// 收集所有文件,过滤后异步并行处理
2let tasks: Vec<_> = entries
3 .into_iter()
4 .filter(|entry| is_rust_file(entry)) // 串行过滤
5 .map(|entry| {
6 let path = entry.path().to_path_buf();
7 tokio::spawn(async move {
8 get_rust_file_info_async(path).await
9 })
10 })
11 .collect();
- 文件遍历: 3.672s (收集957,055个文件)
- 异步处理: 165.92ms 🐌
- 总耗时: ~3.84s
方法3:早期过滤并行处理 (walkdir + filter + rayon)
1// 在遍历阶段就过滤,只收集需要的文件
2let rust_files: Vec<_> = WalkDir::new(target_dir)
3 .into_iter()
4 .filter_map(|e| e.ok())
5 .filter(|entry| is_rust_file(entry)) // 早期过滤
6 .collect();
7
8let file_info: Vec<_> = rust_files
9 .par_iter()
10 .map(|entry| get_rust_file_info_sync(entry)) // 纯并行处理
11 .collect();
- 文件遍历+过滤: 3.67s (只收集3,398个Rust文件)
- 并行处理: 23.32ms 🚀
- 总耗时: ~3.69s
AI给出的性能分析
🏆 处理阶段性能排名
- 方法3 (23.32ms) - 最快,早期过滤优势
- 方法1 (28.27ms) - 第二,并行过滤+处理
- 方法2 (165.92ms) - 最慢,异步开销大
📊 关键性能指标对比
方法 | 收集阶段 | 处理阶段 | 内存使用 | 缓存效率 |
---|---|---|---|---|
方法1 | 3.68s (95万文件) | 28.27ms | 高 | 中等 |
方法2 | 3.67s (95万文件) | 165.92ms | 高 | 差 |
方法3 | 3.67s (3398文件) | 23.32ms | 低 | 优秀 |
🔍 性能差异深度解析
为什么方法3最快?
早期过滤优势:
- 只在内存中保存3,398个有用对象,而不是957,055个
- 减少了约99.6%的内存分配
- 更好的缓存局部性和内存访问模式
数据流对比:
方法1/2: 95万文件 → 内存 → 并行处理(过滤+读取)
方法3: 95万文件 → 过滤 → 3398文件 → 内存 → 并行处理(仅读取)
为什么tokio表现最差?
异步开销分析:
- 创建3,398个
tokio::spawn
任务的开销 - 异步调度器的上下文切换成本
Future
状态机的内存和CPU开销
任务开销计算:
165.92ms ÷ 3398 ≈ 0.049ms/文件 (tokio)
23.32ms ÷ 3398 ≈ 0.007ms/文件 (rayon)
tokio每文件开销是rayon的7倍!
CPU密集型 vs I/O密集型
本测试的工作负载特征:
- 文件读取: 相对较小的文件,系统缓存命中率高
- 文本处理: 行数统计,CPU计算高于I/O
- 内存访问: 频繁的小对象分配和访问
结论: 这是典型的CPU密集型工作负载,rayon天然优势明显。
💡 优化策略分析
方法3的优化亮点
内存效率提升:
内存减少 = (957055 - 3398) / 957055 = 99.64%
缓存友好性:
- 更小的工作集适合CPU缓存
- 减少cache miss和内存带宽压力
简化并行模式:
- 无需并行过滤,只做并行处理
- 减少并行计算中的分支预测失败
🎯 实际应用建议
场景 | 推荐方法 | 原因 |
---|---|---|
大规模文件分析 | 方法3 | 早期过滤,内存高效 |
实时文件监控 | 方法1 | 简单直接,性能足够 |
网络服务集成 | 方法2 | 可与其他异步操作协作 |
内存受限环境 | 方法3 | 显著减少内存使用 |
🚀 进一步优化思路
- SIMD加速: 文本处理部分可用SIMD指令优化
- 内存池: 预分配
RustFileInfo
对象池 - 并行I/O: 使用
rayon
的并行文件读取 - 压缩存储: 对路径字符串进行去重压缩
很无聊的结论
- 早期过滤是关键: 方法3通过在遍历阶段过滤,实现了最佳性能
- 选择合适的并行模型: CPU密集型任务优选rayon,I/O密集型选tokio
- 内存局部性很重要: 减少无用数据的内存占用能显著提升性能
- 不要过度工程化: 简单的数据并行往往比复杂的异步方案更高效
核心启示: 在文件系统操作中,“过滤后处理"比"处理时过滤"更高效,体现了算法设计中"减少问题规模"的重要性。
源代码:
1use rayon::prelude::*;
2use std::time::Instant;
3use walkdir::WalkDir;
4
5#[derive(Debug, Clone)]
6struct RustFileInfo {
7 path: std::path::PathBuf,
8 lines: usize,
9 size: u64,
10}
11
12/// 检查是否为Rust文件
13fn is_rust_file(entry: &walkdir::DirEntry) -> bool {
14 entry
15 .path()
16 .extension()
17 .and_then(|ext| ext.to_str())
18 .map(|ext| ext == "rs")
19 .unwrap_or(false)
20}
21
22/// 从文件内容和大小创建RustFileInfo(核心逻辑)
23fn create_rust_file_info(
24 path: std::path::PathBuf,
25 content: String,
26 file_size: u64,
27) -> RustFileInfo {
28 let line_count = content.lines().count();
29
30 RustFileInfo {
31 path,
32 lines: line_count,
33 size: file_size,
34 }
35}
36
37/// 同步获取Rust文件信息
38fn get_rust_file_info_sync(entry: &walkdir::DirEntry) -> RustFileInfo {
39 let path = entry.path();
40 let content = std::fs::read_to_string(path).unwrap_or_default();
41 let file_size = entry.metadata().map(|m| m.len()).unwrap_or(0);
42
43 create_rust_file_info(path.to_path_buf(), content, file_size)
44}
45
46/// 异步获取Rust文件信息
47async fn get_rust_file_info_async(path: std::path::PathBuf) -> RustFileInfo {
48 let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
49 let file_size = tokio::fs::metadata(&path)
50 .await
51 .map(|m| m.len())
52 .unwrap_or(0);
53
54 create_rust_file_info(path, content, file_size)
55}
56
57fn main() {
58 println!("=== Rust 高效并行文件遍历示例 ===\n");
59
60 // 使用正确的D盘根目录路径
61 let target_dir = "D:\\";
62
63 // 方法1: 使用 walkdir + rayon 进行并行处理
64 println!("方法1: 同步并行遍历 (walkdir + rayon)");
65 println!("遍历目录: {}", target_dir);
66 parallel_walk_sync(target_dir);
67
68 println!("\n{}\n", "=".repeat(50));
69
70 // 方法2: 使用 tokio 进行异步并行处理
71 println!("方法2: 异步并行遍历 (tokio)");
72 println!("遍历目录: {}", target_dir);
73 let rt = tokio::runtime::Runtime::new().unwrap();
74 rt.block_on(parallel_walk_async(target_dir));
75
76 println!("\n{}\n", "=".repeat(50));
77
78 // 方法3: 高级示例 - 查找特定类型文件
79 println!("方法3: 高级并行处理 - 查找所有Rust文件");
80 println!("遍历目录: {}", target_dir);
81 advanced_parallel_processing(target_dir);
82
83 // 方法4:更加实用的方式
84 println!("方法4:更加实用的方式");
85 println!("遍历目录: {}", target_dir);
86 seasoned_walk(target_dir);
87}
88
89fn seasoned_walk(dir: &str){
90 let start = Instant::now();
91 let rust_file_info: Vec<_> = WalkDir::new(dir)
92 .into_iter()
93 .filter_map(|e| e.ok())
94 .filter(|entry| is_rust_file(entry))
95 .collect::<Vec<_>>()
96 .par_iter()
97 .map(|entry| get_rust_file_info_sync(entry))
98 .collect();
99
100 println!("收集到 {} 个文件/目录", rust_file_info.len());
101
102 // 统计结果
103 let total_lines: usize = rust_file_info.iter().map(|f| f.lines).sum();
104 let total_size: u64 = rust_file_info.iter().map(|f| f.size).sum();
105
106 println!("处理完成:");
107 println!(" Rust文件数: {}", rust_file_info.len());
108 println!(" 总代码行数: {}", total_lines);
109 println!(" 总大小: {} bytes", total_size);
110 println!("用时: {:?}", start.elapsed());
111
112 // 显示最大的10个Rust文件
113 println!("\n最大的10个Rust文件:");
114 let mut large_files: Vec<_> = rust_file_info.iter().collect();
115 large_files.sort_by(|a, b| b.size.cmp(&a.size));
116
117 large_files.iter().take(10).for_each(|f| {
118 let path_str = f.path.to_string_lossy();
119 println!(" 🦀 {} ({} bytes, {} 行)", path_str, f.size, f.lines);
120 });
121
122}
123
124/// 使用 walkdir + rayon 进行同步并行遍历
125/// 这是最常用和高效的方式
126fn parallel_walk_sync(dir: &str) {
127 let start = Instant::now();
128 // 使用 WalkDir 收集所有路径,限制深度避免遍历过深
129 let entries: Vec<_> = WalkDir::new(dir)
130 .into_iter()
131 .filter_map(|e| e.ok())
132 .collect();
133
134 println!("收集到 {} 个文件/目录", entries.len());
135 println!("用时: {:?}", start.elapsed());
136
137 let start = Instant::now();
138 // 使用 rayon 并行处理:过滤Rust文件并获取信息
139 let rust_file_info: Vec<_> = entries
140 .par_iter()
141 .filter(|entry| is_rust_file(entry))
142 .map(|entry| get_rust_file_info_sync(entry))
143 .collect();
144
145 // 统计结果
146 let total_lines: usize = rust_file_info.iter().map(|f| f.lines).sum();
147 let total_size: u64 = rust_file_info.iter().map(|f| f.size).sum();
148
149 println!("处理完成:");
150 println!(" Rust文件数: {}", rust_file_info.len());
151 println!(" 总代码行数: {}", total_lines);
152 println!(" 总大小: {} bytes", total_size);
153 println!(" 用时: {:?}", start.elapsed());
154
155 // 显示最大的10个Rust文件
156 println!("\n最大的10个Rust文件:");
157 let mut large_files: Vec<_> = rust_file_info.iter().collect();
158 large_files.sort_by(|a, b| b.size.cmp(&a.size));
159
160 large_files.iter().take(10).for_each(|f| {
161 let path_str = f.path.to_string_lossy();
162 println!(" 🦀 {} ({} bytes, {} 行)", path_str, f.size, f.lines);
163 });
164}
165
166/// 使用 tokio 进行异步并行遍历
167/// 适用于 I/O 密集型操作
168async fn parallel_walk_async(dir: &str) {
169 let start = Instant::now();
170 // 首先同步收集所有路径(因为 walkdir 不是异步的),限制深度
171 let entries: Vec<_> = WalkDir::new(dir)
172 .into_iter()
173 .filter_map(|e| e.ok())
174 .collect();
175
176 println!("收集到 {} 个文件/目录", entries.len());
177 println!("用时: {:?}", start.elapsed());
178
179 let start = Instant::now();
180
181 // 使用 tokio 异步并行处理:过滤Rust文件并获取信息
182 let tasks: Vec<_> = entries
183 .into_iter()
184 .filter(|entry| is_rust_file(entry))
185 .map(|entry| {
186 let path = entry.path().to_path_buf();
187 tokio::spawn(async move { get_rust_file_info_async(path).await })
188 })
189 .collect();
190
191 // 等待所有任务完成
192 let rust_file_info: Vec<RustFileInfo> = futures::future::join_all(tasks)
193 .await
194 .into_iter()
195 .filter_map(|r| r.ok())
196 .collect();
197
198 // 统计结果
199 let total_lines: usize = rust_file_info.iter().map(|f| f.lines).sum();
200 let total_size: u64 = rust_file_info.iter().map(|f| f.size).sum();
201
202 println!("处理完成:");
203 println!(" Rust文件数: {}", rust_file_info.len());
204 println!(" 总代码行数: {}", total_lines);
205 println!(" 总大小: {} bytes", total_size);
206 println!(" 用时: {:?}", start.elapsed());
207
208 // 显示最大的10个Rust文件
209 println!("\n最大的10个Rust文件:");
210 let mut large_files: Vec<_> = rust_file_info.iter().collect();
211 large_files.sort_by(|a, b| b.size.cmp(&a.size));
212
213 large_files.iter().take(10).for_each(|f| {
214 let path_str = f.path.to_string_lossy();
215 println!(" 🦀 {} ({} bytes, {} 行)", path_str, f.size, f.lines);
216 });
217}
218
219/// 更高级的示例:并行处理特定类型的文件
220fn advanced_parallel_processing(target_dir: &str) {
221 let start = Instant::now();
222 // 在WalkDir阶段就过滤出Rust文件
223 let rust_files: Vec<_> = WalkDir::new(target_dir)
224 .into_iter()
225 .filter_map(|e| e.ok())
226 .filter(|entry| is_rust_file(entry))
227 .collect();
228
229 println!("收集到 {} 个 Rust 文件", rust_files.len());
230 println!("用时: {:?}", start.elapsed());
231
232 let start = Instant::now();
233
234 // 使用 rayon 并行处理Rust文件信息
235 let file_info: Vec<_> = rust_files
236 .par_iter()
237 .map(|entry| get_rust_file_info_sync(entry))
238 .collect();
239
240 let total_lines: usize = file_info.iter().map(|f| f.lines).sum();
241 let total_size: u64 = file_info.iter().map(|f| f.size).sum();
242
243 println!("处理完成:");
244 println!(" Rust文件数: {}", file_info.len());
245 println!(" 总代码行数: {}", total_lines);
246 println!(" 总大小: {} bytes", total_size);
247 println!(" 处理用时: {:?}", start.elapsed());
248
249 // 显示最大的10个Rust文件
250 println!("\n最大的10个Rust文件:");
251 let mut large_rust_files: Vec<_> = file_info.iter().collect();
252 large_rust_files.sort_by(|a, b| b.size.cmp(&a.size));
253
254 large_rust_files.iter().take(10).for_each(|f| {
255 let path_str = f.path.to_string_lossy();
256 println!(" 🦀 {} ({} bytes, {} 行)", path_str, f.size, f.lines);
257 });
258}
工程文件:
1[package]
2name = "walking-folders"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7walkdir = "2"
8rayon = "1.8"
9tokio = { version = "1.0", features = ["full"] }
10futures = "0.3"
文章标签
|-->rust |-->walkdir |-->parallel processing |-->tokio |-->rayon
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository