CSVサポートを追加して45年目の言語AWKを現代化する
原文は Ben Hoyt により に公開されました。 このブログを購読する
最近、POSIX互換のAWKインタプリタであるGoAWKにCSVファイルを適切に扱う機能を追加しました。データがあふれる今の時代、開発者やデータアナリストにとってAWKを一段と有用にする機能になると考えています。
スプレッドシートへの入力を作成するにしても、公開データソースのデータを分析するにしても、「ビッグデータ」を処理するスクリプトを書くにしても、今日ではCSVやTSVファイルは至るところに存在します。それが最良のフォーマットかどうかは議論の余地がありますが、ある人が言ったように、データフォーマットには2種類しかありません。文句を言われるものと、誰にも使われないものです。
2018年にGoAWKをリリースして間もなく、Hacker Newsのコメントをきっかけに、GoAWKへのCSV対応を考えるようになりました。
はは。CSVファイルをネイティブに理解するツールでawkプログラミング言語が使えたらどれだけ便利だろうと、ちょうど考えていたところだ。突然、それがずっと現実的に思えてきた!
当時、私は次のようにはっきりしない返事をしました。
面白い指摘ですね。AWKにはCSVファイルの引用符を正しくパースするモードがあってもいいと思っていました。もしかしたらそのための -csv オプションを追加するかもしれません(あるいはFSが「,」のときに自動的にそうするようにするかも――ただし後方互換性はなくなりますが)。
ついに、そんなモードが実現しました!コマンドラインオプションは結局-i csv(「input CSV」)になりましたが、ほぼ思った通りです。
この機能にご支援いただいたアントワープ大学図書館に心より感謝します。私の知る限りGoAWKを使っている主要なチーム/プロジェクトは2つあり、そのうちの1つが同図書館で、もう1つはストリームプロセッサのBenthosです。
なぜ必要なのか?
そもそも、なぜこれが必要なのでしょうか。残念ながら標準のAWKには、引用符で囲まれたフィールドを含むCSVファイルを扱う方法がありません。これは実際のCSVファイルを処理するうえで非常に重要です。
フィールド区切り文字をカンマに設定することはできます(-F,やFS=",")。しかしそれは完全な場当たり的な対応で、引用符で囲まれたフィールドが出てきた途端に破綻します。例えば次のようになります。
$ cat quoted.csv
"Smith, Bob",42
$ awk -F, '{ print $1 }' quoted.csv
"Smith # you want to print the first field: Smith, Bob現在、AWKでCSVを処理するための回避策はいくつか存在します。GawkのFPAT機能や、Gawk用のさまざまなCSV拡張、あるいはAdam Gordon Bell氏によるcsvquoteツールなどです。csvquoteはawkを実行する前に入力を変換し、実行後にcsvquote -uで変換を元に戻すという使い方をします。
また、Eli Rosenthal氏による素晴らしいツールfrawkもあり、こちらはネイティブにCSVをサポートしています。しかし残念ながらPOSIX準拠のAWKからはやや逸脱しています(GoAWKのCSVサポートの一部、例えば-iや-oといったコマンドラインオプションはfrawkに着想を得ています)。
だからこそ、POSIX互換のAWKにおける適切なCSVサポートには本当の需要があると考えています。そしてGoAWKはそれを提供します。
本題に入る前に、先ほどの例の引用符の問題を、GoAWKのCSV入力モードを使って修正してみましょう。
$ goawk -i csv '{ print $1 }' quoted.csv
Smith, Bob # that's better!機能
CSV入力モード -i csvオプションはGoAWKにCSV入力モードを使うよう指示します。つまり、標準のフィールド区切り文字やレコード区切り文字(FSやRS)を無視し、代わりにCSVのパース処理を使うということです。TSVモードもありますし、カスタムの区切り文字を指定することもできます。
さらに、-Hオプションを使うと、GoAWKは入力の1行目をフィールド名として扱い、番号ではなく名前でフィールドを取得できる便利な@"named-field"構文が使えるようになります。
例えば、次のような内容のstates.csvファイルがあるとします。
"State","Abbreviation"
"Alabama","AL"
"Alaska","AK"
"Arizona","AZ"
...次のスクリプトで、州の略称だけを出力できます。
$ goawk -i csv -H '{ print @"Abbreviation" }' states.csv
AL
AK
AZ
...あるいは、名前に「New」を含む州の数を数えるには次のようにします。
$ goawk -i csv -H '@"State" ~ /New/ { n++ } END { print n }' states.csv
4~は標準AWKの正規表現マッチ演算子なので、このコードは「Stateフィールドが正規表現Newにマッチするレコードについてnをインクリメントし、最後にnを出力する」という意味になります。
CSV出力モード コマンドライン引数の-o csvはGoAWKにCSV出力モードを使うよう指示します。これにより、1つ以上の引数を伴うprintが出力を適切なCSVエンコーディングで書き出すようになります。
例えば、states.csvをTSVに変換するには次のようにします(ここではあえて-Hを使っていないので、ヘッダー行も含まれます)。
$ goawk -i csv -o tsv '{ print $1, $2 }' states.csv
State Abbreviation
Alabama AL
Alaska AK
Arizona AZ
...さらに、標準AWKのすべての機能が使えるので、他にもできることはたくさんあります。
CSVドキュメントで詳細や多数の使用例をご確認ください。
実装メモ
実装は大規模なテストを含めて約2000行のコードです。
もちろん、オプション設定用のinterp.Config構造体を使ってCSV入力・出力モードを有効化できるGo APIも用意されています。また、BEGINブロック内でINPUTMODEやOUTPUTMODEという特殊変数を設定する方法もあります。
さまざまな理由から、CSV入力モードではencoding/csv.Readerをそのまま使うことはできませんでしたが、標準ライブラリのコード構造を自身のcsvSplitterで再利用し、フィールドをパースして行全体をトークンとして提供するbufio.SplitFuncへと作り変えました。
私自身による多数のテストに加え、実装が正しくRFC 4180に準拠していることを確認するため、標準ライブラリのcsv.Readerテストのうち関連するサブセットを自分の実装に対して実行しています。
一方、CSV出力モードではencoding/csv.Writerを直接使うことができました(ただしCSVの書き込みは読み込みよりずっとシンプルです)。詳細はwriteCSV関数をご覧ください。
パフォーマンス
パフォーマンスについてはまだそれほど時間をかけていません。いずれきちんとプロファイリングするつもりですが、現時点では「十分に高速」です。Goのencoding/csvパッケージを直接使う場合と同等で、Pythonのcsvモジュールよりはずっと高速です。私のノートPCでは、複雑なCSV入力を1ギガバイト読み込むのに約3秒かかります。
frawkと比べると、CSVの入力速度は大幅に遅いですが、(やや意外なことに)CSVの出力速度は大幅に速くなっています。
以下は、goawkとfrawk、そして素のPythonとGoを使ったシンプルな読み書きのベンチマーク結果です。書き込みベンチマークの出力は1GB、350万行、20列(引用符で囲まれた列を含む)のCSVファイルで、読み込みベンチマークの入力には同じファイルを使用しています。時間は秒単位で、SSD搭載の64ビットLinuxノートPCで3回実行したうちの最速の結果を示しています。
| テスト | goawk | frawk | Python | Go |
|---|---|---|---|---|
| 1GB CSVの読み込み | 3.18 | 1.01 | 13.4 | 3.22 |
| 1GB CSVの書き込み | 5.64 | 13.0 | 17.0 | 3.24 |
結論
コンピュータは今や本当に高速なので、開発用のノートPCや比較的小さな仮想マシンだけで、ギガバイトサイズのものも含め、ほとんどのCSVデータセットをGoAWKで処理できます。もはやHadoopなど必要ありません。
ぜひ次のCSV処理タスクでGoAWKを試してみてください。AWKは誕生から45年が経ちますが今でも広く使われており、この機能によって開発者のデータツールキットの中でさらに有用なものになることを願っています。
記事をランダムに読む
コメント
ログインしてコメントする