<-- 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    })

常见使用模式

  1. 代码分析工具: 遍历项目找出所有源代码文件
  2. 文件搜索: 按条件查找特定文件
  3. 目录统计: 计算目录大小、文件数量等
  4. 文件同步: 比较两个目录树的差异
  5. 清理工具: 找出并删除临时文件、缓存等

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给出的性能分析

🏆 处理阶段性能排名

  1. 方法3 (23.32ms) - 最快,早期过滤优势
  2. 方法1 (28.27ms) - 第二,并行过滤+处理
  3. 方法2 (165.92ms) - 最慢,异步开销大

📊 关键性能指标对比

方法收集阶段处理阶段内存使用缓存效率
方法13.68s (95万文件)28.27ms中等
方法23.67s (95万文件)165.92ms
方法33.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的优化亮点

  1. 内存效率提升:

    内存减少 = (957055 - 3398) / 957055 = 99.64%
    
  2. 缓存友好性:

    • 更小的工作集适合CPU缓存
    • 减少cache miss和内存带宽压力
  3. 简化并行模式:

    • 无需并行过滤,只做并行处理
    • 减少并行计算中的分支预测失败

🎯 实际应用建议

场景推荐方法原因
大规模文件分析方法3早期过滤,内存高效
实时文件监控方法1简单直接,性能足够
网络服务集成方法2可与其他异步操作协作
内存受限环境方法3显著减少内存使用

🚀 进一步优化思路

  1. SIMD加速: 文本处理部分可用SIMD指令优化
  2. 内存池: 预分配RustFileInfo对象池
  3. 并行I/O: 使用rayon的并行文件读取
  4. 压缩存储: 对路径字符串进行去重压缩

很无聊的结论

  1. 早期过滤是关键: 方法3通过在遍历阶段过滤,实现了最佳性能
  2. 选择合适的并行模型: CPU密集型任务优选rayon,I/O密集型选tokio
  3. 内存局部性很重要: 减少无用数据的内存占用能显著提升性能
  4. 不要过度工程化: 简单的数据并行往往比复杂的异步方案更高效

核心启示: 在文件系统操作中,“过滤后处理"比"处理时过滤"更高效,体现了算法设计中"减少问题规模"的重要性。

源代码:

  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


GitHub