3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rust で CSV を加工して Regex 生成に気をつけようと思った話

Last updated at Posted at 2016-11-03

発端

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;

みたいなことはできない。
(よく分からないけど,conststatic もコンパイル時に値が確定しなくてはならなくて,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::Writerquote_style のメソッドで指定する。

本当は,CSV ファイルのサイズを抑え,パース時間を抑えるため,必要なセルと空セルのみダブルクオートで囲ませたかったのだが,そういうオプションは無かった。

3
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?