读取和写入文件

RustBeginner
立即练习

简介

许多实用程序都需要跨越文件系统边界:读取已有数据、处理数据,然后保存结果。这些操作可能失败,因此 Rust 的文件 API 返回 Result 值,而不是假设操作一定成功。

你将检查一份准备好的服务日志,使用 Path 表示文件位置,使用 std::fs 读取文本,生成一个简短的汇总,将汇总写入另一个文件,并使用 ? 传递 I/O 错误。

通过 Path 读取文本文件

在此步骤中,你将检查一份准备好的日志,并将其读取到 Rust 的 String 中。

项目目录是 /home/labex/project/log-summary。其中包含作为输入的 data/events.log、供后续保存报告的空 output 目录,以及作为程序源文件的 src/main.rs。进入项目目录:

cd /home/labex/project/log-summary

使用 nano 打开输入文件,了解程序将读取的内容:

nano data/events.log

该文件包含三条简短记录。不要修改文件;按 Ctrl+X 退出。现在打开 Rust 源文件:

nano src/main.rs

预先准备好的两行 use 语句让你可以使用简短名称调用 std::fs 的文件函数和 Path 类型。它们只是导入设置,不是本实验的主要主题;后续介绍模块的实验会完整讲解路径和 use 声明。返回类型 std::io::Result<()> 表示 main 要么以空成功值 () 完成,要么返回 I/O 错误。最后的 Ok(()) 表示操作成功。

将 Step 1 注释替换为:

    let input_path = Path::new("data/events.log");
    let contents = fs::read_to_string(input_path)?;
    println!("Input: {}", input_path.display());
    println!("Contents:\n{}", contents.trim_end());

Path::new 创建一个借用的文件系统路径视图,但不会打开任何文件。相对路径会从你运行 Cargo 的项目目录开始解析。fs::read_to_string 打开文件,将 UTF-8 文本读取到一个拥有所有权的 String 中,并返回一个 Result。成功时,? 会得到其中的 String;如果失败,则从 main 返回 I/O 错误。display() 用适合人阅读的形式格式化路径,trim_end() 则避免文件末尾的换行符导致输出中出现额外的空行。

按 Ctrl+O 保存,按 Enter 确认,然后按 Ctrl+X 退出。检查并运行程序:

cargo check
cargo run --quiet
Input: data/events.log
Contents:
INFO service started
WARN disk nearing limit
INFO backup complete

输出中的路径和三条记录表明程序读取了预期的文件。

统计已加载文本中的记录数

在此步骤中,你将统计内存中已有的 String 里的逻辑行数。

lines() 方法会创建一个遍历文本行的迭代器,不会修改源字符串。它的 count() 消费器会返回迭代器产生的项目数。打开源文件:

nano src/main.rs

将 Step 2 注释替换为:

    let line_count = contents.lines().count();
    println!("Line count: {line_count}");

保存并退出,然后运行:

cargo run --quiet

新增的最后一行应为:

Line count: 3

无论输入文件是否以换行符结尾,这段代码都能正常工作,因为 lines() 识别的是逻辑记录,而不是统计换行字节的数量。

构建稳定的汇总字符串

在此步骤中,你将统计警告记录,并将两个统计结果格式化为可以保存的文本。

字符串方法 starts_with 用于测试前缀。将它与前面已经学过的迭代器适配器结合起来,程序就可以只统计以 WARN 开头的行。打开源文件:

nano src/main.rs

将 Step 3 注释替换为:

    let warning_count = contents
        .lines()
        .filter(|line| line.starts_with("WARN"))
        .count();
    let summary = format!("lines: {line_count}\nwarnings: {warning_count}\n");
    println!("Summary ready:\n{}", summary.trim_end());

过滤器谓词会保留警告行,count() 则消费经过过滤的迭代器。format! 会返回一个新的 String,而不是直接打印内容;显式写入的 \n 字符会创建两行,并在末尾保留一个适合文本文件的换行符。

保存并退出,然后检查并运行:

cargo check
cargo run --quiet

输出的最后一部分应为:

Summary ready:
lines: 3
warnings: 1

现在,汇总已经作为内存中的数据准备就绪,可以写入文件系统。

写入并重新读取汇总文件

在此步骤中,你将把汇总写入准备好的输出目录,然后重新读取文件以验证结果。

fs::write(path, data) 会创建文件,或者使用提供的字节完全替换文件原有内容;它不会追加内容。与读取一样,写入也可能失败,并且会返回一个 I/O Result,因此你将使用 ? 传递错误。打开源文件:

nano src/main.rs

将 Step 4 注释替换为:

    let output_path = Path::new("output/summary.txt");
    fs::write(output_path, &summary)?;
    let saved = fs::read_to_string(output_path)?;
    println!("Saved {}:\n{}", output_path.display(), saved.trim_end());

&summary 会在 write 读取其字节期间借用汇总内容,因此 String 的所有权仍由程序持有。重新读取文件可以检查实际保存的文件,而不是直接假设写入已经成功。

保存并退出,然后检查并运行:

cargo check
cargo run --quiet

输出的最后一部分应为:

Saved output/summary.txt:
lines: 3
warnings: 1

最后检查一次生成的文件:

nano output/summary.txt

你应该能看到相同的两行汇总内容。按 Ctrl+X 退出,不要进行修改。

总结

你使用 Path 表示文件位置,使用 fs::read_to_string 读取 UTF-8 文本,使用 ? 传递 I/O 错误,汇总已加载的记录,使用 fs::write 写入结果,并重新读取保存的文件来确认其内容。