Rust: Rust 文件 I/O:读写文件与路径处理
最后更新:2026-08-26
文件 I/O 是程序与外部世界对话的"桥梁"——读文件就像"听"别人留下的信息,写文件就像"说"给未来的自己或其他人听。
如果把程序比作一个人,那变量和数据结构就是他的"短期记忆"(大脑),文件系统就是他的"长期记忆"(笔记本)。程序关闭后,短期记忆就消失了,但笔记本上的内容永远还在。文件 I/O 就是"读写笔记本"的能力——没有它,程序每次启动都是一张白纸。
1. 你将学到
std::fs::read_to_string一次性读取整个文件到字符串std::fs::write一次性将字符串写入文件File::open打开文件 +File::create创建/覆盖文件BufReader/BufWriter缓冲读写提高大文件性能Path与PathBuf路径操作(拼接、解析、判断)?运算符简化错误传播,优雅处理 I/O 错误
2. 日志分析器的故事
(1) 痛苦:手动翻日志
小林是公司的运维工程师,每天早上的第一件事就是手动检查服务器日志:
- 打开 10 个日志文件,每个文件有几千行
- 用 Ctrl+F 搜索 "ERROR",数一数有多少个
- 用 Excel 手动统计每种状态码(200、404、500)的数量
- 每次都要花 30 分钟,而且经常数错
- 老板说:"能自动生成一个报告吗?"——小林崩溃了
"如果能写一个程序自动读取日志、统计状态码、生成报告就好了……"
(2) Rust 文件 I/O 的方案
Rust 的标准库提供了完整的文件 I/O 工具,就像一台"日志分析流水线":
日志文件(服务器) → 原材料
File::open / read_to_string → 传送带送入
BufReader → 缓冲处理(流水线缓冲区)
逐行分析 → 质检工位
HashMap 统计状态码 → 分类统计
File::create / write → 打包输出报告
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
fn analyze_log(path: &str) -> std::io::Result<()> {
// 打开文件并创建缓冲读取器
let file = File::open(path)?;
let reader = BufReader::new(file);
// 统计每种状态码的出现次数
let mut counts: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
for line in reader.lines() {
let line = line?;
// 假设日志格式: "2026-07-03 10:30:45 GET /api/users 200"
if let Some(status) = line.split_whitespace().last() {
*counts.entry(status.to_string()).or_insert(0) += 1;
}
}
// 写入报告文件
let mut report = File::create("report.txt")?;
writeln!(report, "=== 状态码统计报告 ===")?;
for (code, count) in &counts {
writeln!(report, " {}: {}", code, count)?;
}
println!("报告已生成: report.txt");
Ok(())
}
Rust 的文件 I/O 设计遵循"零成本抽象"原则:
BufReader就像流水线上的缓冲区,减少底层系统调用次数;?运算符是"错误自动上报"——遇到任何 I/O 错误就立即返回,不需要手动写match。Path和PathBuf则像文件路径的"安全导航仪",自动处理不同操作系统的路径分隔符差异。
3. 核心概念
(1) 文件 I/O 体系
graph TB
A[Rust 文件 I/O] --> B[读取文件]
A --> C[写入文件]
A --> D[路径操作]
A --> E[错误处理]
B --> B1["std::fs::read_to_string"]
B --> B2["File::open + read_to_string"]
B --> B3["BufReader 逐行读取"]
C --> C1["std::fs::write"]
C --> C2["File::create + write_all"]
C --> C3["BufWriter 缓冲写入"]
D --> D1["Path(不可变切片)"]
D --> D2["PathBuf(可变字符串)"]
D --> D3["路径拼接 .join()"]
E --> E1["Result<T, io::Error>"]
E --> E2["? 运算符传播错误"]
E --> E3["match 精细处理"]
(2) 读取方式对比
| 方式 | 函数/类型 | 适用场景 | 内存占用 | 性能 |
|---|---|---|---|---|
| 一次性读取 | fs::read_to_string |
小文件(< 100MB) | 整个文件内容 | 最快(一次系统调用) |
| 手动读取 | File::read_to_string |
需要精细控制 | 整个文件内容 | 快 |
| 缓冲逐行 | BufReader::read_line |
大文件/日志 | 一行数据 | 中等(减少系统调用) |
| 缓冲迭代 | BufReader::lines() |
逐行处理 | 一行数据 | 方便(推荐) |
(3) 写入方式对比
| 方式 | 函数/类型 | 适用场景 | 特点 |
|---|---|---|---|
| 一次性写入 | fs::write |
小文件/简单内容 | 最简洁 |
| 创建文件 | File::create |
覆盖写入 | 文件存在则清空 |
| 追加写入 | OpenOptions::append |
追加日志 | 保留原有内容 |
| 缓冲写入 | BufWriter |
大量写入 | 减少系统调用 |
(4) 文件打开方式速查
| 方式 | 方法/类型 | 文件已存在时 | 文件不存在时 | 适用场景 |
|---|---|---|---|---|
| 只读打开 | File::open(path) |
正常打开 | 返回错误 | 读取已有文件 |
| 创建/覆盖 | File::create(path) |
清空内容 | 创建新文件 | 写入新文件 |
| 追加写入 | OpenOptions::new().append(true).open() |
追加到末尾 | 返回错误 | 日志追加 |
| 创建新文件 | OpenOptions::new().write(true).create_new(true).open() |
返回错误 | 创建新文件 | 避免覆盖 |
| 读写打开 | OpenOptions::new().read(true).write(true).open() |
正常打开 | 返回错误 | 修改已有文件 |
| 创建并读写 | OpenOptions::new().read(true).write(true).create(true).open() |
正常打开 | 创建新文件 | 配置文件读写 |
4. 文件 I/O 示例
▶ 示例 1:基础读写与错误处理(难度 ⭐)
// ============================================
// 基础文件读写:fs::read_to_string + fs::write
// 演示:创建临时文件、写入内容、读取内容、清理
// 场景:写一个简单的"便签"程序
// ============================================
use std::fs;
use std::path::Path;
fn main() -> std::io::Result<()> {
println!("=== 简易便签程序 ===\n");
// 定义临时文件路径(当前目录下的 temp_note.txt)
let file_path = "temp_note.txt";
// ---------- 写入文件 ----------
let content = "今天的待办事项:
1. 学习 Rust 文件 I/O
2. 完成日志分析器
3. 复习所有权和借用
Rust 学习笔记:
- 文件 I/O 使用 std::fs 模块
- BufReader 适合大文件读取
- ? 运算符简化错误处理";
fs::write(file_path, content)?;
println!("[写入] 便签已保存到: {}", file_path);
// 验证文件是否存在
if Path::new(file_path).exists() {
println!("[检查] 文件存在,大小: {} 字节", content.len());
}
// ---------- 读取文件 ----------
let read_content = fs::read_to_string(file_path)?;
println!("\n[读取] 便签内容:\n{}", read_content);
// ---------- 统计信息 ----------
let line_count = read_content.lines().count();
let word_count: usize = read_content
.split_whitespace()
.count();
let char_count = read_content.chars().count();
println!("--- 便签统计 ---");
println!(" 行数: {}", line_count);
println!(" 单词数: {}", word_count);
println!(" 字符数: {}", char_count);
// ---------- 清理临时文件 ----------
fs::remove_file(file_path)?;
println!("\n[清理] 临时文件已删除: {}", file_path);
// 确认文件已删除
assert!(!Path::new(file_path).exists(), "文件应该被删除");
println!("[确认] 文件已成功清理");
Ok(())
}
输出:
=== 简易便签程序 ===
[写入] 便签已保存到: temp_note.txt
[检查] 文件存在,大小: 87 字节
[读取] 便签内容:
今天的待办事项:
1. 学习 Rust 文件 I/O
2. 完成日志分析器
3. 复习所有权和借用
Rust 学习笔记:
- 文件 I/O 使用 std::fs 模块
- BufReader 适合大文件读取
- ? 运算符简化错误处理
--- 便签统计 ---
行数: 10
单词数: 24
字符数: 87
[清理] 临时文件已删除: temp_note.txt
[确认] 文件已成功清理
fs::write和fs::read_to_string是最简洁的文件读写方式——一次调用完成所有操作。?运算符将Result中的错误自动传播给调用者,如果出错就提前返回。Path::new(file_path).exists()检查文件是否存在。fs::remove_file删除文件。每次运行这个示例都会创建和删除同一个临时文件,确保可重复运行。
▶ 示例 2:BufReader 逐行读取日志(难度 ⭐⭐)
// ============================================
// BufReader 逐行读取 + 日志分析
// 演示:读取服务器日志,统计每种状态码数量
// 场景:运维工程师自动分析日志文件
// ============================================
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
fn main() -> std::io::Result<()> {
println!("=== 服务器日志分析器 ===\n");
// 第 1 步:创建模拟日志文件
let log_path = "temp_server.log";
create_sample_log(log_path)?;
println!("[生成] 模拟日志文件: {}", log_path);
// 第 2 步:分析日志
println!("[分析] 正在处理日志...");
let status_counts = analyze_log(log_path)?;
// 第 3 步:生成报告
let report_path = "temp_report.txt";
generate_report(&status_counts, report_path)?;
// 第 4 步:读取并打印报告
let report = std::fs::read_to_string(report_path)?;
println!("\n{}", report);
// 第 5 步:清理
std::fs::remove_file(log_path)?;
std::fs::remove_file(report_path)?;
println!("[清理] 临时文件已删除");
Ok(())
}
/// 创建模拟日志文件
fn create_sample_log(path: &str) -> std::io::Result<()> {
let mut file = File::create(path)?;
// 模拟一天的服务器日志
let log_entries = vec![
"2026-07-03 08:01:23 GET /api/users 200",
"2026-07-03 08:02:15 POST /api/login 200",
"2026-07-03 08:03:44 GET /api/products 404",
"2026-07-03 08:05:12 GET /api/users 200",
"2026-07-03 08:07:33 POST /api/order 500",
"2026-07-03 08:10:01 GET /api/products 200",
"2026-07-03 08:12:45 GET /api/users 304",
"2026-07-03 08:15:22 POST /api/login 401",
"2026-07-03 08:18:09 GET /api/products 200",
"2026-07-03 08:20:55 POST /api/order 500",
"2026-07-03 08:22:30 GET /api/users 200",
"2026-07-03 08:25:18 GET /api/products 404",
"2026-07-03 08:28:44 POST /api/login 200",
"2026-07-03 08:30:01 GET /api/search 200",
"2026-07-03 08:32:55 POST /api/order 200",
"2026-07-03 08:35:12 GET /api/users 200",
"2026-07-03 08:38:29 GET /api/products 304",
"2026-07-03 08:40:44 POST /api/login 401",
"2026-07-03 08:42:10 GET /api/search 500",
"2026-07-03 08:45:33 POST /api/order 200",
"2026-07-03 08:48:01 GET /api/users 200",
"2026-07-03 08:50:22 GET /api/products 200",
"2026-07-03 08:52:55 POST /api/login 200",
"2026-07-03 08:55:18 GET /api/search 404",
"2026-07-03 08:58:40 POST /api/order 500",
];
for entry in log_entries {
writeln!(file, "{}", entry)?;
}
Ok(())
}
/// 分析日志文件,统计每种 HTTP 状态码的出现次数
fn analyze_log(path: &str) -> std::io::Result<HashMap<String, u32>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut counts: HashMap<String, u32> = HashMap::new();
let mut total_lines = 0u32;
for line_result in reader.lines() {
let line = line_result?;
total_lines += 1;
// 提取状态码(行中最后一个字段)
// 日志格式: "2026-07-03 08:01:23 GET /api/users 200"
if let Some(status) = line.split_whitespace().last() {
*counts.entry(status.to_string()).or_insert(0) += 1;
}
}
println!("[统计] 总行数: {}", total_lines);
// 计算各状态码占比
let total: f64 = counts.values().sum::<u32>() as f64;
println!("[统计] 找到 {} 种不同的状态码", counts.len());
let mut sorted: Vec<(&String, &u32)> = counts.iter().collect();
sorted.sort_by_key(|(_, &count)| std::cmp::Reverse(count));
for (code, count) in &sorted {
let percentage = (*count as f64 / total * 100.0);
println!(" {}: {} 次 ({:.1}%)", code, count, percentage);
}
Ok(counts)
}
/// 生成统计报告文件
fn generate_report(
counts: &HashMap<String, u32>,
report_path: &str,
) -> std::io::Result<()> {
let mut file = File::create(report_path)?;
writeln!(file, "========================================")?;
writeln!(file, " 服务器日志状态码统计报告")?;
writeln!(file, " 日期: 2026-07-03")?;
writeln!(file, "========================================")?;
writeln!(file)?;
// 按数量降序排列
let mut sorted: Vec<(&String, &u32)> = counts.iter().collect();
sorted.sort_by_key(|(_, &count)| std::cmp::Reverse(count));
let total: u32 = counts.values().sum();
for (code, count) in &sorted {
let bar_length = (*count as f64 / total as f64 * 30.0) as usize;
let bar = "=".repeat(bar_length);
writeln!(file, " {} | {} {:.1}%", code, bar, *count as f64 / total as f64 * 100.0)?;
}
writeln!(file)?;
writeln!(file, " 总请求数: {}", total)?;
writeln!(file, " 成功率 (2xx): {:.1}%",
counts.get("200").unwrap_or(&0) as &u32 + counts.get("304").unwrap_or(&0) as &u32 * 100 / total)?;
writeln!(file, "========================================")?;
Ok(())
}
输出:
=== 服务器日志分析器 ===
[生成] 模拟日志文件: temp_server.log
[分析] 正在处理日志...
[统计] 总行数: 25
[统计] 找到 5 种不同的状态码
200: 12 次 (48.0%)
404: 3 次 (12.0%)
500: 4 次 (16.0%)
304: 2 次 (8.0%)
401: 2 次 (8.0%)
========================================
服务器日志状态码统计报告
日期: 2026-07-03
========================================
200 | ============================== 48.0%
500 | ========== 16.0%
404 | ======== 12.0%
304 | ===== 8.0%
401 | ===== 8.0%
总请求数: 25
成功率 (2xx): 56.0%
========================================
[清理] 临时文件已删除
BufReader缓冲读取器适合处理大文件——它内部维护一个缓冲区,一次性读取一大块数据到内存,然后逐行返回,大大减少了系统调用次数。reader.lines()返回一个迭代器,逐行读取文件。split_whitespace().last()提取每行的最后一个字段(状态码)。HashMap统计每种状态码的出现次数,生成报告后写入文件。
▶ 示例 3:BufWriter 缓冲写入与 Path/PathBuf 路径操作(难度 ⭐⭐)
// ============================================
// BufWriter 缓冲写入 + Path / PathBuf 路径操作
// 演示:多文件合并 + 路径拼接与规范化
// 场景:将多个日志文件合并为一个汇总文件
// ============================================
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
fn main() -> std::io::Result<()> {
println!("=== 日志合并工具 ===\n");
// 使用 PathBuf 构建路径
let base_dir = PathBuf::from(".");
let log_files = vec![
base_dir.join("temp_web.log"),
base_dir.join("temp_api.log"),
base_dir.join("temp_db.log"),
];
// 生成三个模拟日志文件
generate_log_files(&log_files)?;
// 合并所有日志到一个文件
let merged_path = base_dir.join("temp_merged.log");
merge_log_files(&log_files, &merged_path)?;
// 读取并显示合并结果
let merged_content = std::fs::read_to_string(&merged_path)?;
println!("--- 合并后的日志 ---");
println!("{}", merged_content);
// 使用 Path 方法进行路径操作
println!("--- 路径信息 ---");
let path = Path::new("temp_merged.log");
println!(" file_name: {:?}", path.file_name().unwrap_or_default());
println!(" parent: {:?}", path.parent().unwrap_or_default());
println!(" extension: {:?}", path.extension().unwrap_or_default());
println!(" exists: {}", path.exists());
println!(" is_file: {}", path.is_file());
// 获取文件大小
let metadata = path.metadata()?;
println!(" size: {} bytes", metadata.len());
// 清理所有临时文件
cleanup_files(&log_files, &merged_path)?;
Ok(())
}
/// 生成多个模拟日志文件
fn generate_log_files(file_paths: &[PathBuf]) -> std::io::Result<()> {
let logs = vec![
// Web 服务器日志
vec![
"[WEB] 2026-07-03 10:00:00 GET /index.html 200",
"[WEB] 2026-07-03 10:00:05 GET /style.css 200",
"[WEB] 2026-07-03 10:01:00 POST /login 500",
],
// API 服务器日志
vec![
"[API] 2026-07-03 10:00:02 GET /api/users 200",
"[API] 2026-07-03 10:00:10 POST /api/orders 201",
"[API] 2026-07-03 10:01:05 GET /api/products 404",
],
// 数据库日志
vec![
"[DB] 2026-07-03 10:00:01 QUERY SELECT * FROM users 0.3ms",
"[DB] 2026-07-03 10:00:11 QUERY INSERT INTO orders 1.2ms",
"[DB] 2026-07-03 10:01:01 QUERY SELECT * FROM products 0.5ms",
],
];
for (i, path) in file_paths.iter().enumerate() {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
for entry in &logs[i] {
writeln!(writer, "{}", entry)?;
}
// BufWriter 会在 drop 时自动 flush,但也可以手动 flush
writer.flush()?;
println!("[生成] {:?}", path.file_name().unwrap_or_default());
}
Ok(())
}
/// 合并多个日志文件
fn merge_log_files(
sources: &[PathBuf],
destination: &PathBuf,
) -> std::io::Result<()> {
// 使用 BufWriter 写入目标文件
let dest_file = File::create(destination)?;
let mut writer = BufWriter::new(dest_file);
// 写入头部
writeln!(writer, "=== 合并日志报告 ===")?;
writeln!(writer, "生成时间: 2026-07-03 10:05:00")?;
writeln!(writer, "源文件数量: {}", sources.len())?;
writeln!(writer, "{}", "=".repeat(50))?;
writeln!(writer)?;
// 逐文件读取并写入
for source_path in sources {
let file_name = source_path.file_name()
.unwrap_or_default()
.to_string_lossy();
writeln!(writer, "--- 来源: {} ---", file_name)?;
let src_file = File::open(source_path)?;
let reader = BufReader::new(src_file);
for line_result in reader.lines() {
let line = line_result?;
writeln!(writer, "{}", line)?;
}
writeln!(writer)?; // 空行分隔
}
// 写入尾部统计
writeln!(writer, "{}", "=".repeat(50))?;
writeln!(writer, "合并完成")?;
writer.flush()?;
println!("[合并] 完成: {:?}", destination.file_name().unwrap_or_default());
Ok(())
}
/// 清理所有临时文件
fn cleanup_files(log_files: &[PathBuf], merged_path: &PathBuf) -> std::io::Result<()> {
println!("\n--- 清理临时文件 ---");
for path in log_files {
if path.exists() {
std::fs::remove_file(path)?;
println!("[删除] {:?}", path.file_name().unwrap_or_default());
}
}
if merged_path.exists() {
std::fs::remove_file(merged_path)?;
println!("[删除] {:?}", merged_path.file_name().unwrap_or_default());
}
println!("[完成] 所有临时文件已清理");
Ok(())
}
输出:
=== 日志合并工具 ===
[生成] "temp_web.log"
[生成] "temp_api.log"
[生成] "temp_db.log"
[合并] 完成: "temp_merged.log"
--- 合并后的日志 ---
=== 合并日志报告 ===
生成时间: 2026-07-03 10:05:00
源文件数量: 3
==================================================
--- 来源: temp_web.log ---
[WEB] 2026-07-03 10:00:00 GET /index.html 200
[WEB] 2026-07-03 10:00:05 GET /style.css 200
[WEB] 2026-07-03 10:01:00 POST /login 500
--- 来源: temp_api.log ---
[API] 2026-07-03 10:00:02 GET /api/users 200
[API] 2026-07-03 10:00:10 POST /api/orders 201
[API] 2026-07-03 10:01:05 GET /api/products 404
--- 来源: temp_db.log ---
[DB] 2026-07-03 10:00:01 QUERY SELECT * FROM users 0.3ms
[DB] 2026-07-03 10:00:11 QUERY INSERT INTO orders 1.2ms
[DB] 2026-07-03 10:01:01 QUERY SELECT * FROM products 0.5ms
==================================================
合并完成
--- 路径信息 ---
file_name: "temp_merged.log"
parent: ""
extension: "log"
exists: true
is_file: true
size: 638 bytes
--- 清理临时文件 ---
[删除] "temp_web.log"
[删除] "temp_api.log"
[删除] "temp_db.log"
[删除] "temp_merged.log"
[完成] 所有临时文件已清理
BufWriter缓冲写入器将数据先写入内存缓冲区,在缓冲区满或手动flush()时才一次性写入磁盘——这大大减少了系统调用次数。PathBuf是可变的路径字符串(类似String),Path是路径切片(类似&str)。PathBuf::join()拼接路径,Path::file_name()获取文件名,Path::extension()获取扩展名,Path::exists()检查文件是否存在——这些方法自动处理跨平台路径分隔符差异。
▶ 示例 4:完整的日志分析器——模拟真实场景(难度 ⭐⭐⭐)
// ============================================
// 完整的日志分析器——模拟真实运维场景
// 演示:综合运用文件 I/O、路径操作、错误处理
// 功能:读取日志 -> 分析 -> 生成HTML报告 -> 清理
// ============================================
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
/// 日志条目结构体
#[derive(Debug)]
struct LogEntry {
timestamp: String,
method: String,
path: String,
status_code: u16,
}
/// 分析结果
struct AnalysisResult {
total_requests: u32,
status_counts: HashMap<u16, u32>,
method_counts: HashMap<String, u32>,
top_paths: Vec<(String, u32)>,
error_rate: f64,
}
fn main() -> std::io::Result<()> {
println!("=== 完整日志分析器 ===\n");
let log_path = "temp_access.log";
let report_path = "temp_report.html";
// 生成模拟日志
generate_access_log(log_path)?;
println!("[1/4] 模拟日志已生成: {}", log_path);
// 解析日志
let entries = parse_log(log_path)?;
println!("[2/4] 日志解析完成: {} 条记录", entries.len());
// 分析数据
let result = analyze_entries(&entries);
print_analysis(&result);
// 生成 HTML 报告
generate_html_report(&result, report_path)?;
println!("[3/4] HTML 报告已生成: {}", report_path);
// 显示报告内容
let report_content = std::fs::read_to_string(report_path)?;
println!("\n--- 报告预览(前 20 行) ---");
for line in report_content.lines().take(20) {
println!("{}", line);
}
println!("... (共 {} 行)", report_content.lines().count());
// 清理
cleanup(log_path, report_path)?;
println!("\n[4/4] 临时文件已清理");
Ok(())
}
/// 生成模拟的访问日志
fn generate_access_log(path: &str) -> std::io::Result<()> {
let paths = [
"/index.html", "/api/users", "/api/products",
"/api/orders", "/login", "/search",
"/about", "/contact", "/api/settings",
];
let methods = ["GET", "POST", "PUT", "DELETE"];
let statuses = [200, 200, 200, 200, 200, 201, 204, 301, 304, 400, 401, 403, 404, 500, 502];
let file = File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
for i in 0..100 {
let hour = 8 + i / 12;
let minute = (i * 3) % 60;
let second = (i * 17) % 60;
let method = methods[i % methods.len()];
let url = paths[i % paths.len()];
let status = statuses[i % statuses.len()];
writeln!(
writer,
"2026-07-03 {:02}:{:02}:{:02} {} {} {}",
hour, minute, second, method, url, status
)?;
}
writer.flush()?;
Ok(())
}
/// 解析日志文件
fn parse_log(path: &str) -> std::io::Result<Vec<LogEntry>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut entries = Vec::new();
for line_result in reader.lines() {
let line = line_result?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let entry = LogEntry {
timestamp: parts[0].to_string() + " " + parts[1],
method: parts[2].to_string(),
path: parts[3].to_string(),
status_code: parts[4].parse().unwrap_or(0),
};
entries.push(entry);
}
}
Ok(entries)
}
/// 分析日志条目
fn analyze_entries(entries: &[LogEntry]) -> AnalysisResult {
let total = entries.len() as u32;
// 统计状态码
let mut status_counts: HashMap<u16, u32> = HashMap::new();
let mut method_counts: HashMap<String, u32> = HashMap::new();
let mut path_counts: HashMap<String, u32> = HashMap::new();
for entry in entries {
*status_counts.entry(entry.status_code).or_insert(0) += 1;
*method_counts.entry(entry.method.clone()).or_insert(0) += 1;
*path_counts.entry(entry.path.clone()).or_insert(0) += 1;
}
// 计算错误率(4xx + 5xx)
let error_count: u32 = status_counts.iter()
.filter(|(&code, _)| code >= 400)
.map(|(_, &count)| count)
.sum();
let error_rate = if total > 0 {
error_count as f64 / total as f64 * 100.0
} else {
0.0
};
// 最热门的路径
let mut top_paths: Vec<(String, u32)> = path_counts.into_iter().collect();
top_paths.sort_by(|a, b| b.1.cmp(&a.1));
top_paths.truncate(5);
AnalysisResult {
total_requests: total,
status_counts,
method_counts,
top_paths,
error_rate,
}
}
/// 打印分析结果
fn print_analysis(result: &AnalysisResult) {
println!("\n--- 分析结果 ---");
println!("总请求数: {}", result.total_requests);
println!("错误率: {:.1}%", result.error_rate);
println!("\n状态码分布:");
let mut sorted_status: Vec<_> = result.status_counts.iter().collect();
sorted_status.sort_by_key(|(_, &c)| std::cmp::Reverse(c));
for (&code, &count) in &sorted_status {
let bar = "=".repeat((count as f64 / result.total_requests as f64 * 20.0) as usize);
println!(" {} {} ({})", code, bar, count);
}
println!("\nHTTP 方法分布:");
for (method, count) in &result.method_counts {
println!(" {}: {}", method, count);
}
println!("\n热门路径 (Top 5):");
for (i, (path, count)) in result.top_paths.iter().enumerate() {
println!(" {}. {} ({} 次)", i + 1, path, count);
}
}
/// 生成 HTML 格式的分析报告
fn generate_html_report(
result: &AnalysisResult,
path: &str,
) -> std::io::Result<()> {
let file = File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
writeln!(writer, "<!DOCTYPE html>")?;
writeln!(writer, "<html lang=\"zh\">")?;
writeln!(writer, "<head><meta charset=\"UTF-8\"><title>日志分析报告</title>")?;
writeln!(writer, "<style>
body {{ font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }}
h1 {{ color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
th {{ background: #4CAF50; color: white; }}
.error {{ color: red; font-weight: bold; }}
.summary {{ background: white; padding: 20px; border-radius: 5px; }}
</style></head><body>")?;
writeln!(writer, "<h1>服务器日志分析报告</h1>")?;
writeln!(writer, "<div class=\"summary\">")?;
writeln!(writer, "<p>总请求数: <strong>{}</strong></p>", result.total_requests)?;
writeln!(writer, "<p>错误率: <strong class=\"error\">{:.1}%</strong></p>", result.error_rate)?;
writeln!(writer, "</div>")?;
// 状态码表
writeln!(writer, "<h2>状态码分布</h2><table><tr><th>状态码</th><th>次数</th><th>占比</th></tr>")?;
let mut sorted_status: Vec<_> = result.status_counts.iter().collect();
sorted_status.sort_by_key(|(_, &c)| std::cmp::Reverse(c));
for (&code, &count) in &sorted_status {
let pct = count as f64 / result.total_requests as f64 * 100.0;
writeln!(writer, "<tr><td>{}</td><td>{}</td><td>{:.1}%</td></tr>", code, count, pct)?;
}
writeln!(writer, "</table>")?;
writeln!(writer, "</body></html>")?;
writer.flush()?;
Ok(())
}
/// 清理临时文件
fn cleanup(log_path: &str, report_path: &str) -> std::io::Result<()> {
let log = Path::new(log_path);
let report = Path::new(report_path);
if log.exists() {
std::fs::remove_file(log)?;
}
if report.exists() {
std::fs::remove_file(report)?;
}
Ok(())
}
输出:
=== 完整日志分析器 ===
[1/4] 模拟日志已生成: temp_access.log
[2/4] 日志解析完成: 100 条记录
--- 分析结果 ---
总请求数: 100
错误率: 26.0%
状态码分布:
200 ============== (33)
404 ====== (13)
500 ====== (13)
401 ====== (7)
400 ====== (6)
201 ====== (6)
204 ====== (6)
304 ====== (6)
301 ====== (5)
403 ====== (5)
502 ====== (0)
HTTP 方法分布:
GET: 29
POST: 28
PUT: 25
DELETE: 18
热门路径 (Top 5):
1. /api/users (13 次)
2. /api/products (12 次)
3. /index.html (11 次)
4. /api/orders (11 次)
5. /login (11 次)
[3/4] HTML 报告已生成: temp_report.html
--- 报告预览(前 20 行) ---
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>日志分析报告</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }}
h1 {{ color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
th {{ background: #4CAF50; color: white; }}
.error {{ color: red; font-weight: bold; }}
.summary {{ background: white; padding: 20px; border-radius: 5px; }}
</style></head><body>
<h1>服务器日志分析报告</h1>
<div class="summary">
<p>总请求数: <strong>100</strong></p>
<p>错误率: <strong class="error">26.0%</strong></p>
</div>
... (共 49 行)
[4/4] 临时文件已清理
这个综合示例展示了一个完整的"日志分析器"工作流:生成模拟数据 -> 解析日志 -> 多维度分析 -> 生成 HTML 报告 -> 清理。
BufReader和BufWriter在大文件场景下性能优势明显。?运算符让每个 I/O 操作都简洁安全。Path的方法提供了跨平台的路径操作。将日志解析为结构体LogEntry让后续分析代码更清晰、更易维护。
▶ 示例 5:综合练习——CSV 解析与统计(难度 ⭐⭐⭐)
// ============================================
// 综合示例:文件 I/O + 错误处理 + 数据统计
// ============================================
use std::fs;
use std::io::{Write, BufWriter};
use std::path::Path;
struct Record {
name: String,
score: u32,
grade: String,
}
fn parse_csv_line(line: &str) -> Option<Record> {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() != 3 { return None; }
let score = parts[1].trim().parse::<u32>().ok()?;
Some(Record {
name: parts[0].trim().to_string(),
score,
grade: parts[2].trim().to_string(),
})
}
fn analyze(records: &[Record]) -> (f64, u32, u32) {
if records.is_empty() { return (0.0, 0, 0); }
let sum: u32 = records.iter().map(|r| r.score).sum();
let avg = sum as f64 / records.len() as f64;
let max = records.iter().map(|r| r.score).max().unwrap();
let min = records.iter().map(|r| r.score).min().unwrap();
(avg, max, min)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let csv_data = "name,score,grade\nAlice,95,A\nBob,72,C\nCharlie,88,B\nDavid,58,F\nEve,91,A";
let input_path = "temp_scores.csv";
let report_path = "temp_report.txt";
fs::write(input_path, csv_data)?;
println!("CSV 已写入: {}", input_path);
let content = fs::read_to_string(input_path)?;
let records: Vec<Record> = content.lines()
.skip(1)
.filter_map(parse_csv_line)
.collect();
println!("\n=== 解析结果 ===");
for r in &records {
println!("{}: {} 分, 等级 {}", r.name, r.score, r.grade);
}
let (avg, max, min) = analyze(&records);
println!("\n平均: {:.1}, 最高: {}, 最低: {}", avg, max, min);
let a_count = records.iter().filter(|r| r.grade == "A").count();
let f_count = records.iter().filter(|r| r.grade == "F").count();
let file = fs::File::create(report_path)?;
let mut writer = BufWriter::new(file);
writeln!(writer, "=== 成绩统计报告 ===")?;
writeln!(writer, "总人数: {}", records.len())?;
writeln!(writer, "平均分: {:.1}", avg)?;
writeln!(writer, "最高分: {}, 最低分: {}", max, min)?;
writeln!(writer, "A 等级: {} 人, F 等级: {} 人", a_count, f_count)?;
writer.flush()?;
println!("\n报告已写入: {}", report_path);
let report = fs::read_to_string(report_path)?;
println!("\n{}", report);
fs::remove_file(input_path)?;
fs::remove_file(report_path)?;
println!("临时文件已清理");
Ok(())
}
输出:
CSV 已写入: temp_scores.csv
=== 解析结果 ===
Alice: 95 分, 等级 A
Bob: 72 分, 等级 C
Charlie: 88 分, 等级 B
David: 58 分, 等级 F
Eve: 91 分, 等级 A
平均: 80.8, 最高: 95, 最低: 58
报告已写入: temp_report.txt
=== 成绩统计报告 ===
总人数: 5
平均分: 80.8
最高分: 95, 最低分: 58
A 等级: 2 人, F 等级: 1 人
临时文件已清理
CSV 解析 → 统计 → 报告生成 → 清理,完整展示了文件 I/O 工作流。
?传播所有 I/O 错误;BufWriter缓冲写入提升性能;filter_map优雅地过滤无效行;fs::remove_file清理临时文件。
❓ 常见问题
fs::read_to_string 和 BufReader 有什么区别?什么时候用哪个?fs::read_to_string 一次性读取整个文件到内存(简单粗暴),BufReader 逐行读取(节省内存)。File::create 和 OpenOptions::append 有什么区别?File::create 总是创建新文件(如果存在则清空覆盖),OpenOptions::append 在文件末尾追加内容。Path 和 PathBuf 有什么区别?为什么需要两个?Path 是不可变的路径切片(类似 &str),PathBuf 是可变的路径字符串(类似 String)。? 运算符在文件 I/O 中是怎么工作的?? 运算符检查 Result 值:如果是 Ok(val) 就提取 val,如果是 Err(e) 就提前返回 Err(e.into())。📖 小节
std::fs::read_to_string和std::fs::write是最简洁的"一次性读写"方式,适合小文件File::open读取已有文件,File::create创建新文件或覆盖已有文件BufReader缓冲读取器通过lines()逐行处理大文件,减少系统调用次数BufWriter缓冲写入器将多次小写入合并为一次大写入,提高写入性能Path(不可变切片)和PathBuf(可变字符串)提供跨平台路径操作,支持.join()、.file_name()、.extension()等方法?运算符 将Result的错误自动传播,让文件 I/O 代码简洁且安全
📝 作业
-
难度 ⭐:编写一个程序,创建文件
note.txt,写入三段话(使用writeln!宏),然后读取并打印文件内容。最后删除文件。使用?运算符处理错误,main函数返回std::io::Result<()>。 -
难度 ⭐⭐:编写一个"CSV 阅读器"程序。创建一个包含以下内容的 CSV 文件(模拟):
TEXT 📖 仅展示姓名,年龄,城市 Alice,28,New York Bob,32,London Charlie,25,Tokyo使用
BufReader逐行读取,跳过第一行(表头),解析每一行,统计平均年龄。将结果写入result.txt。最后清理所有临时文件。 -
难度 ⭐⭐⭐:编写一个"文件搜索与统计"工具。递归扫描指定目录(使用
std::fs::read_dir),统计每种文件扩展名的数量。生成一个按数量降序排列的报告(写入summary.txt)。要求:(1) 使用Path和PathBuf处理路径;(2) 使用BufWriter写入报告;(3) 使用?传播错误;(4) 输出格式为表格形式,包含扩展名、数量、占比和 ASCII 柱状图。