発端
Ruby で CSV ファイルを処理することがよくあるのだが,大きな CSV だとけっこう処理時間がかかる。
そこで,全角英数字の半角化,半角片仮名類の全角化,末尾スペースの削除のような,どんなプロジェクトでも共通に使うような基本処理だけ Rust で書いて,前処理としてかませたらどうかと考えた。
作譜
Rust はド素人なんだが,regex クレートや csv クレートなどを使ってどうにか CSV 変換プログラムを書き上げた。
コンパイラーの激しいツッコミが却って私を楽にしてくれた感じもする。
試行錯誤を繰り返すと,あちこちに不要コードが散在しがちだけど,コンパイラーがいちいち「この変数,この関数使ってねえだろ!」とか言ってくるので,「はいはいはいはい(ハイは四回まで),消せばいいんでしょ!」とやっていったところ,カオスにならずに済んだような気がする。
試行
200列,3000行,10MB 程度の CSV ファイルを処理させてみたところ,90 秒ほどかかった。
うーん,Rust でもそんなにかかるのかあ。(もちろん --release
で動かしてますョ)
小手先の高速化を試みたけど,たいして改善しない。
疑惑
ふと正規表現を使って置換を実行する部分が気になった。
それは例えばこんなふうな感じ:
Regex::new(r"[0-9a-zA-Z]").unwrap().replace_all(src, ...)
「...
」の部分は文字列変換のクロージャーだ。
実際には,Regex::new
して unwrap
するのが面倒なので,以下のような関数
fn regex(re_str: &str) -> Regex {
Regex::new(re_str).unwrap()
}
を用意しておいて,
regex(r"[0-9a-zA-Z]").replace_all(src, ...)
って書いてるんだけど,本質は同じこと。
で,これってさ,置換をやるたびに毎回 Regex
を作ってるってことだよね?
ちょっと効率悪くね?
セル値ごとにやってるわけだから,何十万回も同じ Regex を作ってるわけだ?
しかもセルあたり 3 種類の置換を実行しているから,百何十万回も無駄な処理を?
改善
そこで,Regex は最初に 1 回だけ作るように変えてみた。
そして,まずはビルド
cargo build --release
して。
そして実行
cargo run --release
だ。
えっと,あれ? Enter キー押した途端に終わっちゃったぞ?
あっそうか,なんか修正ミスして処理が働いてないんだな。
いや? ちゃんと変換されてるみたいだが?
マジかよ。90 秒かかってた処理が瞬きくらいになっちまったじゃねえか。
もっとも,今回の CSV では,実際に変換が起こる箇所(半角片仮名など)はわずかしか無い。変換箇所が多数あればもっと時間がかかるのかもしれない。
教訓
ループ内で Regex を作るべからず。
参考
あらかじめ Regex を作っておくのはいいのだけど,今回の場合,何かの関数内で生成するのではなく,プログラム全体の定数みたいな形で定義しておきたかった。
定数としては const
があり,それとは別に「グローバル変数」として static
がある。
ところが,Regex は const
にも static
にもできない。つまり,
static re: Regex = Regex::new(r"[0-9a-zA-Z]").unwrap;
みたいなことはできない。
(よく分からないけど,const
も static
もコンパイル時に値が確定しなくてはならなくて,Regex::new
みたいなメソッド呼び出しは認められないということかな?)
どうすりゃいいのさ?と思ったら次のありがたい記事が。
Rustのstatic変数とthread local - Qiita
おかげで実現しましたデス。
だいたいこんな感じで書いた。(ネーミングがイマイチだけど)
mod str_const {
use regex::Regex;
lazy_static! {
pub static ref RE1: Regex = {
Regex::new(r"[0-9a-zA-Z]")
};
}
}
lazy_static!
マクロがポイントね。
こうしておくと,str_const::RE1
で呼び出せる。
蛇足
Ruby で CSV を扱う場合,その名もずばり csv という標準添付ライブラリーを使う。
なかなか使いやすいのだが,一つ妙な癖がある。
foo,,""
という CSV 行は
["foo", "", ""]
にはならずに
["foo", nil, ""]
になるのだ。つまり,ダブルクオートで囲まれていない空セルは空文字列ではなく nil
になる。(凶悪ぅ)
そのため,すべてのセル値にわざわざ .to_s
をかまして文字列化するという無駄な処理を書く羽目になる。
そこで,あらかじめすべてのセルをダブルクオートで囲む前処理を Rust でやらせれば,ついでに全角/半角変換とかもやらせれば,Ruby 側で楽かつ時短になるかも,というのが一番最初の動機であった。
ただ,いま測ったら Ruby で 1000 万回の to_s
が 1 秒程度なので,実行時間では大したロスではないようだ。全角/半角変換の類は Rust 化で速くなるかもしれないが,計測はしていない(あとで試す)。
Rust の csv クレートでは,CSV 書き出しのときに
- 必要なセルだけダブルクオートで囲む(デフォルト)
- すべてのセルをダブルクオートで囲む
- ダブルクオートで囲まない(ダブルクオートが必要なセルがあると死ぬ)
が選べるようになっている。csv::Writer の quote_style のメソッドで指定する。
本当は,CSV ファイルのサイズを抑え,パース時間を抑えるため,必要なセルと空セルのみダブルクオートで囲ませたかったのだが,そういうオプションは無かった。