クローリング会社としてクローラーを運用していく場合、「毎時XX時にデータを取得する」や、「XX時間に一回データをCSVとして出力する」というような、バッチ処理をよく作成します。バッチ処理は複数のRubyモジュールやプロセスをまたぐこともあるため、それらを包括したシェルスクリプトを作成することが多いです。

そのシェルスクリプトではテキストフィルタリングや加工にAWKスクリプトがよく使われます。先日弊社の記事でも「知ってると得するAWKの書き方9選」という記事を紹介しました。

ふと、

  • AWKの処理をRubyで代替したらどのようにできるのか?
  • Rubyプログラマーが多いメンバー構成の場合、AWKの処理までRubyでやってしまったほうが学習コスト的にはメリット大きいのでは?

と思いたち、今回Rubyワンライナーについて調べてみました。

Rubyワンライナーの定義

まず、Rubyワンライナーとは?というところからですが、Rubyプログラムを記述したファイルをrubyコマンドで実行するおなじみの形式ではなく

$ cat <<EOF > test.rb
puts '普通のRubyプログラムだよ'
EOF

$ ruby test.rb
普通のRubyプログラムだよ

コマンドライン上で以下のように一行のシェルコマンドとして実行する形式のRubyスクリプトをRubyワンライナーとすることとします。

$ ruby -e 'puts "Rubyだってワンライナー書けるもん"'

あまり使う機会が無いかもしれませんがRubyには-eオプションで引数として実行するプログラムを記述することができるのです。

Rubyワンライナー基礎知識(便利なワンライナー向けオプション達)

基本的には上述の-eオプションを用いてRubyワンライナーを書きますが、AWK的な用途で使う場合

  • 標準入力で渡されたファイル(文字列)をメモリ上に読み込み(一行づつ)、
  • 行単位に決められたフィルタリング処理を実行し、
  • フィルタリング結果を出力

というような流れの処理が必要とされます。

例えばファイルの内容を標準入力で受け取り、それをそのまま標準出力で返すというだけの処理を書くとすると、

$ cat test.csv
"id","name","age"
"1","Yoshida","22"
"2","Kameda","30"
"3","Kurume","35"

$ ruby -e 'while gets; puts $_; end' test.csv
"id","name","age"
"1","Yoshida","22"
"2","Kameda","30"
"3","Kurume","35"

非常に頑張って短く書いたとしても'while gets; puts $_; end'という処理が必要となり、入出力処理だけで結構な文字数を消費してしまいます。

このあたりをよしなに処理してくれるAWKを相手とする場合、この入出力処理の記述はRubyにとって結構なハンデとなりますね。

そこはRubyサイドもきちんと考えており、上述の処理を以下のように実行時オプションで省略できるようになってます。

$ ruby -pe '' test.csv
"id","name","age"
"1","Yoshida","22"
"2","Kameda","30"
"3","Kurume","35"

おわかりでしょうか、入出力処理のコードは一文字も記述していません('')。代わりに-pというオプションが増えています。-pオプションが'while gets; puts $_; end'という、標準入力を1行づつ読み込み、各行ごとの処理内容(上記の場合は何も処理してませんが)を出力するという処理を代替したことになります。

このあたりのワンライナーの便利オプションに関しては以下の記事にキレイにまとめてあるのでぜひ参照してみてください。

RubyのワンライナーでAWKのスクリプトに対抗する

以上の基礎知識を踏まえて、標題のAWKのスクリプトに対抗するRubyワンライナーを書いていこうと思います。

テーマは弊社ブログ過去記事「知ってると得するAWKの書き方9選」で取り扱った9つのAWKスクリプトのうち、AWKでもワンライナーで記述された、5つのAWKワンライナーサンプル例をRubyワンライナーで再現できるか?、という5番勝負で対決です。

1.最後からn番目の列を出力する

AWKワンライナー

$ echo "a b c d" | awk '{ print $(NF-1) }'
c

Rubyワンライナー

$ echo "a b c d" | ruby -ane 'print $F[-2]'
c

うん、いいですねRubyも全く負けてません。

2.ファイルの2行目以降だけ扱いたい

CSVファイルのヘッダ行をスキップして1カラム目のみ抽出する処理です。

AWKワンライナー

$ cat <<EOF > foo.csv
name,age,gender
Tom,18,male
Alice,20,female
Watson,45,male
EOF
$ awk -F',' 'FNR > 1 { print $1 }' foo.csv
Tom
Alice
Watson

Rubyワンライナー

$ ruby -F',' -nae 'BEGIN{gets}; puts $F[0]' foo.csv
Tom
Alice
Watson

もしくは

$ ruby -F',' -nae 'puts $F[0] if $. != 1' foo.csv
Tom
Alice
Watson

まだなんとか食らいついていってます。Rubyの-Fでカラムの区切り文字を指定するあたりは、AWKが参考にされているようにも見えますね。

ちなみにRuby組み込み関数$_にはgetsで読み込んだ行の内容が、$.には行番号が入るとのこと。またBEGINはループ開始前に処理される初期化処理が、同様にENDで後処理が記述できます。

3.コマンドラインから引数を与える

事前にワンライナーの処理をファイルに記述しておき、コマンドラインからの実行時に変数を指定したい場合の処理です。

AWKワンライナー

$ cat <<EOF > foo.awk
{ print \$1 * k }
EOF
$ echo "1" | awk -f foo.awk -v 'k=2'
2

Rubyワンライナー

$ cat <<EOF > foo.rb
print STDIN.gets.to_i * ARGV[0].to_i
EOF
$ echo "1" | ruby foo.rb 2
2

これはAWKの-vにあたるオプションがRuby無いのでそのまま再現はキツイ。。代替案として実行時引数を使えばいいのではということにて良しとしましょう。

ちなみにRubyでgetsARGVを併用するときはSTDIN.getsと標準入力を取得すると明記しないとエラーになります。

4.区切り文字を複数指定する

行のカラム区切り文字を複数していしたいケースです。

AWKワンライナー

$ echo "a,b c" | awk -F '[, ]' '{ print $1, $2, $3 }'
a b c

Rubyワンライナー

$ echo "a,b c" | ruby -nle 'a=$_.split(/[, ]/); print "#{a[0]} #{a[1]} #{a[2]}"'
a b c

ここまで助けてもらったrubyの-a, -Fのオートスプリットオプションが、複数区切り文字には対応できないよう…。悔しくも自前スプリットで実装することに。

これは完全にAWKよりも長い記述になってしまいましたね。

5.パターンマッチに変数を使用する

ワンライナーでパターンマッチする行を抽出する例です。

AWKワンライナー

cat <<EOF > foo.txt
a b c
d e f
EOF
$ awk -v 'str=e f' 'match( $0, str ){ print $0 }' foo.txt
d e f

Rubyワンライナー

$ ruby -ne 'print $_ if $_ =~ /e f/' foo.txt
d e f

上記のケースは$_を省略できるため、さらに短縮して以下の様に記述できる。(なんパターンか$_が省略できるケースが用意されているようです。)

$ ruby -ne 'print if /e f/' foo.txt
d e f

先の例の通り、rubyには-vの通り実行時変数定義のオプションが無いことと、上記の例の場合あまり実行時に変数を変更するメリットも無いため、純粋にパターンマッチ行を抽出するサンプルとします。

まとめ

途中から勝ち負けに関してはもうどうでも良くなり、とっても爽やかな汗を書くことができました。。。僕なりのRubyワンライナーの例を書かせて頂きました。もっと効率的な書き方もあるかと思いますので、まだまだ勉強が必要です。

シェルスクリプトのテキスト処理といえばAWKという常識でしたが、Rubyでもなかなか効率の良いワンライナーが書けることが分かりました。それもこれもRubyのコマンドラインスクリプトオプション-eに、かゆいところに手が届く追加オプションが準備されているからです。ただし、これらオプションには癖があるので、一度しっかり勉強する必要はありそうですが。(だったらAWKを勉強しろと言われそう…。)

AWK、Rubyどちらが優れているというものでもなく、ようは使い所だと思います。ケースに応じてそれぞれのメリットが活かせる方を選択する、そのためにはAWK, Rubyそれぞれの良いところをしっかり理解する必要がありますね。

いろんなケースでたくさん試行錯誤しながら、自分なりのベストプラクティスを見つけてください。