commander.jsでらくらく集計バッチ作成

簡単なログファイルの処理にはshell scriptやperlなどを使っていましたが、大きくなく、かつ、大したことがない処理にnode.jsでちゃちゃっと書くことが増えて来ました。

その理由として、d3.jsがcsvやt svをparseしてくれたり、配列の集計などのメソッドが揃っているがあります。

この投稿では、下記2つのモジュールの使い方をご説明します。

1. [commander.js](https://github.com/visionmedia/commander.js/)
2. [d3.js](http://d3js.org/)

## d3.js

[d3.js](http://d3js.org/)はmike bostock氏による集計＋SVG操作ツールです。
この投稿ではSVG操作は利用しません。

```coffeescript:
data = d3.csv.parse csvString
```
として、Object Arrayに変換し、その後、
```coffeescript:
d3.nest().key((d)-> d.key).entries(data)
```
とするだけで、簡単にkeyでの集計を行うことができます。

また、タグの回数の集計などで利用したObjectも簡単に配列にすることができます。

```coffeescript:
d3.entries({"node.js": 32, "hadoop": 43}) 
# [{key: "node.js", value: 32}, {key: "hadoop", value: 43}]
```
ただ、こちらは、coffeeを利用している場合は、下記と等価な処理となります。

```coffeescript:
({ key: key, value: value} for key, value of {"node.js": 32, "hadoop": 43})
```


## commander

[commander.js](https://github.com/visionmedia/commander.js/)はTJによって開発された、コマンドラインのためのヘルパーモジュールです。

例えば引数としてファイルのパスを取るような場合は、下記のように記述するだけです。

```coffeescript:
fs = require 'fs'
commander = require 'commander'

program = commander.version('0.0.0')
  .parse(process.argv)

[filename] = program.args
program.help() unless filename
```

`program.help()`と書くと使い方を表示した上で、process.exitでプロセスを終了してくれます。

また、オプションを取るときには、次のように記述します。

```coffeescript:
fs = require 'fs'
commander = require 'commander'

program = commander.version('0.0.0')
  .option('-v,--verbose', 'url')
  .option('-u,--url <url>', 'url', String)
  .option('-o,--option [optional]', 'url', String)
  .parse(process.argv)

[filename] = program.args
url = program.url
program.help() unless filename or url
```

オプションの書き方は3種類あります。

1. 引数なしのフラグ `.option('-v, --verbose', 'description')`
2. 引数が必要なオプション `.option('-u,--url <url>', 'url', String)`
3. 引数がオプショナルなオプション `.option('-o,--option [optional]', 'url', String)`

## まとめ

2つのモジュールを利用することで、csvでもtsvでも、もちろん、JSONでも、簡単に取り込んで集計処理を書くことができます。

## サンプル

例えば、アクセスログのCSV(これをどうやって入手したか、があれですが)が会ったとすると

```coffeescript:
fs = require 'fs'
commander = require 'commander'

program = commander.version('0.0.0')
  .parse(process.argv)

[filename] = program.args

program.help() unless filename

lines = d3.csv.parse fs.readFileSync filename, 'utf8'
d3.nest().key((d)-> d.status).entries lines
# [
#   { key: 200, values: [ {status 200の行が全部入ってる }] }
#   { key: 404, values: [ {status 404の行が全部入ってる }] }
#   { key: 500, values: [ {status 500の行が全部入ってる }] }
# ]


# 時間あたりのステータスの割合の場合は`.key()`をchainします。
lines = d3.csv.parse fs.readFileSync filename, 'utf8'
hour = d3.time.format("%Y%m%d%H0000")
d3.nest().key((d)-> hour(d.time)).key((d)-> d.status).entries lines
# [
#   { key: 20130524120000, values: [ 
#     { key: 200, values: [ {status 200の行が全部入ってる }] }
#     { key: 404, values: [ {status 404の行が全部入ってる }] }
#     { key: 500, values: [ {status 500の行が全部入ってる }] }
#   }
#   { key: 20130524130000, values: [ 
#     { key: 200, values: [ {status 200の行が全部入ってる }] }
#     { key: 404, values: [ {status 404の行が全部入ってる }] }
#     { key: 500, values: [ {status 500の行が全部入ってる }] }
#   }
#   { key: 20130524140000, values: [ 
#     { key: 200, values: [ {status 200の行が全部入ってる }] }
#     { key: 404, values: [ {status 404の行が全部入ってる }] }
#     { key: 500, values: [ {status 500の行が全部入ってる }] }
#   }
# ]
```