2014-10-03 03:30 追記
jr として gem にして公開しました。
最近はログをとりあえず行指向の JSON に保存している、という方も多いと思います。
そしてそれを jq でフィルタしたりしている方も多いことでしょう。
問題はその結果に対して、何らかの集計処理を行いたいときです。
jq も reduce
サポートしているようなのですが、どうやって書いていいかわからない。
というか、Ruby で書きたい。
そう思って、以下のようなコマンドを作ってみました。
仮に reduce-json
と名づけます。
なお、この Ruby スクリプトのライセンスは The MIT License とします。
# !/usr/bin/env ruby
require 'yajl'
inputs = ARGV[2] ? ARGV[2..-1].map {|f| open f } : [STDIN]
json_enumerator = Enumerator.new do |yielder|
inputs.each do |input|
Yajl::load(input) do |d|
yielder.yield d
end
end
end
puts Yajl::Encoder.encode(eval "json_enumerator.reduce(#{ARGV[0]}) #{ARGV[1]}")
Yajl が Enumerable
を返してくれなくてちょっと困ったのですが、あっさり解決できました。
Ruby 凄い。
使い方
まずは上記の Ruby スクリプトを reduce-json
として保存して、chmod +x
します。
以下のようなアクセスログがあるとして、そこから各ユーザのアクセス数を集計してみましょう。
これを access_log.json
として保存したとします。
{"user_id":1,"path":"/"}
{"user_id":2,"path":"/home"}
{"user_id":3,"path":"/my"}
{"user_id":1,"path":"/articles"}
{"user_id":1,"path":"/home"}
{"user_id":3,"path":"/"}
{"user_id":2,"path":"/articles/1"}
{"user_id":4,"path":"/my"}
{"user_id":1,"path":"/"}
{"user_id":2,"path":"/"}
そして以下のコマンドを実行します。
コマンドの第一引数が Enumerable#reduce
の第一引数 (つまり初期値) で、第二引数はコードブロックです。
入力は第三引数にファイル名を渡すこともできますし、パイプから標準入力を受け取ることもできます。
$ ./reduce-json '{}' '{|acc, log| user_id = log["user_id"]; acc[user_id] = 0 unless acc[user_id]; acc[user_id] += 1; acc }' access_log.json
{"1":4,"2":3,"3":2,"4":1}
各ユーザごとのアクセス数が JSON として取得できました。
結果は JSON なので、jq
に渡すこともできます。
$ ./reduce-json '{}' '{|acc, log| user_id = log["user_id"]; acc[user_id] = 0 unless acc[user_id]; acc[user_id] += 1; acc }' access_log.json | jq '.'
{
"1": 4,
"2": 3,
"3": 2,
"4": 1
}
簡単なスクリプトですが、UNIX 哲学にもとづいたクールなツールっぽいですね。
配布について
せっかくなので週末にでも Gem として配布できる状態にしておこうと思います。
それまでに、こういう実装がいいんじゃないかとか、こういうオプションが必要なんじゃないかとかあったらコメントください。
あと名前とか。