Rust: Rust 文件 I/O:读写文件与路径处理

最后更新:2026-08-26

文件 I/O 是程序与外部世界对话的"桥梁"——读文件就像"听"别人留下的信息,写文件就像"说"给未来的自己或其他人听。

如果把程序比作一个人,那变量和数据结构就是他的"短期记忆"(大脑),文件系统就是他的"长期记忆"(笔记本)。程序关闭后,短期记忆就消失了,但笔记本上的内容永远还在。文件 I/O 就是"读写笔记本"的能力——没有它,程序每次启动都是一张白纸。


1. 你将学到


2. 日志分析器的故事

(1) 痛苦:手动翻日志

小林是公司的运维工程师,每天早上的第一件事就是手动检查服务器日志:

"如果能写一个程序自动读取日志、统计状态码、生成报告就好了……"

(2) Rust 文件 I/O 的方案

Rust 的标准库提供了完整的文件 I/O 工具,就像一台"日志分析流水线":

TEXT 📖 仅展示
日志文件(服务器)           → 原材料
  File::open / read_to_string → 传送带送入
  BufReader                   → 缓冲处理(流水线缓冲区)
  逐行分析                    → 质检工位
  HashMap 统计状态码          → 分类统计
  File::create / write        → 打包输出报告
RUST
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 错误就立即返回,不需要手动写 matchPathPathBuf 则像文件路径的"安全导航仪",自动处理不同操作系统的路径分隔符差异。


3. 核心概念

(1) 文件 I/O 体系

100%
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:基础读写与错误处理(难度 ⭐)

RUST
// ============================================
// 基础文件读写: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(())
}

输出:

TEXT 📖 仅展示
=== 简易便签程序 ===

[写入] 便签已保存到: temp_note.txt
[检查] 文件存在,大小: 87 字节

[读取] 便签内容:
今天的待办事项:
1. 学习 Rust 文件 I/O
2. 完成日志分析器
3. 复习所有权和借用

Rust 学习笔记:
- 文件 I/O 使用 std::fs 模块
- BufReader 适合大文件读取
- ? 运算符简化错误处理

--- 便签统计 ---
 行数: 10
 单词数: 24
 字符数: 87

[清理] 临时文件已删除: temp_note.txt
[确认] 文件已成功清理

fs::writefs::read_to_string 是最简洁的文件读写方式——一次调用完成所有操作。? 运算符将 Result 中的错误自动传播给调用者,如果出错就提前返回。Path::new(file_path).exists() 检查文件是否存在。fs::remove_file 删除文件。每次运行这个示例都会创建和删除同一个临时文件,确保可重复运行。


▶ 示例 2:BufReader 逐行读取日志(难度 ⭐⭐)

RUST
// ============================================
// 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(())
}

输出:

TEXT 📖 仅展示
=== 服务器日志分析器 ===

[生成] 模拟日志文件: 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 路径操作(难度 ⭐⭐)

RUST
// ============================================
// 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(())
}

输出:

TEXT 📖 仅展示
=== 日志合并工具 ===

[生成] "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:完整的日志分析器——模拟真实场景(难度 ⭐⭐⭐)

RUST
// ============================================
// 完整的日志分析器——模拟真实运维场景
// 演示:综合运用文件 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(())
}

输出:

TEXT 📖 仅展示
=== 完整日志分析器 ===

[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 报告 -> 清理。BufReaderBufWriter 在大文件场景下性能优势明显。? 运算符让每个 I/O 操作都简洁安全。Path 的方法提供了跨平台的路径操作。将日志解析为结构体 LogEntry 让后续分析代码更清晰、更易维护。


▶ 示例 5:综合练习——CSV 解析与统计(难度 ⭐⭐⭐)

RUST
// ============================================
// 综合示例:文件 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(())
}

输出:

TEXT 📖 仅展示
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 清理临时文件。


❓ 常见问题

Q fs::read_to_stringBufReader 有什么区别?什么时候用哪个?
A fs::read_to_string 一次性读取整个文件到内存(简单粗暴),BufReader 逐行读取(节省内存)。
Q File::createOpenOptions::append 有什么区别?
A File::create 总是创建新文件(如果存在则清空覆盖),OpenOptions::append 在文件末尾追加内容。
Q PathPathBuf 有什么区别?为什么需要两个?
A Path 是不可变的路径切片(类似 &str),PathBuf 是可变的路径字符串(类似 String)。
Q ? 运算符在文件 I/O 中是怎么工作的?
A ? 运算符检查 Result 值:如果是 Ok(val) 就提取 val,如果是 Err(e) 就提前返回 Err(e.into())
Q 文件操作完成后需要手动关闭吗?
A 不需要。

📖 小节


📝 作业

  1. 难度 ⭐:编写一个程序,创建文件 note.txt,写入三段话(使用 writeln! 宏),然后读取并打印文件内容。最后删除文件。使用 ? 运算符处理错误,main 函数返回 std::io::Result<()>

  2. 难度 ⭐⭐:编写一个"CSV 阅读器"程序。创建一个包含以下内容的 CSV 文件(模拟):

    TEXT 📖 仅展示
    姓名,年龄,城市
     Alice,28,New York
     Bob,32,London
     Charlie,25,Tokyo
    

    使用 BufReader 逐行读取,跳过第一行(表头),解析每一行,统计平均年龄。将结果写入 result.txt。最后清理所有临时文件。

  3. 难度 ⭐⭐⭐:编写一个"文件搜索与统计"工具。递归扫描指定目录(使用 std::fs::read_dir),统计每种文件扩展名的数量。生成一个按数量降序排列的报告(写入 summary.txt)。要求:(1) 使用 PathPathBuf 处理路径;(2) 使用 BufWriter 写入报告;(3) 使用 ? 传播错误;(4) 输出格式为表格形式,包含扩展名、数量、占比和 ASCII 柱状图。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏