はじめに
Rust実践編では、実際の開発で必要になるスキルを学びます。第1回はファイル操作です。
Rustではstd::fsとstd::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ファイルの操作
serdeとserde_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でパス操作
次回は例外処理について学びます。