RustのファイルI/O:ファイルの読み書きとパスの扱い方
ファイル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の標準ライブラリには、「ログ分析パイプライン」のような、ファイル入出力のためのツール一式が用意されています:
Log Files(Server) → Raw Materials
File::open / read_to_string → Conveyor Feed
BufReader → Buffering(Assembly Line Buffer)
Line-by-Line Analysis → Quality Inspection Station
HashMap Statistical Status Codes → Categorical Statistics
File::create / write → Generate and Export Report
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
fn analyze_log(path: &str) -> std::io::Result<()> {
// Open the file and create a buffer reader
let file = File::open(path)?;
let reader = BufReader::new(file);
// Count the number of occurrences for each status code
let mut counts: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
for line in reader.lines() {
let line = line?;
// Assumed Log Format: "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;
}
}
// Write to the report file
let mut report = File::create("report.txt")?;
writeln!(report, "=== Status Code Statistics Report ===")?;
for (code, count) in &counts {
writeln!(report, " {}: {}", code, count)?;
}
println!("The report has been generated: report.txt");
Ok(())
}
RustのファイルI/Oの設計は、「ゼロコストの抽象化」という原則に従っています。
BufReaderはパイプライン上のバッファのように機能し、低レベルのシステムコールの回数を減らします。また、?演算子は「自動エラー報告」機能を提供しており、I/Oエラーが発生すると直ちに返却されるため、手動でmatchを記述する必要がなくなります。PathおよびPathBufは、ファイルパスに対する「安全なナビゲーター」として機能し、オペレーティングシステム間のパス区切り文字の違いを自動的に処理します。
3. 基本概念
(1) ファイル入出力システム
graph TB
A[Rust Document I/O] --> B[Read a File]
A --> C[Write to a file]
A --> D[Path Operations]
A --> E[Error Handling]
B --> B1["std::fs::read_to_string"]
B --> B2["File::open + read_to_string"]
B --> B3["BufReader Read line by line"]
C --> C1["std::fs::write"]
C --> C2["File::create + write_all"]
C --> C3["BufWriter Buffered Write"]
D --> D1["Path(Immutable Slices)"]
D --> D2["PathBuf(Variable strings)"]
D --> D3["Path Concatenation .join()"]
E --> E1["Result<T, io::Error>"]
E --> E2["? Operator Propagation Error"]
E --> E3["match Precision Processing"]
(2) 読解法の比較
| メソッド | 関数/型 | ユースケース | メモリ使用量 | パフォーマンス |
|---|---|---|---|---|
| シングルパス読み込み | fs::read_to_string |
小さなファイル(100MB未満) | ファイルの内容全体 | 最速(1回のシステムコール) |
| 手動読み取り | File::read_to_string |
精密な制御が必要 | ファイルの内容全体 | 高速 |
| 行単位のバッファリング | BufReader::read_line |
大容量のファイル/ログ | 1行分のデータ | 中程度(システムコールを削減) |
| バッファ付き反復処理 | BufReader::lines() |
行単位で処理 | 1行分のデータ | 便利(推奨) |
(3) 記述方法の比較
| メソッド | 関数/型 | ユースケース | 機能 |
|---|---|---|---|
| 1回限りの書き込み | 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. ファイル入出力の例
(1) ▶ サンプル:エラー処理を含む基本的な読み書き(難易度 ⭐)
// ============================================
// Basic File Reading and Writing: fs::read_to_string + fs::write
// Demo:Create a temporary file、Enter content、Read the content、Cleanup
// Scene: Write a simple "Memo" program
// ============================================
use std::fs;
use std::path::Path;
fn main() -> std::io::Result<()> {
println!("=== Simple Note-Taking Program ===\n");
// Define the temporary file path (In the current directory temp_note.txt)
let file_path = "temp_note.txt";
// ---------- Write to a file ----------
let content = "Today's To-Do List:
1. Study Rust Document I/O
2. Complete the Log Analyzer
3. Review Ownership and Borrowing
Rust Study Notes:
- Document I/O Usage std::fs Module
- BufReader Suitable for reading large files
- ? Simplifying Error Handling for Operators";
fs::write(file_path, content)?;
println!("[Write] The note has been saved to: {}", file_path);
// Check if the file exists
if Path::new(file_path).exists() {
println!("[Inspection] The file exists,Size: {} Byte", content.len());
}
// ---------- Read a File ----------
let read_content = fs::read_to_string(file_path)?;
println!("\n[Read] Note Content:\n{}", read_content);
// ---------- Statistical Information ----------
let line_count = read_content.lines().count();
let word_count: usize = read_content
.split_whitespace()
.count();
let char_count = read_content.chars().count();
println!("--- Memo Statistics ---");
println!(" Number of lines: {}", line_count);
println!(" Number of words: {}", word_count);
println!(" Number of characters: {}", char_count);
// ---------- Clear Temporary Files ----------
fs::remove_file(file_path)?;
println!("\n[Cleanup] The temporary file has been deleted.: {}", file_path);
// Confirm that the file has been deleted
assert!(!Path::new(file_path).exists(), "The file should be deleted.");
println!("[Confirm] The files have been successfully deleted.");
Ok(())
}
出力:
=== Simple Note-Taking Program ===
[Write] The note has been saved to: temp_note.txt
[Inspection] The file exists,Size: 87 Byte
[Read] Note Content:
Today's To-Do List:
1. Study Rust Document I/O
2. Complete the Log Analyzer
3. Review Ownership and Borrowing
Rust Study Notes:
- Document I/O Usage std::fs Module
- BufReader Suitable for reading large files
- ? Simplifying Error Handling for Operators
--- Memo Statistics ---
Number of lines: 10
Number of words: 24
Number of characters: 87
[Cleanup] The temporary file has been deleted.: temp_note.txt
[Confirm] The files have been successfully deleted.
fs::writeおよびfs::read_to_stringは、ファイルの読み書きを行う最も簡潔な方法です。すべての操作が 1 回の呼び出しで完了します。?演算子は、Resultからのエラーを呼び出し元に自動的に伝播し、エラーが発生した場合は早期に処理を終了します。Path::new(file_path).exists()は、ファイルが存在するかどうかを確認します。fs::remove_fileはファイルを削除します。この例を実行するたびに、同じ一時ファイルが作成・削除されるため、再現性のある結果が得られます。
(2) ▶ サンプル:BufReader によるログの行単位での読み込み (難易度 ⭐⭐)
// ============================================
// BufReader Read line by line + Log Analysis
// Demo: Read the server logs, count the number of each status code
// Scene: Operations Engineers Automatically Analyze Log Files
// ============================================
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
fn main() -> std::io::Result<()> {
println!("=== Server Log Analyzer ===\n");
// Step 1: Create a sample log file
let log_path = "temp_server.log";
create_sample_log(log_path)?;
println!("[Generate] Sample Log File: {}", log_path);
// Step 2: Analyze Logs
println!("[Analysis] Processing logs...");
let status_counts = analyze_log(log_path)?;
// Step 3: Generate Report
let report_path = "temp_report.txt";
generate_report(&status_counts, report_path)?;
// Step 4: Read and Print the Report
let report = std::fs::read_to_string(report_path)?;
println!("\n{}", report);
// Step 5: Cleanup
std::fs::remove_file(log_path)?;
std::fs::remove_file(report_path)?;
println!("[Cleanup] The temporary file has been deleted.");
Ok(())
}
/// Create a sample log file
fn create_sample_log(path: &str) -> std::io::Result<()> {
let mut file = File::create(path)?;
// Simulated Server Logs for One Day
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(())
}
/// Analyze Log Files,Count each type of HTTP Number of occurrences of status codes
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;
// Retrieve Status Code (The last field in the row)
// Log Format: "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!("[Statistics] Total Number of Lines: {}", total_lines);
// Calculate the percentage of each status code
let total: f64 = counts.values().sum::<u32>() as f64;
println!("[Statistics] Found {} Different status codes", 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!(" {}: {} times ({:.1}%)", code, count, percentage);
}
Ok(counts)
}
/// Generate a statistical report file
fn generate_report(
counts: &HashMap<String, u32>,
report_path: &str,
) -> std::io::Result<()> {
let mut file = File::create(report_path)?;
writeln!(file, "========================================")?;
writeln!(file, " Server Log Status Code Statistics Report")?;
writeln!(file, " Date: 2026-07-03")?;
writeln!(file, "========================================")?;
writeln!(file)?;
// Sort by quantity in descending order
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 Number of Requests: {}", total)?;
writeln!(file, " Success Rate (2xx): {:.1}%",
counts.get("200").unwrap_or(&0) as &u32 + counts.get("304").unwrap_or(&0) as &u32 * 100 / total)?;
writeln!(file, "========================================")?;
Ok(())
}
出力:
=== Server Log Analyzer ===
[Generate] Sample Log File: temp_server.log
[Analysis] Processing logs...
[Statistics] Total Number of Lines: 25
[Statistics] Found 5 Different status codes
200: 12 times (48.0%)
404: 3 times (12.0%)
500: 4 times (16.0%)
304: 2 times (8.0%)
401: 2 times (8.0%)
========================================
Server Log Status Code Statistics Report
Date: 2026-07-03
========================================
200 | ============================== 48.0%
500 | ========== 16.0%
404 | ======== 12.0%
304 | ===== 8.0%
401 | ===== 8.0%
Total Number of Requests: 25
Success Rate (2xx): 56.0%
========================================
[Cleanup] The temporary file has been deleted.
BufReaderバッファリーダーは、大容量ファイルの処理に適しています。内部バッファを維持し、大量のデータを一度にメモリに読み込んだ後、行単位で返却するため、システムコールの回数を大幅に削減できます。reader.lines()ファイルを行単位で読み込むイテレータを返します。split_whitespace().last()各行の最後のフィールド(ステータスコード)を抽出します。HashMap各ステータスコードの出現回数をカウントし、レポートを生成してファイルに書き込みます。
(3) ▶ サンプル:BufWriter によるバッファ書き込みと Path/PathBuf によるパス操作 (難易度 ⭐⭐)
// ============================================
// BufWriter Buffered Write + Path / PathBuf Path Operations
// Demo: Merging Multiple Files + Path Concatenation and Normalization
// Scene: Merge multiple log files into a single consolidated file
// ============================================
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
fn main() -> std::io::Result<()> {
println!("=== Log Merging Tool ===\n");
// Usage PathBuf Build Path
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 three sample log files
generate_log_files(&log_files)?;
// Merge all logs into a single file
let merged_path = base_dir.join("temp_merged.log");
merge_log_files(&log_files, &merged_path)?;
// Read and display the merge results
let merged_content = std::fs::read_to_string(&merged_path)?;
println!("--- Merged Logs ---");
println!("{}", merged_content);
// Usage Path Methods for Performing Path Operations
println!("--- Path Information ---");
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());
// Get the file size
let metadata = path.metadata()?;
println!(" size: {} bytes", metadata.len());
// Delete all temporary files
cleanup_files(&log_files, &merged_path)?;
Ok(())
}
/// Generate multiple sample log files
fn generate_log_files(file_paths: &[PathBuf]) -> std::io::Result<()> {
let logs = vec![
// Web Server Logs
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 Server Logs
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",
],
// Database Logs
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 will be drop Automatically flush,But you can also do it manually flush
writer.flush()?;
println!("[Generate] {:?}", path.file_name().unwrap_or_default());
}
Ok(())
}
/// Merge Multiple Log Files
fn merge_log_files(
sources: &[PathBuf],
destination: &PathBuf,
) -> std::io::Result<()> {
// Usage BufWriter Write to the target file
let dest_file = File::create(destination)?;
let mut writer = BufWriter::new(dest_file);
// Write to the header
writeln!(writer, "=== Consolidated Log Report ===")?;
writeln!(writer, "Generation Time: 2026-07-03 10:05:00")?;
writeln!(writer, "Number of source files: {}", sources.len())?;
writeln!(writer, "{}", "=".repeat(50))?;
writeln!(writer)?;
// Read and write on a per-file basis
for source_path in sources {
let file_name = source_path.file_name()
.unwrap_or_default()
.to_string_lossy();
writeln!(writer, "--- Source: {} ---", 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)?; // Blank line separator
}
// Write to the end of the statistics
writeln!(writer, "{}", "=".repeat(50))?;
writeln!(writer, "Merge Complete")?;
writer.flush()?;
println!("[Merge] Done: {:?}", destination.file_name().unwrap_or_default());
Ok(())
}
/// Delete all temporary files
fn cleanup_files(log_files: &[PathBuf], merged_path: &PathBuf) -> std::io::Result<()> {
println!("\n--- Clear Temporary Files ---");
for path in log_files {
if path.exists() {
std::fs::remove_file(path)?;
println!("[Delete] {:?}", path.file_name().unwrap_or_default());
}
}
if merged_path.exists() {
std::fs::remove_file(merged_path)?;
println!("[Delete] {:?}", merged_path.file_name().unwrap_or_default());
}
println!("[Done] All temporary files have been deleted.");
Ok(())
}
出力:
=== Log Merging Tool ===
[Generate] "temp_web.log"
[Generate] "temp_api.log"
[Generate] "temp_db.log"
[Merge] Done: "temp_merged.log"
--- Merged Logs ---
=== Consolidated Log Report ===
Generation Time: 2026-07-03 10:05:00
Number of source files: 3
==================================================
--- Source: 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
--- Source: 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
--- Source: 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
==================================================
Merge Complete
--- Path Information ---
file_name: "temp_merged.log"
parent: ""
extension: "log"
exists: true
is_file: true
size: 638 bytes
--- Clear Temporary Files ---
[Delete] "temp_web.log"
[Delete] "temp_api.log"
[Delete] "temp_db.log"
[Delete] "temp_merged.log"
[Done] All temporary files have been deleted.
BufWriterバッファライターは、まずデータをメモリバッファに書き込み、バッファがいっぱいになったとき、またはflush()が手動で呼び出されたときにのみ、1回の操作でディスクに書き込みを行います。これにより、システムコールの回数が大幅に削減されます。PathBufは可変パス文字列(Stringと同様)であり、Pathはパススライス(&strと同様)です。PathBuf::join()はパスを連結し、Path::file_name()はファイル名を取得し、Path::extension()はファイル拡張子を取得し、Path::exists()はファイルが存在するかどうかを確認します。これらのメソッドは、クロスプラットフォームのパス区切り文字の違いを自動的に処理します。
(4) ▶ サンプル:完全なログアナライザー――実環境のシナリオをシミュレートする(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Log Analyzer——Simulate real-world operations and maintenance scenarios
// Demo:Comprehensive Implementation Document I/O、Path Operations、Error Handling
// Features: Read the log -> Analysis -> Generate HTML Report -> Cleanup
// ============================================
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
/// Log Entry Structure
#[derive(Debug)]
struct LogEntry {
timestamp: String,
method: String,
path: String,
status_code: u16,
}
/// Analysis Results
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!("=== Complete Log Analyzer ===\n");
let log_path = "temp_access.log";
let report_path = "temp_report.html";
// Generate a sample log
generate_access_log(log_path)?;
println!("[1/4] The simulation log has been generated: {}", log_path);
// Analysis Log
let entries = parse_log(log_path)?;
println!("[2/4] Log parsing complete: {} records", entries.len());
// Analyze Data
let result = analyze_entries(&entries);
print_analysis(&result);
// Generate HTML Report
generate_html_report(&result, report_path)?;
println!("[3/4] HTML The report has been generated: {}", report_path);
// Display Report Content
let report_content = std::fs::read_to_string(report_path)?;
println!("\n--- Report Preview(first 20 lines) ---");
for line in report_content.lines().take(20) {
println!("{}", line);
}
println!("... (total {} lines)", report_content.lines().count());
// Cleanup
cleanup(log_path, report_path)?;
println!("\n[4/4] Temporary files have been cleared.");
Ok(())
}
/// Generate a simulated access log
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(())
}
/// Analyzing Log Files
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)
}
/// Analyze Log Entries
fn analyze_entries(entries: &[LogEntry]) -> AnalysisResult {
let total = entries.len() as u32;
// Statistical Status Codes
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;
}
// Calculate the error rate (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
};
// Most Popular Routes
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,
}
}
/// Print the analysis results
fn print_analysis(result: &AnalysisResult) {
println!("\n--- Analysis Results ---");
println!("Total Number of Requests: {}", result.total_requests);
println!("Error Rate: {:.1}%", result.error_rate);
println!("\nStatus Code Distribution:");
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 Method Distribution:");
for (method, count) in &result.method_counts {
println!(" {}: {}", method, count);
}
println!("\nPopular Routes (Top 5):");
for (i, (path, count)) in result.top_paths.iter().enumerate() {
println!(" {}. {} ({} times)", i + 1, path, count);
}
}
/// Generate HTML Analysis Report on Formats
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>Log Analysis Report</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>Server Log Analysis Report</h1>")?;
writeln!(writer, "<div class=\"summary\">")?;
writeln!(writer, "<p>Total Number of Requests: <strong>{}</strong></p>", result.total_requests)?;
writeln!(writer, "<p>Error Rate: <strong class=\"error\">{:.1}%</strong></p>", result.error_rate)?;
writeln!(writer, "</div>")?;
// Status Code Table
writeln!(writer, "<h2>Status Code Distribution</h2><table><tr><th>Status Code</th><th>Count</th><th>Percentage</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(())
}
/// Clear Temporary Files
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(())
}
出力:
=== Complete Log Analyzer ===
[1/4] The simulation log has been generated: temp_access.log
[2/4] Log parsing complete: 100 records
--- Analysis Results ---
Total Number of Requests: 100
Error Rate: 26.0%
Status Code Distribution:
200 ============== (33)
404 ====== (13)
500 ====== (13)
401 ====== (7)
400 ====== (6)
201 ====== (6)
204 ====== (6)
304 ====== (6)
301 ====== (5)
403 ====== (5)
502 ====== (0)
HTTP Method Distribution:
GET: 29
POST: 28
PUT: 25
DELETE: 18
Popular Routes (Top 5):
1. /api/users (13 times)
2. /api/products (12 times)
3. /index.html (11 times)
4. /api/orders (11 times)
5. /login (11 times)
[3/4] HTML The report has been generated: temp_report.html
--- Report Preview(first 20 lines) ---
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>Log Analysis Report</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>Server Log Analysis Report</h1>
<div class="summary">
<p>Total Number of Requests: <strong>100</strong></p>
<p>Error Rate: <strong class="error">26.0%</strong></p>
</div>
... (total 49 lines)
[4/4] Temporary files have been cleared.
この包括的な例では、「ログアナライザー」の完全なワークフロー(シミュレーションデータの生成 → ログの解析 → 多次元分析 → HTMLレポートの生成 → クリーンアップ)を示しています。
BufReaderおよびBufWriterは、大容量ファイルを扱う際に大幅なパフォーマンス上の利点をもたらします。?オペレータは、すべての I/O 操作が簡潔かつ安全に行われることを保証します。Pathメソッドは、クロスプラットフォームでのパス操作を実現します。ログをLogEntry構造体にパースすることで、その後の分析コードがより明確になり、メンテナンスも容易になります。
(5) ▶ サンプル:題 5:総合演習—CSVの解析と統計(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example: File I/O + Error Handling + Statistics
// ============================================
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 Written: {}", 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=== Analysis Results ===");
for r in &records {
println!("{}: {} points, Level {}", r.name, r.score, r.grade);
}
let (avg, max, min) = analyze(&records);
println!("\nAverage: {:.1}, Highest: {}, Lowest: {}", 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, "=== Grades Statistics Report ===")?;
writeln!(writer, "Total number of people: {}", records.len())?;
writeln!(writer, "Average Score: {:.1}", avg)?;
writeln!(writer, "Highest Score: {}, Lowest score: {}", max, min)?;
writeln!(writer, "A Level: {} people, F Level: {} people", a_count, f_count)?;
writer.flush()?;
println!("\nThe report has been submitted.: {}", report_path);
let report = fs::read_to_string(report_path)?;
println!("\n{}", report);
fs::remove_file(input_path)?;
fs::remove_file(report_path)?;
println!("Temporary files have been cleared.");
Ok(())
}
出力:
CSV Written: temp_scores.csv
=== Analysis Results ===
Alice: 95 points, Level A
Bob: 72 points, Level C
Charlie: 88 points, Level B
David: 58 points, Level F
Eve: 91 points, Level A
Average: 80.8, Highest: 95, Lowest: 58
The report has been submitted.: temp_report.txt
=== Grades Statistics Report ===
Total number of people: 5
Average Score: 80.8
Highest Score: 95, Lowest score: 58
A Level: 2 people, F Level: 1 people
Temporary files have been cleared.
CSVの解析 → 統計処理 → レポート生成 → クリーンアップ――これがファイルI/Oのワークフローを完全に示しています。
?すべてのI/Oエラーを伝播します;BufWriterパフォーマンス向上のために書き込みをバッファリングします;filter_map無効な行を適切に除外します;fs::remove_file一時ファイルをクリーンアップします。
❓ よくある質問
fs::read_to_string と BufReader の違いは何ですか?それぞれどのような場合に使用すべきですか?fs::read_to_string はファイル全体を一度にメモリに読み込みます(単純で力業的な方法)。一方、BufReader は行ごとに読み込みます(メモリを節約するため)。 小さなファイル(数MB未満)の場合は、read_to_string を使う方が簡単です。大きなファイル(数百MB以上)の場合は、BufReader を使用する必要があります。そうしないとメモリ不足になります。ログファイルは通常、数百KBから数MB程度ですので、どちらのオプションでも問題ありませんが、BufReader の方が洗練されています。File::createとOpenOptions::appendの違いは何ですか?File::createは常に新しいファイルを作成します(ファイルが存在する場合は、その内容を消去して上書きします)。一方、OpenOptions::appendはファイルの末尾に内容を追加します。 File::createはOpenOptions::new().write(true).create(true).truncate(true)と同等であり、追加モードはOpenOptions::new().append(true).open(path)と同等です。追加モードは通常、ログ記録に使用され、上書きモードは通常、レポートの生成に使用されます。Path と PathBuf の違いは何ですか?なぜ2つ必要なのでしょうか?Path は不変のパススライス(&str に類似)であり、PathBuf は可変のパス文字列(String に類似)です。 PathBufは連結(.join())や変更(.push())が可能で、所有権の譲渡も行えますが、Pathは読み取り専用です。この2つの関係は、Stringと&strの関係に似ています: PathBuf がデータを所有し、Path は借用参照です。関数の引数には通常 &Path(または AsRef<Path>)が使用され、変数は PathBuf を使用して格納されます。?演算子はどのように動作しますか??演算子はResultの値をチェックします。その値がOk(val)の場合はvalを抽出し、Err(e)の場合はErr(e.into())を早期に返します。 ファイルI/Oでは、ほぼすべての操作が Result<T, io::Error> を返します。? を使用すると、コードから match のネストを排除できます。具体的なエラーの詳細を気にしない場合は ? を、エラーの種類ごとに異なる処理が必要な場合は match を使用してください。Fileがdrop(スコープ外に出る)際に自動的にファイルを閉じます。Rustの所有権システムにより、File変数がスコープ外に出ると、Dropトレイトのdropメソッドが自動的に呼び出され、ファイルハンドルが閉じられます。同様に、BufWriterはdrop時に自動的にバッファをflushします。(ファイルが閉じられる前にデータがディスクに書き込まれることを確実にしたい場合は)flush() を使用して手動でバッファをフラッシュするようにしてください。📖 まとめ
std::fs::read_to_stringおよびstd::fs::writeは、最も簡潔な「1回限りの読み書き」方式であり、小さなファイルに適していますFile::open既存のファイルを読み込む;File::create新しいファイルを作成するか、既存のファイルを上書きするBufReaderバッファリーダーは、lines()を使用して大容量ファイルを1行ずつ処理し、システムコールの回数を減らしますBufWriterバッファライターは、複数の小さな書き込みを1つの大きな書き込みにまとめ、書き込みパフォーマンスを向上させますPath(不変スライス)およびPathBuf(可変文字列)は、クロスプラットフォームのパス操作を提供し、.join()、.file_name()、.extension()などのメソッドをサポートしています。?演算子 は、Resultからのエラーを自動的に伝播させるため、ファイル I/O コードを簡潔かつ安全なものにします
📝 練習問題
-
難易度 ⭐:
note.txtという名前のファイルを作成し、そのファイルに(writeln!マクロを使用して)3つの段落を書き込んだ後、ファイルの内容を読み込んで出力するプログラムを作成してください。最後に、そのファイルを削除してください。エラー処理には?演算子を使用し、main関数を使用してstd::io::Result<()>を返してください。 -
難易度 ⭐⭐: 「CSVリーダー」プログラムを作成してください。以下の内容を含むCSVファイルを作成してください(模擬データ):
TEXTName,Age,City Alice,28,New York Bob,32,London Charlie,25,TokyoBufReaderを使用して、1行ずつ読み込み、最初の行(ヘッダー)をスキップし、各行を解析して平均年齢を算出してください。結果をresult.txtに書き出してください。最後に、すべての一時ファイルを削除してください。 -
難易度 ⭐⭐⭐:「ファイル検索および統計」ツールを作成してください。
std::fs::read_dirを使用して指定されたディレクトリを再帰的にスキャンし、各ファイル拡張子ごとのファイル数をカウントします。カウント数の多い順に並べ替えたレポートを生成し、summary.txtに書き出してください。要件:(1) パスの処理にはPathおよびPathBufを使用すること;(2) レポートの出力にはBufWriterを使用すること;(3) エラーの伝播には?を使用すること;(4) 出力形式は、ファイル拡張子、件数、割合、および ASCII 棒グラフを含む表とする。



