【翻訳】RustとCSV解析(csv crateチュートリアル):前編

Dual-licensed under MIT or the UNLICENSE.

Rust and CSV parsing - Andrew Gallant’s Blog

後編はこちら

翻訳のライセンスはMIT LICENSE

ついにcsv 1.0のベータ版がリリースされ*1、RustでCSVを読み込み書き込みするチュートリアルを書くにはちょうどよいころ合いになった。このチュートリアルは初心者Rustプログラマを対象としている。よってここでは豊富な例を取り上げ、基本的な概念の説明にも紙幅を割いた。熟達したRustプログラマにとっては、使える部分もあるかもしれないが、この点は軽く読み流したほうが得策であろう。

Rustのイントロは公式のオンライン本を参照してほしい。もしあなたが他の言語経験のあるRust入門者なら、他の予備知識を必要とせずにこの本をいきなり読んでも大丈夫だろう。

CSVライブラリはGitHubから入手可能包括的なドキュメントも存在する。

最後に、このブログポストはAPIドキュメントのチュートリアルに含まれており、時間とともに更新される余地があることを承知いただきたい。

対象読者 : 初心者のRustプログラマ

CSV 1.0 beta リリース

チュートリアルに入る前に、手短にこのライブラリが1.0に到達するまでの道のりを語らせてほしい。rust-csvリポジトリへの最初のコミット日は2014年3月22日である。これはRust言語のバージョン1.0がリリースされる一年ちょっと前のことである。Rust 1.0以前から関わっていた人々にとって、この言語にどれほど多くの変更が行われてきたか、というのは記憶に鮮やかなところだろう。もちろん、作者も同様に変化に追従することで、この言語に親しみイディオムに精通していった。しかしながら、CSVライブラリのAPIはほとんど最初のバージョンから変更されることはなかった。当時のAPIはパフォーマンスを向上させるのが難しく、いくつかのすさまじいバグを抱えており、そしてもっと悪いことに、古いシリアライゼーション基盤*2を利用していた。

CSV 1.0はライブラリとしての高速化を達成した。またSerdeというシリアライゼーションフレームワークの下支えを受け、より良いAPIを手に入れた。

新しいCSVライブラリにはcsv-core crateが同梱されている。これはRustの標準ライブラリに頼らずCSVをパースすることができ、またパフォーマンスの改善について大部分の責任を負うライブラリである。とりわけ、古いCSVライブラリは有限状態機械の型を用いておりこれが大きなオーバーヘッドとなっていた。csv-core crateはそのパーサをテーブルに基づくDFAコンパイルする。これは数百バイト程度しかスタックを使わない。結果として、我々は全体的に約2倍の改善を得ることができた:

count_game_deserialize_owned_bytes  30,404,805 (85 MB/s)   23,878,089 (108 MB/s)    -6,526,716  -21.47%   x 1.27
count_game_deserialize_owned_str    30,431,169 (85 MB/s)   22,861,276 (113 MB/s)    -7,569,893  -24.88%   x 1.33
count_game_iter_bytes               21,751,711 (119 MB/s)  11,873,257 (218 MB/s)    -9,878,454  -45.41%   x 1.83
count_game_iter_str                 25,609,184 (101 MB/s)  13,769,390 (188 MB/s)   -11,839,794  -46.23%   x 1.86
count_game_read_bytes               12,110,082 (214 MB/s)  6,686,121 (388 MB/s)     -5,423,961  -44.79%   x 1.81
count_game_read_str                 15,497,249 (167 MB/s)  8,269,207 (314 MB/s)     -7,228,042  -46.64%   x 1.87
count_mbta_deserialize_owned_bytes  5,779,138 (125 MB/s)   3,775,874 (191 MB/s)     -2,003,264  -34.66%   x 1.53
count_mbta_deserialize_owned_str    5,777,055 (125 MB/s)   4,353,921 (166 MB/s)     -1,423,134  -24.63%   x 1.33
count_mbta_iter_bytes               3,991,047 (181 MB/s)   1,805,387 (400 MB/s)     -2,185,660  -54.76%   x 2.21
count_mbta_iter_str                 4,726,647 (153 MB/s)   2,354,842 (307 MB/s)     -2,371,805  -50.18%   x 2.01
count_mbta_read_bytes               2,690,641 (268 MB/s)   1,253,111 (577 MB/s)     -1,437,530  -53.43%   x 2.15
count_mbta_read_str                 3,399,631 (212 MB/s)   1,743,035 (415 MB/s)     -1,656,596  -48.73%   x 1.95
count_nfl_deserialize_owned_bytes   10,608,513 (128 MB/s)  5,828,747 (234 MB/s)     -4,779,766  -45.06%   x 1.82
count_nfl_deserialize_owned_str     10,612,366 (128 MB/s)  6,814,770 (200 MB/s)     -3,797,596  -35.78%   x 1.56
count_nfl_iter_bytes                6,798,767 (200 MB/s)   2,564,448 (532 MB/s)     -4,234,319  -62.28%   x 2.65
count_nfl_iter_str                  7,888,662 (172 MB/s)   3,579,865 (381 MB/s)     -4,308,797  -54.62%   x 2.20
count_nfl_read_bytes                4,588,369 (297 MB/s)   1,911,120 (714 MB/s)     -2,677,249  -58.35%   x 2.40
count_nfl_read_str                  5,755,926 (237 MB/s)   2,847,833 (479 MB/s)     -2,908,093  -50.52%   x 2.02
count_pop_deserialize_owned_bytes   11,052,436 (86 MB/s)   8,848,364 (108 MB/s)     -2,204,072  -19.94%   x 1.25
count_pop_deserialize_owned_str     11,054,638 (86 MB/s)   9,184,678 (104 MB/s)     -1,869,960  -16.92%   x 1.20
count_pop_iter_bytes                6,190,345 (154 MB/s)   3,110,704 (307 MB/s)     -3,079,641  -49.75%   x 1.99
count_pop_iter_str                  7,679,804 (124 MB/s)   4,274,842 (223 MB/s)     -3,404,962  -44.34%   x 1.80
count_pop_read_bytes                3,898,119 (245 MB/s)   2,218,535 (430 MB/s)     -1,679,584  -43.09%   x 1.76
count_pop_read_str                  5,195,237 (183 MB/s)   3,209,998 (297 MB/s)     -1,985,239  -38.21%   x 1.62

では寄り道はこれくらいにして、はじめて行こう。

セットアップ

本節では、単純なプログラムでCSVを読み込み、そして「デバッグ」形式でそれぞれのレコードを表示する。本節はRustツールチェイン(Rust本体とCargo)がすでにインストールされていることを前提としている。

Cargoで新しいプロジェクトを作成する:

$ cargo new --bin csvtutor
$ cd csvtutor

csvtutorディレクトリの内部に降り、Cargo.tomlをお好きなテキストエディタで開いていただき、csv = "1.0.0-beta.1"[dependencies]セクションに追加して欲しい。ここまでで、Cargo.tomlは以下のようになっているはずだ。

[package]
name = "csvtutor"
version = "0.1.0"
authors = ["Your Name"]

[dependencies]
csv = "1.0.0-beta.1"

つづいてこのプロジェクトをビルドしてみよう。csv crateを依存関係に追加したので、Cargoは自動的にそれをダウンロードしてコンパイルしてくれる。Cargoよりプロジェクトを以下のようにビルドする:

$ cargo build

このコマンドはcsvtutorという新しいバイナリをtarget/debugディレクトリに作成する。この時点ではあまり役に立たないが、これは実行することができる:

$ ./target/debug/csvtutor
Hello, world!

このプログラムを何か役に立つことをするものにしてやろう。最初のプログラムはCSVデータをstdinから読み込み、それぞれのレコードをstdoutにデバッグ出力を行う。そのようなプログラムを書くためには、src/main.rsを開いて内容を次のように書き換える。

//tutorial-setup-01.rs
// これはcsv crateをプログラムから利用可能にする
extern crate csv;

// 標準ライブラリのI/Oモジュールをインポートしてstdinからの読み込みをできるようにする
use std::io;

// `main`関数はプログラムの実行が始まるところである
fn main() {
    // CSVパーサを生成しstdinからデータを読む
    let mut rdr = csv::Reader::from_reader(io::stdin());
    // それぞれのレコード上をループする
    for result in rdr.records() {
        // もしエラーが起こったら、プログラムを不親切に中止(abort)する
        // ここはあとでより丁寧なチェックを行う
        let record = result.expect("a CSV record");
        // レコードをデバッグ形式で出力する
        println!("{:?}", record);
    }
}

以上のコードが何を意味するかということについて過度な不安を抱かないでほしい;次節で詳しく解説する。さしあたりは、プロジェクトをリビルドしてみてほしい;

$ cargo build

ビルドが成功したと仮定して、プログラムを実行してみよう。ただし、まずは何か遊べるCSVデータが必要だ! というわけで、アメリカの100都市を適当に抽出して人口と地理座標を合わせたデータを使うことにする(このCSVデータはチュートリアルを通して利用する)。データはgithubから次のコマンドでダウンロードする。

$ curl -LO 'https://raw.githubusercontent.com/BurntSushi/rust-csv/master/examples/data/uspop.csv'

最終的に、プログラムにuspop.csvを与えて次のように実行する。

$ ./target/debug/csvtutor < uspop.csv
StringRecord(["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])
StringRecord(["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])
StringRecord(["Oakman", "AL", "", "33.7133333", "-87.3886111"])
# ... and much more

エラー処理の基本

CSVデータの読み込みはエラーに終わることがあるため、エラー処理はこのチュートリアルのコード例ではいたるところで行われることになる。それゆえに、多少なりともの時間をエラー処理の基本に割くことになる。特に、前節のコード例をより親切な形のエラーを表示するように修正する。もしあなたがすでにRustのReuslttry!/?に慣れ親しんでいるなら、この節は読み飛ばしてかまわない。

留意してほしいのは、The Rust Programming Language Book一般的なエラー処理のイントロを内容として含んでいるという点である。より深く掘り下げるには、エラー処理に関する私のブログポストを参照してほしい。このブログポストは、特にRustのライブラリを設計するときに重要である。

Rustのエラー処理には2つの異なった形式がある:回復不可能なエラーと回復可能なエラーである。

回復不可能なエラーとは一般的に、不変性や約束ごと(contract)が壊れたときに起こる可能性のある、プログラム中のバグに該当する。そのような場合は、プログラムの状態は予測不可能であり、パニックするほかに頼みとするものがほとんどない。Rustでは、パニックは単純にプログラムの中断(aborting)と似通っており、違いはプログラムが終了する前にスタックをアンワインドし、リソースをクリーンアップするものであるということだ。

対して、回復可能なエラーは、一般的に予測可能なエラーに該当する。存在しないファイルや、正しくないCSVデータの取り扱い時に起こるエラーは回復可能なエラーの例である。Rustでは、回復可能なエラーはResultにより処理される。Resultは成功または失敗どちらかの計算の状態を表現している。これは次のように定義される:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

すなわち、Resultは計算が成功したとき型Tの値を含み、計算が失敗したときは型Eの値を含む。

回復不可能なエラーと回復可能なエラーの関係は重要である。特に、回復可能なエラーを回復不可能なものとして扱うことはまったく推奨されない。例として、ファイルが見つからなかったときや、正しくないCSVであったときにパニックすることは、悪い習慣と考えられている。パニックせずに、予測可能なエラーはResult型を用いて処理するべきである。

ここまでで得た知識により、前回の例を見直しエラー処理を詳細化してみよう:

[まず元のコードは以下のようになる。]

//tutorial-error-01.rs
extern crate csv;

use std::io;

fn main() {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        let record = result.expect("a CSV record");
        println!("{:?}", record);
    }
}

このプログラムには、エラーが起こりそうなところが2カ所ある。第一にstdinからレコードを読み込むときに問題があった場合。第二に、stdoutへの書き込みに問題がある場合である。通例にしたがい、このチュートリアルでは後者の問題は無視することにするが、堅牢なコマンドラインアプリケーションを作ろうとするときは処理を怠らないようにしたほうがいい(例:broken pipeが起こったとき)。前者のエラーは詳細を調べる価値がある。例として、もしプログラムのユーザが間違ったCSVを与えたとき、プログラムはパニックを起こす:

$ cat invalid
header1,header2
foo,bar
quux,baz,foobar
$ ./target/debug/csvtutor < invalid
StringRecord { position: Some(Position { byte: 16, line: 2, record: 1 }), fields: ["foo", "bar"] }
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: UnequalLengths { pos: Some(Position { byte: 24, line: 3, record: 2 }), expected_len: 2, len: 3 }', /checkout/src/libcore/result.rs:859
note: Run with `RUST_BACKTRACE=1` for a backtrace.

何が起こったのだろうか? 真っ先に話すべきことは、なぜこのCSVデータが正しくないのかということである。このCSVデータは3つのレコードから成っている:1つのヘッダと2つのデータレコードである。ヘッダとひとつ目のデータレコードは2つのフィールドを持っているが、ふたつ目のデータレコードは3つのフィールドを持っている。デフォルトでは、csv crateは一貫性のないレコード長をエラーとして取り扱う(この挙動はReaderBuilder::flexibleの設定をいじることにより切り替えることができる)。この例は、なぜ最初のデータレコードがこのプログラムで表示できたのかを説明する。すべてヘッダと同じ長さのデータフィールドを持っていたからである。言い換えると、2行目のデータレコードをパースするまでは、実際にエラーに当たるかどうかは分からないのである。

(留意しておいていただきたいのは、CSVリーダーは自動的に最初のレコードをヘッダとして解釈する点である。これについてはReaderBuilder::has_headersの設定から切り替えることができる。)

実際にこのプログラムでパニックを引き起こしているものは何か? 答えはループの1行目にある:

for result in rdr.records() {
    let record = result.expect("a CSV record"); // this panics
    println!("{:?}", record);
}

ここで重要な点は、rdr.records()Result値を生じる(yields)イテレータを返すということである。すなわち、それはレコードそのものを生み出す代わりに、レコードまたはエラーを含むResultを生じるということである。Result上で定義されているexpectメソッドは、Result中の成功した値を取り出す(unwrap)。実際にはResultにエラーが含まれていることもあるので、そのような場合にexpectを呼ぶとプログラムはパニックを起こす。

expectの実装を読んでみるとよい。

use std::fmt;

// This says, "for all types T and E, where E can be turned into a human
// readable debug message, define the `expect` method."
impl<T, E: fmt::Debug> Result<T, E> {
    fn expect(self, msg: &str) -> T {
        match self {
            Ok(t) => t,
            Err(e) => panic!("{}: {:?}", msg, e),
        }
    }
}

これはCSVデータが正しくない場合パニックを引き起こす。また正しくないCSVデータの読み込みは完全に予測可能なエラーであるのに、ここでは回復可能なエラーを回復不可能なものにしてしまっている。これは回復不可能なエラーをその場しのぎの方法で用いている。そして、それはバッドプラクティスである。ゆえに以降のチュートリアルでは回復不可能なエラーを避けるよう努めていく。

回復可能なエラーに切り替える

3ステップに分けて回復不可能なエラーを回復可能なエラーに変換する。最初に、パニックを取り除いて手作業でエラーメッセージを表示する。

//tutorial-error-02.rs
extern crate csv;

use std::io;
use std::process;

fn main() {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // Examine our Result.
        // If there was no problem, print the record.
        // Otherwise, print the error message and quit the program.
        match result {
            Ok(record) => println!("{:?}", record),
            Err(err) => {
                println!("error reading CSV from <stdin>: {}", err);
                process::exit(1);
            }
        }
    }
}

もう一度プログラムを実行すると、いまだにエラーメッセージは現れれるものの、それはパニック時のメッセージではなくなっている:

$ cat invalid
header1,header2
foo,bar
quux,baz,foobar
$ ./target/debug/csvtutor < invalid
StringRecord { position: Some(Position { byte: 16, line: 2, record: 1 }), fields: ["foo", "bar"] }
error reading CSV from <stdin>: CSV error: record 2 (line: 3, byte: 24): found record with 3 fields, but the previous record has 2 fields

回復可能なエラーに向かう第2のステップはCSVレコードに対するループを関数に分離することである。この関数はオプションとしてエラーを返し、main関数はエラーを調査しどのようにそれを扱うか決めることができる:

//tutorial-error-03.rs
extern crate csv;

use std::error::Error;
use std::io;
use std::process;

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // Examine our Result.
        // If there was no problem, print the record.
        // Otherwise, convert our error to a Box<Error> and return it.
        match result {
            Err(err) => return Err(From::from(err)),
            Ok(record) => {
              println!("{:?}", record);
            }
        }
    }
    Ok(())
}

新しく定義した関数runは、返り値の型Result<(), Box<Error>を持っている。簡単にいうと、runは成功したときには何も返さず、エラーが起きた時は、いかなる種類のエラーをも表すBox<Error>を返す。エラーの詳細に関心があるとき、Box<Error>では何が起こったのか調べるのが難しくなる。しかし目先の問題としては、うやうやしくエラーメッセージを表示しプログラムを終了することでこと足りるのでこれ以上は求めない。

第3の、最後のステップは、明示的なmatch式を、Rustの特有の言語仕様であるクエスチョンマーク(?)で置き換えることである。

//tutorial-error-04.rs
extern crate csv;

use std::error::Error;
use std::io;
use std::process;

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // これは実際には前コード例の`match`と同等である。
        // 言い換えると`?`はシンタックスシュガーである。
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

この最後のステップは、matchで明示的に場合分けを行うことなく、?を使って関数の呼び出し元に自動的にエラーを送出する方法を示している。このチュートリアルでは?を使い倒すが、重要なのは、?Result型を返す関数の中でのみ使うことができるという点である。

これで本節を終えるが一つ注意書きを:ここで使ったBox<Error>は、我々が許容できる最低限度のエラーに過ぎないということである。すなわち、これはプログラムに正常にエラー処理をさせてはいるが、実際にエラーが起きた時、詳細なエラー状態を呼び出し側から調べることが難しい。このチュートリアルで書くのはCSVをパースするコマンドラインプログラムなので、これで問題ないと考える。この辺りについてより深く知りたい人、あるいはCSVデータを処理するライブラリ作りに興味のある向きは、エラー処理に関する私のブログポストを参考にしてほしい。

そんなわけで、もしあなたがCSVを変換するちょっとしたプログラムを書く程度ならば、expectのようなメソッドを使ってエラーが起こった時はパニックを起こす、というのも完全に合理的なやり方であると言える。

しかしながら、このチュートリアルでは[よい習慣を身につけてもらうため]イディオマティックなコードを示すように努める。

CSVの読み込み

これで基本的なセットアップとエラー処理の説明が終わり、ようやくやりたいことができるようになった:CSVデータの処理である。すでにstdinからCSVデータを読み込む方法は見てきたので、この節では、ファイルからのCSVデータの読み込み方と、異なる区切り文字や、クォーティングの戦略に合わせてCSVリーダーをどのように設定するかについてカバーする。

まず最初に、これまでの例をstdinの代わりにファイルパス引数を受け取るように変更する。

//tutorial-read-01.rs
extern crate csv;

use std::env;
use std::error::Error;
use std::ffi::OsString;
use std::fs::File;
use std::process;

fn run() -> Result<(), Box<Error>> {
    let file_path = get_first_arg()?;
    let file = File::open(file_path)?;
    let mut rdr = csv::Reader::from_reader(file);
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

/// このプロセスに送られた最初の固定引数を返す
/// 固定引数がなかった場合エラーを返す。
fn get_first_arg() -> Result<OsString, Box<Error>> {
    match env::args_os().nth(1) {
        None => Err(From::from("expected 1 argument, but got none")),
        Some(file_path) => Ok(file_path),
    }
}

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

src/main.rsのコードを上のように書き換えたら、プロジェクトをリビルドしてテストデータに使ってみてほしい。

$ cargo build
$ ./target/debug/csvtutor uspop.csv
StringRecord(["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])
StringRecord(["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])
StringRecord(["Oakman", "AL", "", "33.7133333", "-87.3886111"])
# ... and much more

このコード例は2つの新しい部分からなっている:

  1. ある固定引数を照会するためのコードが追加された。get_first_argである。このプログラムはファイルパスとなる第一引数を期待し(これは添え字1で取り出せる:引数の添え字0には実行ファイル名が入っている)、もし引数が存在しなければget_first_argはエラーを返す。
  2. ファイルを開くコードが追加された。runの中でFile::openを用いてファイルを開き、もしファイルを開く際に問題が起これば、runの呼び出し元にエラーを送出する(ここではこのプログラムのmain)。留意すべきは、我々はファイルをバッファで包むようなことはしていないということである。ファイルのバッファリングはCSVリーダーが内部的に行うので、呼び出し側がそれを行う必要はないのである。

ここでもうひとつ、CSVリーダーの別のコンストラクタを紹介しておこう。ファイルからCSVデータを開くのにこれまでより若干便利にしてくれるものである。それは以下の部分を

let file_path = get_first_arg()?;
let file = File::open(file_path)?;
let mut rdr = csv::Reader::from_reader(file);

次のように書き換える。

let file_path = get_first_arg()?;
let mut rdr = csv::Reader::from_path(file_path)?;

csv::Reader::from_pathはファイルをCSVデータとして開き、もしファイルを開けなかったときはエラーを返す。

ヘッダの読み出し

uspop.csvの中身を見てみると、ヘッダレコードが次のようになっていることに気がつくだろう。

City,State,Population,Latitude,Longitude

いままでに実行したコマンドの出力に立ち返ってみると、ヘッダレコードが決して出力されていないことに気づいただろう。なぜそうなるのか? デフォルト設定では、CSVリーダーはCSVデータの最初のレコードをヘッダとして解釈し、それ以下の行のレコード中の実データとは区別して扱うようにしているからである。それゆえに、CSVデータを読み込みレコードを舐めようとするときは、常にヘッダがスキップされるようになっている。

CSVリーダーはヘッダレコードに関して賢く振る舞おうとはせず、最初のレコードがヘッダであるかどうかを自動的に判別するために、なにか発見的な手法を用いていたり、などということは全くない。代わりに、もし最初のレコードをヘッダとして取り扱いたくないときは、CSVリーダーに対して明示的にそれはヘッダでないことを伝えてやる必要がある。

そのような望み通りの設定をCSVリーダーに対して行うために、ReaderBuilderを用いる必要がある。以下に使った例を示す(注意:コードはstdinから読み出すものに戻っているが簡単のためである)

//tutorial-read-headers-01.rs
fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_reader(io::stdin());
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

このプログラムをコンパイルしてuspop.csvとともに実行すると、ヘッダレコードが表示されたのが確認できるだろう:

$ cargo build
$ ./target/debug/csvtutor < uspop.csv
StringRecord(["City", "State", "Population", "Latitude", "Longitude"])
StringRecord(["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])
StringRecord(["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])
StringRecord(["Oakman", "AL", "", "33.7133333", "-87.3886111"])

もしヘッダレコードを直接読み出したいときは、Reader::headerメソッドを使ってこのようにする:

//tutorial-read-headers-02.rs
fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    {
        // We nest this call in its own scope because of lifetimes.
        let headers = rdr.headers()?;
        println!("{:?}", headers);
    }
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    // We can ask for the headers at any time. There's no need to nest this
    // call in its own scope because we never try to borrow the reader again.
    let headers = rdr.headers()?;
    println!("{:?}", headers);
    Ok(())
}

ひとつの興味深いのは、rdr.headers()の呼び出しがその所有スコープ(its own scope)の中で呼び出されている点である。なぜこのような入れ子を作るのかというと、rdr.headers()CSVリーダーの内部のヘッダ状態のborrow*3を返すからである。このコードにおいてネストしたスコープは、レコードをイテレートしようとする前に、borrowを終了することを可能たらしめている。もしrdr.header()をその所有スコープにネストさせることを怠れば、コードはコンパイルできない。なぜなら、CSVリーダーのヘッダからのborrowと、CSVリーダーがレコード上をイテレートしようとするとき必要なborrowを、同時に行うことはできないからである。

borrowの問題に対する別解としては、ヘッダをcloneすればよいというものがある:

let headers = rdr.headers()?.clone();

このコードはCSVリーダーからのborrowを新しいowned valueに変換する。この解決法はコードをわずかに読みやすくするが、ヘッダレコードを新しく割り当てたメモリにコピーするというコストを払わなければならない。

デリミタ、クォートそして可変長レコード

この節では一時的にuspop.csvのことは忘れ、他のあまりきれいでないCSVデータの読み込み方を示す。以下のCSVデータは;をデリミタとして使い、クォートを\"エスケープしている。そしてこのCSVデータはレコード長がバラバラである。データの内容は、WWEのプロレスラーのリストと、そのデビュー年(もし分からない場合は欠落データとなる)を含むものである。

$ cat strange.csv
"\"Hacksaw\" Jim Duggan";1987
"Bret \"Hit Man\" Hart";1984
# We're not sure when Rafael started, so omit the year.
Rafael Halperin
"\"Big Cat\" Ernie Ladd";1964
"\"Macho Man\" Randy Savage";1985
"Jake \"The Snake\" Roberts";1986

このCSVデータを読み込むために、以下のようなことをしたい。

  1. ヘッダ読み込みを無効化する。このデータにはヘッダがない。
  2. デリミタを,から;に変更する。
  3. クォート戦略を2つ囲み(e.g., "")からエスケープ(e.g., \")に変更する。
  4. 年が省略されていてもよいよう、柔軟なレコード長を許可する。
  5. # から始まる行を無視する(コメント)

これらすべてはReaderBuilderから設定することができる(実際にはそれ以上のことができる)。以下に例を示す:

//tutorial-read-delimiter-01.rs
fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(false)
        .delimiter(b';')
        .double_quote(false)
        .escape(Some(b'\\'))
        .flexible(true)
        .comment(Some(b'#'))
        .from_reader(io::stdin());
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

コンパイルしてstrange.csvに対してプログラムを走らせてみる:

$ cargo build
$ ./target/debug/csvtutor < strange.csv
StringRecord(["\"Hacksaw\" Jim Duggan", "1987"])
StringRecord(["Bret \"Hit Man\" Hart", "1984"])
StringRecord(["Rafael Halperin"])
StringRecord(["\"Big Cat\" Ernie Ladd", "1964"])
StringRecord(["\"Macho Man\" Randy Savage", "1985"])
StringRecord(["Jake \"The Snake\" Roberts", "1986"])

設定周りで少し遊んでみたくなったのではないだろうか。次のようなことを試してみると面白いかもしれない:

  1. escapeの設定を削除しても、CSVの読み込みについてなんらエラーが報告されないことに気づくだろう。レコードはそれでもなおパースできている。これはCSVパーサーの仕様である。与えられたデータが少し間違っていても、使えそうなデータにパースを行なってくれるのである。これはとっちらかった現実世界のCSVデータを取り扱う上で、便利な性質である。
  2. delimiterの設定を削除してもパースは成功する、しかしすべてのレコードは一つのフィールドしか持たない。
  3. flexibleの設定を外すと、CSVリーダーは最初の2つのレコードを表示し(それぞれのフィールドは同数である)、それからフィールドが一つしかない3つめのレコードでパースエラーを返す。

この節でCSVリーダーを設定するのに必要な大部分はカバーすることができた。とはいえここにはまだ紹介していない設定項目がいくつかある。例として、レコードの終端記号を改行文字から別の文字に変更することもできる(デフォルトでは、終端記号はCRLFである。これは\r\n\nを一つのレコードの終端記号としてあつかう)。より詳くは、ドキュメントとReaderBuilderのそれぞれのメソッドを参考にしてほしい。

Serdeとともに読み込む

csv crateのもっとも便利な特徴は、Serdeをサポートしているという点である。Serdeは、データを自動的にシリアライズとデシリアライズして、Rustの型に落とし込むためのフレームワークである。もっと簡単にいうと、文字列フィールドの配列としてレコードをイテレーションする代わりに、我々が選んだ特定の型のレコードをイテレーションすることができる、ということである。

例として、uspop.csvからいくつかデータを見てみよう

City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333

いくつかのフィールドは文字列として意味をなす(City, State)が、他のフィールドは文字列というより数値であるように思えるだろう。例として、Populationは整数を含んでおり、LattitudeLongitudeは小数を含んでいるようだ。もしこれらのフィールドを適切な型に変換したい思ったら、多くの手作業を必要とするだろう。次の例はそれを示す。

//tutorial-read-serde-01.rs
fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        let record = result?;

        let city = &record[0];
        let state = &record[1];
        // Some records are missing population counts, so if we can't
        // parse a number, treat the population count as missing instead
        // of returning an error.
        let pop: Option<u64> = record[2].parse().ok();
        // Lucky us! Latitudes and longitudes are available for every record.
        // Therefore, if one couldn't be parsed, return an error.
        let latitude: f64 = record[3].parse()?;
        let longitude: f64 = record[4].parse()?;

        println!(
            "city: {:?}, state: {:?}, \
             pop: {:?}, latitude: {:?}, longitude: {:?}",
            city, state, pop, latitude, longitude);
    }
    Ok(())
}

ここでの問題は、それぞれのフィールドを手作業でパースせざるをえなくなっていることであり、これは大変な労力と繰り返し作業を要求するものになりうる。Serdeはこの手順を自動化する。例として、すべてのレコードをタプル型にデシリアライズすることができる。

//tutorial-read-serde-02.rs
// 型エイリアスでレコードの型を簡易に参照している
type Record = (String, String, Option<u64>, f64, f64);

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    // `record`でイテレータを作る代わりに
    // `deserialize`でイテレータを作る
    for result in rdr.deserialize() {
        // We must tell Serde what type we want to deserialize into.
        // Serdeに対してどんな型にデシリアライズしてほしいか
        // 教えてあげる必要がある
        let record: Record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

このコードを実行すると前の例と似たような出力が得られる:

$ cargo build
$ ./target/debug/csvtutor < uspop.csv
("Davidsons Landing", "AK", None, 65.2419444, -165.2716667)
("Kenai", "AK", Some(7610), 60.5544444, -151.2583333)
("Oakman", "AL", None, 33.7133333, -87.3886111)
# ... and much more

Serdeを使う上で1つの不都合な点は、指定したレコードの型が、実際のそれぞれのレコードの順序と一致している必要があるということである。これはCSVデータがヘッダレコードを持っているとき、それぞれのフィールドを数値付きフィールドではなく特定の名前付きフィールドとして考えがちになるため、苦痛となりうる。一つの方策としてHashMapBTreeMapを用いてレコードをmap型にデシリアライズするというのがある。次の例は、とりわけ注意すべきところとして、前の例からRecordエイリアスを変更してuseHashMapをインポートしたところ変わったに過ぎないことを注意されたい。

//tutorial-read-serde-03.rs
use std::collections::HashMap;

// This introduces a type alias so that we can conveniently reference our
// record type.
type Record = HashMap<String, String>;

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

このプログラムを実行すると前と似通っているが、それぞれのレコードはmapとして出力されている。

$ cargo build
$ ./target/debug/csvtutor < uspop.csv
{"City": "Davidsons Landing", "Latitude": "65.2419444", "State": "AK", "Population": "", "Longitude": "-165.2716667"}
{"City": "Kenai", "Population": "7610", "State": "AK", "Longitude": "-151.2583333", "Latitude": "60.5544444"}
{"State": "AL", "City": "Oakman", "Longitude": "-87.3886111", "Population": "", "Latitude": "33.7133333"}

この方法は、特にCSVデータをヘッダレコードとともに読み出すときに使えるが、実際のデータ構造はプログラムを走らせるまで分からない。しかしながら、このケースではuspop.csvのデータ構造は事前に分かっている。特に、HashMapを使ったアプローチは、前のコード例でそれぞれのフィールドを(String, String, Option<u64>, f64, f64>にデシリアライズしたときと比べて、型の詳細情報を失っている。ヘッダ名から対応するフィールドを割り出し、それぞれのフィールドに一意な型をつける方法があるのだろうか? 答えはイエスであるが、serde_deriveという新たなcrateを導入する必要がある。Cargo.toml[dependencies]に以下を追加する:

serde = "1"
serde_derive = "1"

これらのcratesをプロジェクトに追加することで、レコードを表現するカスタム構造体を定義することができるようになる。これでCSVレコードを自作の構造体に落とし込むようなグルーコードを、Serdeに自動的に導出してもらうことができる。以下に例を示す。書き写すときは新しくextern crateするのを忘れないように!

//tutorial-read-serde-04.rs
extern crate csv;
extern crate serde;
// This lets us write `#[derive(Deserialize)]`.
#[macro_use]
extern crate serde_derive;

use std::error::Error;
use std::io;
use std::process;

// We don't need to derive `Debug` (which doesn't require Serde), but it's a
// good habit to do it for all your types.
//
// Notice that the field names in this struct are NOT in the same order as
// the fields in the CSV data!
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Record {
    latitude: f64,
    longitude: f64,
    population: Option<u64>,
    city: String,
    state: String,
}

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
        // Try this if you don't like each record smushed on one line:
        // println!("{:#?}", record);
    }
    Ok(())
}

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

コンパイルして実行すると、前と似たような感じの出力を見れる。

$ cargo build
$ ./target/debug/csvtutor < uspop.csv
Record { latitude: 65.2419444, longitude: -165.2716667, population: None, city: "Davidsons Landing", state: "AK" }
Record { latitude: 60.5544444, longitude: -151.2583333, population: Some(7610), city: "Kenai", state: "AK" }
Record { latitude: 33.7133333, longitude: -87.3886111, population: None, city: "Oakman", state: "AL" }

いまいちど強調しておきたいのは、run関数をまったく変更していないという点である:コードは未だにdeserializeイテレータを用いてレコード上を舐めているものであり、この節のはじめから変わっていない。このコード例で変更されたのはRecord型の定義と追加されたextern crate文だけである。Record型は型エイリアスの代わりにカスタム構造体になっており、結果として、Serdeはそれをデフォルトではどのようにデシリアライズすればいいのか分からない。しかしながら、serde_deriveと呼ばれる特別なコンパイラプラグインを使うことで、構造体の定義をコンパイル時に読み込んで、CSVレコードをRecord値にデシリアライズ可能なコードを生成する。自動導出を外したときに何が起こるのか見たいなら#[derive(Debug, Deserialize)]#[derive(Debug)]に変更するとよい。

この例でもう一つ言及しておくと良さそうなのは、#[serde(rename_all = "PascalCase"]の使い方である。これはSerdeが構造体のフィールドをCSVデータ中のヘッダの名前に紐づけるのに役立つ。ヘッダレコードを思い出してもらうと、次のようになっていたと思う:

City,State,Population,Latitude,Longitude

それぞれの名前がキャピタライズされており、しかし私たちの構造体はそうなっていないことに気づかれたはずである。#[serde(rename_all = "PascalCase"]ディレクティブは、それぞれのフィールドをPascalCaseとして解釈することによりこれを修正する。フィールドの最初の一文字が大文字になっているということである。もしこのようにしてSerdeに名前の再配置の仕方を伝えてやらないと、プログラムはエラーとともに終了する:

$ ./target/debug/csvtutor < uspop.csv
CSV deserialize error: record 1 (line: 2, byte: 41): missing field `latitude`

これについては#[serde(rename_all = "PascalCase")]を使わずとも修正できる。例えばフィールド名をすべて大文字にして、次のように書ける:

#[derive(Debug, Deserialize)]
struct Record {
    Latitude: f64,
    Longitude: f64,
    Population: Option<u64>,
    City: String,
    State: String,
}

ただしこれはRustの命名規則に違反する(実際に、Rustコンパイラは規約を守らない名前に対して警告を行う)。

別の修正方法は、個別のフィールドについてリネームの仕方をSerdeに教えてやることである。これはフィールドからヘッダ名への一貫した名前の対応規則がないときに重宝する。

#[derive(Debug, Deserialize)]
struct Record {
    #[serde(rename = "Latitude")]
    latitude: f64,
    #[serde(rename = "Longitude")]
    longitude: f64,
    #[serde(rename = "Population")]
    population: Option<u64>,
    #[serde(rename = "City")]
    city: String,
    #[serde(rename = "State")]
    state: String,
}

フィールドのリネームや、他のSerdeディレクティブについてより深く知りたい場合は、Serdeドキュメントのattributesの項を読んでもらいたい。

Serdeで正しくないデータを処理する

この節では、きれいでないデータを扱う方法を簡潔な例で示す。練習のために、本節では、今まで使っていたUS人口データを少しばかりおかしくしたデータを利用する。このデータは今まで使っていたデータより少しだけ乱雑になっている。データは次のコマンドで入手できる:

$ curl -LO 'https://raw.githubusercontent.com/BurntSushi/rust-csv/master/examples/data/uspop-null.csv'

前節のプログラムを実行するところから再開しよう。

[以下のコードは前節から変わっていない]

//tutorial-read-serde-invalid-01.rs
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Record {
    latitude: f64,
    longitude: f64,
    population: Option<u64>,
    city: String,
    state: String,
}

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

コンパイルし乱雑化したデータを食わせてみる。

$ cargo build
$ ./target/debug/csvtutor < uspop-null.csv
Record { latitude: 65.2419444, longitude: -165.2716667, population: None, city: "Davidsons Landing", state: "AK" }
Record { latitude: 60.5544444, longitude: -151.2583333, population: Some(7610), city: "Kenai", state: "AK" }
Record { latitude: 33.7133333, longitude: -87.3886111, population: None, city: "Oakman", state: "AL" }
# ... more records
CSV deserialize error: record 42 (line: 43, byte: 1710): field 2: invalid digit found in string

なにが起こったのだろうか? プログラムは数十個のレコードを表示し、それからデシリアライズの問題に蹴つまずいて停止してしまった。エラーメッセージは、43行目の添え字2番目のフィールド(Populationフィールド)が正しくない数字であることを伝えている。43行目のデータはどのようになっているのか?

$ head -n 43 uspop-null.csv | tail -n1
Flint Springs,KY,NULL,37.3433333,-86.7136111

3番目のフィールド(添え字2)には人口数か空データが入っていることが期待されている。しかるに、このデータではNULLという値らしきものが入っているようであり、これがおそらくデータの数え上げが不可であることを示しているようである。

現行のプログラムの問題は、NULL文字列をOption<u64>にデシリアライズする方法が分からないために、レコードの読み込みが失敗するということである。すなわち、Option<u64>が空のフィールドまたは整数にしか対応していない[ために起こる問題である]。

これを修正するために、SerdeはどんなデシリアライズのエラーもNone値に変換する方法を提供している。

//tutorial-read-serde-invalid-02.rs
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Record {
    latitude: f64,
    longitude: f64,
    #[serde(deserialize_with = "csv::invalid_option")]
    population: Option<u64>,
    city: String,
    state: String,
}

fn run() -> Result<(), Box<Error>> {
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

コンパイルし実行すると、他の例のごとくファイルの終わりまで実行できるようになる。

$ cargo build
$ ./target/debug/csvtutor < uspop-null.csv
Record { latitude: 65.2419444, longitude: -165.2716667, population: None, city: "Davidsons Landing", state: "AK" }
Record { latitude: 60.5544444, longitude: -151.2583333, population: Some(7610), city: "Kenai", state: "AK" }
Record { latitude: 33.7133333, longitude: -87.3886111, population: None, city: "Oakman", state: "AL" }
# ... and more

この例で変更されたのは、Record型のpopulationフィールドに、次のattributeを追加した点だけである。

#[serde(deserialize_with = "csv::invalid_option")]

invalid_option関数はごく単純なことを行うジェネリックなヘルパー関数である:この関数がOptionフィールドに適用されると、すべてのデシリアライゼーションのエラーをNone値に変換する。これは乱雑なCSVデータを扱う必要にかられた場合便利である。

CSVの書き込み

この節では、CSVデータを書き込むいくつかの例を示す。CSVデータの書き込みは出力形式を制御できるので読み込みより簡単である。

基本的な例から始めてみよう:いくつかのCSVレコードをstdoutに書き出すコードである。

//tutorial-write-01.rs
extern crate csv;

use std::error::Error;
use std::io;
use std::process;

fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::Writer::from_writer(io::stdout());
    // Since we're writing records manually, we must explicitly write our
    // header record. A header record is written the same way that other
    // records are written.
    wtr.write_record(&["City", "State", "Population", "Latitude", "Longitude"])?;
    wtr.write_record(&["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])?;
    wtr.write_record(&["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])?;
    wtr.write_record(&["Oakman", "AL", "", "33.7133333", "-87.3886111"])?;

    // A CSV writer maintains an internal buffer, so it's important
    // to flush the buffer when you're done.
    wtr.flush()?;
    Ok(())
}

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

コンパイルし実行すると、CSVデータが表示される。

$ cargo build
$ ./target/debug/csvtutor
City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111

先に進む前に、write_recordメソッドを詳しく調べてみるとよいだろう。上の例だけだとかなり単純に見えるが、Rustの初心者にとっては型シグネチャが少しばかり仰々しいものに見えることだろう:

pub fn write_record<I, T>(&mut self, record: I) -> csv::Result<()>
    where I: IntoIterator<Item=T>, T: AsRef<[u8]>
{
    // 実装は省略 
}

この型シグネチャを理解するためには、一つ一つ分解して見ていく必要がある:

  1. このメソッドは2つのパラメータを取る:selfrecordだ。
  2. selfは特別なパラメータでWriterそれ自身に対応する。
  3. recordは書き込みを行いたいCSVレコードである。ジェネリックな型Iを持つ。
  4. このメソッドのwhere節では、型IIntoIterator<Item=T>境界により制限されている。これの意味するところは、IIntoIteratorトレイトを満足させる実装を持っている必要があるということである。IntoIterator traitのドキュメントを見ると、イテレータを構築できるような型について記述されているのがわかる。この例では、我々はIとは別のジェネリックな型Tの値を生じるイテレータを欲している。ここでTは書き込みを行いたいそれぞれのフィールドの型を表している。
  5. Twhere節でまた現れており、AsRef<[u8]>境界という制約を受けている。AsRefトレイトはRustにおいて型同士のゼロコスト(zero-cost)な変換を記述する方法である。この例では、AsRef<[u8]>における[u8]Tからバイト列のスライスをborrowすることができるということを意味する。AsRef<[u8]>境界はString, &str, Vec<u8>のような型がすべて条件を満たすため有用である。
  6. 最後に、このメソッドは型csv::Result<()>の値を返す。これはResult<(), csv::Error>の略記である。これはwrite_recordが成功した暁にはなにも返さず、失敗した時はcsv::Errorを返すことを意味する。

さて、ここで学んだwrite_recordの型シグネチャの知識を応用に移してみよう。前の例を思い出すと、次のように関数を使っていた:

wtr.write_record(&["field 1", "field 2", "etc"])?;

この呼び出しはどのように型の一致を確かめているのか? まず、このコードにおけるそれぞれのフィールドの型は&'static strである(Rustにおいては単なる文字列リテラル)。これをスライスリテラルにはめ込むと、パラメータの型は&'static [&'static str]となり、lifetime注釈を省いて簡潔にすると&[&str]となる。スライスはIntoIterator境界を満足し、文字列はAsRef<[u8]>境界を満足するので、結局この関数呼び出しは合法に行われる。

以下にwrite_recordを呼び出すいくつかの方法を示す:

// A slice of byte strings.
wtr.write_record(&[b"a", b"b", b"c"]);
// A vector.
wtr.write_record(vec!["a", "b", "c"]);
// A string record.
wtr.write_record(&csv::StringRecord::from(vec!["a", "b", "c"]));
// A byte record.
wtr.write_record(&csv::ByteRecord::from(vec!["a", "b", "c"]));

締めくくりに、本節で最初に示したコード例が、stdoutに出力する代わりに、簡単にファイル書き出しに変更できることを示して終わろう。

//tutorial-write-02.rs
extern crate csv;

use std::env;
use std::error::Error;
use std::ffi::OsString;
use std::process;

fn run() -> Result<(), Box<Error>> {
    let file_path = get_first_arg()?;
    let mut wtr = csv::Writer::from_path(file_path)?;

    wtr.write_record(&["City", "State", "Population", "Latitude", "Longitude"])?;
    wtr.write_record(&["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])?;
    wtr.write_record(&["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])?;
    wtr.write_record(&["Oakman", "AL", "", "33.7133333", "-87.3886111"])?;

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

/// Returns the first positional argument sent to this process. If there are no
/// positional arguments, then this returns an error.
fn get_first_arg() -> Result<OsString, Box<Error>> {
    match env::args_os().nth(1) {
        None => Err(From::from("expected 1 argument, but got none")),
        Some(file_path) => Ok(file_path),
    }
}

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

タブ区切りされた値を書き出す

前節では、単純なCSVデータをstdoutに書き出す方法を見てきた:

City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111

ここであなたは自問自答するかもしれない。書き出すデータがかくも単純であるならば、なぜCSVライターが必要になるのか? さて、CSVライターを使う利点は、データの完全性(integrity)を損なうことなくあらゆるデータ型を取り扱うことができるということである。すなわち、CSVライターは、いつデータの中に現れるリテラルクォートをエスケープするか、いつ特殊なCSV文字を含むフィールドをクォートするかについて知っている。またCSVライターは、異なるデリミタやクォート戦略を設定するのにも使える。

この節では、CSVライターの設定をいじる方法をちらっとだけ見ていく。特にここでは、CSVの代わりにTSV(“tab separeted value”)を書き込み、またCSVライターに頼んで非数値のフィールドをクォートしてもらう、ということをする。以下が例である:

//tutorial-write-delimiter-01.rs
fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::WriterBuilder::new()
        .delimiter(b'\t')
        .quote_style(csv::QuoteStyle::NonNumeric)
        .from_writer(io::stdout());

    wtr.write_record(&["City", "State", "Population", "Latitude", "Longitude"])?;
    wtr.write_record(&["Davidsons Landing", "AK", "", "65.2419444", "-165.2716667"])?;
    wtr.write_record(&["Kenai", "AK", "7610", "60.5544444", "-151.2583333"])?;
    wtr.write_record(&["Oakman", "AL", "", "33.7133333", "-87.3886111"])?;

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

コンパイルし実行すると次の出力を得る:

$ cargo build
$ ./target/debug/csvtutor
"City"  "State" "Population"    "Latitude"      "Longitude"
"Davidsons Landing"     "AK"    ""      65.2419444      -165.2716667
"Kenai" "AK"    7610    60.5544444      -151.2583333
"Oakman"        "AL"    ""      33.7133333      -87.3886111

この例では、新しい型QuoteStyleを用いている。QuoteStyle型は異なるクォート戦略を表現しており使えるようにしている。デフォルトでは必要になったときしかフィールドにクォートを加えない。これはおそらく大多数のユースケースでうまくいくが、フィールドの周りにクォートを付けるようCSVライターに頼むこともできるし、決してクォートを付けないようにすることや、非数値のフィールドに付けるように頼むこともできる。

Serdeとともに書き込む

CSVリーダーがSerdeによりRustの型への自動デシリアライゼーションをサポートしていたように、CSVライターもSerdeを使ってRustの型への自動シリアライゼーションをサポートしている。本節では、これの使い方を学ぶ。

読み込みと同様に、Rustのタプルをシリアライズする方法から見ていこう:

//tutorial-write-serde-01.rs
fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    // まだheaderを手動で書く必要がある
    wtr.write_record(&["City", "State", "Population", "Latitude", "Longitude"])?;

    // しかし`serialize`で基本的なRustの値をレコードに書き込むことができる
    //
    // 留意すべきは奇妙な文法`None::<u64>`が必要とされている点である
    // これは`None`自身は具体的な型を持たないのだが、Serdeが
    // シリアライズを行うために具体型を必要としているのである。
    // すなわち、`None`は型`None::<u64>`を持ちそれは`Option<u64>`という型を持つ。
    wtr.serialize(("Davidsons Landing", "AK", None::<u64>, 65.2419444, -165.2716667))?;
    wtr.serialize(("Kenai", "AK", Some(7610), 60.5544444, -151.2583333))?;
    wtr.serialize(("Oakman", "AL", None::<u64>, 33.7133333, -87.3886111))?;

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

コンパイルし実行すると期待した出力が得られる:

$ cargo build
$ ./target/debug/csvtutor
City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111

ここで重要なのは、データを書き込むのにwrite_recordの代わりにserializeを用いている点である。特に、write_recordは文字列的なデータを含む、単純なレコードの書き出しに利用が限られる。いっぽう、serializeはデータが数値、浮動小数点数、オプション値のようにより複雑な値から成る場合に使われる。もちろん、手書きで複雑な値を文字列に変換してからwrite_recordすることも可能であるが、Serdeはそうした作業を自動化してくれるものである。

ここまでで見てきたように、カスタム構造体もまたCSVレコードとしてシリアライズすることができる。おまけに、構造体中のフィールドは自動的にヘッダレコードとして書き出されるのだ!

CSVレコードとしてのカスタム構造体を書くためには、[serdeで読み込みを行なったときと]同様にserde_derive crateを使う必要がある。(もしまだ書いていないなら)Cargo.toml[dependencies]に必要な依存関係を加えよう。

serde = "1"
serde_derive = "1"

またコードにextern crate文を加える必要がある。以下に例を示す:

//tutorial-write-serde-02.rs
extern crate csv;
extern crate serde;
#[macro_use]
extern crate serde_derive;

use std::error::Error;
use std::io;
use std::process;

// 構造体は`Serialize`と`Deserialize`双方からderiveできる
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
struct Record<'a> {
    city: &'a str,
    state: &'a str,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}

fn run() -> Result<(), Box<Error>> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.serialize(Record {
        city: "Davidsons Landing",
        state: "AK",
        population: None,
        latitude: 65.2419444,
        longitude: -165.2716667,
    })?;
    wtr.serialize(Record {
        city: "Kenai",
        state: "AK",
        population: Some(7610),
        latitude: 60.5544444,
        longitude: -151.2583333,
    })?;
    wtr.serialize(Record {
        city: "Oakman",
        state: "AL",
        population: None,
        latitude: 33.7133333,
        longitude: -87.3886111,
    })?;

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

fn main() {
    if let Err(err) = run() {
        println!("{}", err);
        process::exit(1);
    }
}

ヘッダレコードを明示的に書いていないことに注目して欲しい。コンパイルし実行すると、前回の出力と同じものが得られる。

$ cargo build
$ ./target/debug/csvtutor
City,State,Population,Latitude,Longitude
Davidsons Landing,AK,,65.2419444,-165.2716667
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111

このケースでは、serializeメソッドはフィールド名付きの構造体を書き出していることを知らされている。このようにすると、serializeは構造体のフィールド定義順に、自動的にヘッダレコードを書き出す(他のレコードがすでに書き出されていない場合に限り)。ちなみにこの挙動はWriteBuilder::has_headersメソッドから無効化することができる。

ついでにRecord構造体のlifetimeパラメータに言及しておく。

struct Record<'a> {
    city: &'a str,
    state: &'a str,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}

頭の'a lifetimeパラメータはcitystateの文字列スライスのlifetimeに対応している。これはRecord構造体がborrowされたデータを含むことを述べている。この構造体をなんのデータもborrowしないように、つまりlifetimeなしで書くこともできる。

struct Record {
    city: String,
    state: String,
    population: Option<u64>,
    latitude: f64,
    longitude: f64,
}

しかしながら、borrowされた&strをownedなString型で置き換えるということは、レコードを書き込むたびにcitystate双方の新しいStringをアロケートしなければならないことを意味する。これでも書き込みはできるにはできるのだが、メモリとパフォーマンスを少しばかり無駄遣いしているだろう。

リアライゼーションの規則について詳しく知りたい向きは、Writer::serializeを参照してほしい。

後編へ続く。

【翻訳】RustとCSV解析(csv crateチュートリアル):後編 - $Read \overset{\mbox{me}}{\rightarrow} Blog$

*1:訳注:2017.5.23

*2:訳注: 未確認だがrust-serializationのことと思われる

*3:訳注:Rust言語の用語である。訳さずそのまま記述する