チュートリアル

Rust実践編:ファイル操作

Rustファイル操作実践
広告エリア

はじめに

Rust実践編では、実際の開発で必要になるスキルを学びます。第1回はファイル操作です。

Rustではstd::fsstd::ioモジュールを使ってファイル操作を行います。

テキストファイルの読み書き

ファイルを読む

use std::fs;
use std::io::{self, BufRead, BufReader};

fn main() -> io::Result<()> {
    // ファイル全体を読み込む
    let content = fs::read_to_string("sample.txt")?;
    println!("{}", content);

    // バイト列として読み込む
    let bytes = fs::read("sample.txt")?;
    println!("{:?}", bytes);

    // 1行ずつ読み込む
    let file = fs::File::open("sample.txt")?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        let line = line?;
        println!("{}", line);
    }

    Ok(())
}

ファイルに書き込む

use std::fs::{self, File, OpenOptions};
use std::io::{self, Write, BufWriter};

fn main() -> io::Result<()> {
    // 新規作成・上書き
    fs::write("output.txt", "こんにちは\nRust\n")?;

    // Fileを使って書き込み
    let mut file = File::create("output.txt")?;
    file.write_all(b"Hello, Rust!\n")?;
    writeln!(file, "2行目")?;

    // 追記
    let mut file = OpenOptions::new()
        .append(true)
        .open("output.txt")?;
    writeln!(file, "追加の行")?;

    // バッファリング書き込み
    let file = File::create("output.txt")?;
    let mut writer = BufWriter::new(file);
    writeln!(writer, "バッファリング行")?;
    writer.flush()?;  // 明示的にフラッシュ

    Ok(())
}

スコープによるリソース管理

Rustではスコープを抜けると自動でファイルが閉じられます(RAII)。

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}  // ここでfileは自動的に閉じられる

// 明示的にdropも可能
fn explicit_drop() -> io::Result<()> {
    let file = File::open("sample.txt")?;
    // 処理...
    drop(file);  // 明示的に閉じる
    // 以降fileは使えない
    Ok(())
}

OpenOptionsによる詳細制御

use std::fs::OpenOptions;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    // 読み書き可能で開く
    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .open("file.txt")?;

    // 新規作成(既存があれば上書き)
    let file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open("file.txt")?;

    // 追記モード
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open("file.txt")?;

    // 新規作成のみ(既存があればエラー)
    let file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open("new_file.txt")?;

    Ok(())
}

JSONファイルの操作

serdeserde_jsonクレートを使用します。

# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, BufReader, BufWriter};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
    skills: Vec<String>,
}

fn main() -> io::Result<()> {
    // JSONを読み込む
    let content = fs::read_to_string("user.json")?;
    let user: User = serde_json::from_str(&content)?;
    println!("{:?}", user);

    // ストリーミング読み込み
    let file = fs::File::open("user.json")?;
    let reader = BufReader::new(file);
    let user: User = serde_json::from_reader(reader)?;

    // JSONに書き込む
    let new_user = User {
        name: "太郎".to_string(),
        age: 25,
        skills: vec!["Rust".to_string(), "WebAssembly".to_string()],
    };

    let json = serde_json::to_string_pretty(&new_user)?;
    fs::write("output.json", json)?;

    // ストリーミング書き込み
    let file = fs::File::create("output.json")?;
    let writer = BufWriter::new(file);
    serde_json::to_writer_pretty(writer, &new_user)?;

    Ok(())
}

CSVファイルの操作

csvクレートを使用します。

# Cargo.toml
[dependencies]
csv = "1.3"
serde = { version = "1.0", features = ["derive"] }
use csv::{Reader, Writer};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs::File;

#[derive(Debug, Deserialize, Serialize)]
struct Record {
    name: String,
    age: u32,
    job: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    // CSV読み込み
    let file = File::open("data.csv")?;
    let mut reader = Reader::from_reader(file);

    for result in reader.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }

    // ヘッダーなしで読み込む
    let file = File::open("data.csv")?;
    let mut reader = Reader::from_reader(file);

    for result in reader.records() {
        let record = result?;
        println!("{}, {}", &record[0], &record[1]);
    }

    // CSV書き込み
    let file = File::create("output.csv")?;
    let mut writer = Writer::from_writer(file);

    writer.write_record(&["名前", "年齢", "職業"])?;
    writer.write_record(&["太郎", "25", "エンジニア"])?;

    // 構造体から書き込み
    let records = vec![
        Record { name: "太郎".into(), age: 25, job: "エンジニア".into() },
        Record { name: "花子".into(), age: 22, job: "デザイナー".into() },
    ];

    let file = File::create("output.csv")?;
    let mut writer = Writer::from_writer(file);

    for record in records {
        writer.serialize(record)?;
    }

    writer.flush()?;

    Ok(())
}

パス操作

use std::path::{Path, PathBuf};

fn main() {
    // パスの作成
    let path = Path::new("documents").join("report.txt");
    println!("{}", path.display());  // documents/report.txt

    // PathBuf(所有権あり)
    let mut path_buf = PathBuf::from("documents");
    path_buf.push("report.txt");

    // パス情報の取得
    let path = Path::new("/home/user/documents/report.txt");
    println!("{:?}", path.file_name());    // Some("report.txt")
    println!("{:?}", path.file_stem());    // Some("report")
    println!("{:?}", path.extension());    // Some("txt")
    println!("{:?}", path.parent());       // Some("/home/user/documents")

    // 絶対パスに変換
    let abs = std::fs::canonicalize("file.txt").unwrap();
    println!("{}", abs.display());

    // パスの存在確認
    println!("{}", path.exists());
    println!("{}", path.is_file());
    println!("{}", path.is_dir());
}

ディレクトリ操作

use std::fs::{self, DirEntry};
use std::io;
use std::path::Path;

fn main() -> io::Result<()> {
    // ファイル情報
    let metadata = fs::metadata("sample.txt")?;
    println!("サイズ: {}", metadata.len());
    println!("ディレクトリ: {}", metadata.is_dir());
    println!("更新日時: {:?}", metadata.modified()?);

    // ディレクトリ作成
    fs::create_dir("new_folder")?;
    fs::create_dir_all("a/b/c")?;  // 再帰的に作成

    // ディレクトリ内のファイル一覧
    for entry in fs::read_dir(".")? {
        let entry = entry?;
        println!("{}", entry.path().display());
    }

    // 再帰的にファイルを検索
    fn visit_dirs(dir: &Path) -> io::Result<()> {
        if dir.is_dir() {
            for entry in fs::read_dir(dir)? {
                let entry = entry?;
                let path = entry.path();
                if path.is_dir() {
                    visit_dirs(&path)?;
                } else {
                    if let Some(ext) = path.extension() {
                        if ext == "rs" {
                            println!("{}", path.display());
                        }
                    }
                }
            }
        }
        Ok(())
    }
    visit_dirs(Path::new("."))?;

    // ファイル削除
    fs::remove_file("file.txt")?;

    // ディレクトリ削除
    fs::remove_dir("empty_folder")?;
    fs::remove_dir_all("folder")?;  // 中身ごと削除

    // コピーと移動
    fs::copy("src.txt", "dst.txt")?;
    fs::rename("old.txt", "new.txt")?;

    Ok(())
}

実践例:ログファイル処理

use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use chrono::Local;

struct ErrorEntry {
    line: usize,
    content: String,
}

fn parse_log_file(log_path: &str) -> io::Result<Vec<ErrorEntry>> {
    let file = File::open(log_path)?;
    let reader = BufReader::new(file);
    let mut errors = Vec::new();

    for (line_num, line) in reader.lines().enumerate() {
        let line = line?;
        if line.contains("ERROR") {
            errors.push(ErrorEntry {
                line: line_num + 1,
                content: line.trim().to_string(),
            });
        }
    }

    Ok(errors)
}

fn save_error_report(errors: &[ErrorEntry], output_path: &str) -> io::Result<()> {
    let file = File::create(output_path)?;
    let mut writer = BufWriter::new(file);

    writeln!(writer, "エラーレポート - {}", Local::now())?;
    writeln!(writer, "{}", "=".repeat(50))?;
    writeln!(writer)?;

    for error in errors {
        writeln!(writer, "行 {}: {}", error.line, error.content)?;
    }

    writeln!(writer)?;
    writeln!(writer, "合計: {}件のエラー", errors.len())?;

    writer.flush()?;
    Ok(())
}

fn main() -> io::Result<()> {
    let errors = parse_log_file("app.log")?;
    save_error_report(&errors, "error_report.txt")?;
    Ok(())
}
# Cargo.toml (日時用)
[dependencies]
chrono = "0.4"

エラーハンドリング

ファイル操作では適切なエラーハンドリングが重要です。

use std::fs::File;
use std::io::{self, Read};

// ?演算子でエラーを伝播
fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

// マッチでエラーを処理
fn read_file_with_default(path: &str) -> String {
    match std::fs::read_to_string(path) {
        Ok(content) => content,
        Err(e) => {
            eprintln!("ファイル読み込みエラー: {}", e);
            String::new()
        }
    }
}

// unwrap_or_defaultでデフォルト値
fn read_or_default(path: &str) -> String {
    std::fs::read_to_string(path).unwrap_or_default()
}

まとめ

  • fs::read_to_string/fs::writeで簡単なファイル操作
  • BufReader/BufWriterで効率的なI/O
  • スコープを抜けると自動でファイルが閉じられる(RAII)
  • serde_jsonでJSON操作
  • csvクレートでCSV操作
  • Path/PathBufでパス操作

次回は例外処理について学びます。

広告エリア