2013年10月15日

Python でアクセスログを TSV に変換する

Apache のアクセスログを TSV 形式 (or LTSV) に変換するスクリプトが欲しかったので書いてみました。

用途としては、 grepawk の組み合わせでは面倒だけれども、 FlumeFluentd などをインストールする規模でもないサーバーを想定しています。 ちょっとした開発用のサーバーや、社内サーバーのアクセス状況を見るには十分かな、と思います。

準備

Python 2.7 か 3.3 が使える環境で、スクリプトをダウンロードして実行ビットを立てます。

$ curl -L -k http://bit.ly/GXefkr >combined2tsv.py
$ chmod +x combined2tsv.py

システムでデフォルトに設定されている Python でヘルプを表示させてみます。 標準モジュールが読み込まれていることの確認です。

$ ./combined2tsv.py --help

Python のバージョンを変えたい場合は環境変数を変更するかパスを指定します。

$ python3.3 combined2tsv.py --help

MacOSX や Linux の多くのディストリビューションでは Python 2.7 くらいが標準だと思いますが、 Fedora 22 では Python 3 系を標準にすることが提案されています。 どちらでも動くように記述しておくと、OS のアップグレードにも左右されないので良いと思います。

使い方

最も基本的な使い方は、引数でアクセスログのファイルパスを指定する方法です。 生ログがタブ区切りテキストに変換されて出力されます。

$ ./combined2tsv.py access.log

デフォルトでの出力項目は以下の項目です。

  • time : アクセス日時。 datetime 型の出力フォーマットは "%Y-%m-%dT%H:%M:%S" 形式。
  • host : アクセス元の IP アドレス。
  • path : リクエストパス。クエリ文字列は含まない。
  • query : クエリ文字列。 "?" 記号は含まない。
  • method : リクエストメソッド。GET, POST など。
  • protocol : プロトコルバージョン。
  • status : ステータスコード。
  • size: レスポンスサイズ。
  • referer : リファラー。
  • ua : ユーザーエージェント。
  • reqtime_microsec : レスポンスタイム [ミリ秒]。(定義されている場合)
  • trailing : その他、ログフォーマットを拡張した残りの部分。
  • source : 入力ファイル名。標準入力の場合は空。

引数には複数のファイルを指定できます。GZIP で圧縮されていても構いません。

$ ./combined2tsv.py access.log access.log.gz

出力先は -o/--output でも指定できます。 (普通に出力のリダイレクトでも構いません)

$ ./combined2tsv.py access.log -o access.tsv

引数にファイルを指定しない場合は標準入力を読み込みます。 これにより、他のコマンドの出力をパイプで繋ぐことが可能です。

$ head access.log | ./combined2tsv.py

出力にヘッダー行が欲しい場合は --header オプションを付けます。

$ grep "favicon.ico" access.log | ./combined2tsv.py --header

各フィールドにラベルを付ける LTSV 形式にするには --ltsv オプションを付けます。

$ tail access.log | ./combined2tsv.py --ltsv

なお、 --ltsv--header を同時に指定することはできません。

出力項目は外部ファイルで定義することもできます。 JSON Table Shema で定義したファイルを -c/--config オプションで与えます。

$ head access.log | ./combined2tsv.py -c schema.json

スキーマファイルの例( schema.json として保存)は以下のようになります。 Cerberus の記法とも合っているはずですので、一度定義すれば流用が簡単なはずです。 JSON Schema に比べて、フィールドの並び順が明示されている点が CSV/TSV との相性が良いと思います。 ( dict ではなく list である)

{
  "fields": [
    {"id": "time", "type": "datetime", "format": "%Y-%m-%dT%H:%M:%S"},
    {"id": "path", "type": "string"},
    {"id": "query", "type": "string"},
    {"id": "status", "type": "integer"}
  ]
}

実装メモ

Google で検索すればたくさんヒットするけど、そもそも検索するのを忘れそうなことをメモしておきます。

argparse でオプションに排他性を持たせる:
add_mutually_exclusive_group() メソッドを呼び出して引数を設定します。 ログの冗長性を設定するときなどにも有効です。
正規表現の結果を辞書で受け取る (groupdict) のではなく、 namedtuple を使う:
Apache Log Parsing というブログ記事にある内容です。 この方が処理が高速です。
日付文字列の解釈に strptime を使うと遅くなることがある:
勢い余って Python 本体にバグとして報告 (Issue15328) している人もいますが、 ロケールの設定 (*) によっては strptime が異様に処理時間を消費することがあります。 安易に LOCALE=C とか設定すると他の機能に影響が出てしまうかもしれませんので、 入力日付フォーマットが決まっている場合は、自前で文字列を分割した方がほどほどな処理時間で安定するはずです。 Python datetime.strptime() Eating lots of CPU Time への回答にもあるように、 "Jan", "Feb" などの月の略称をルックアップテーブルとして用意しておくのが無難な実装だと思います。
int がグルーバルだから遅い:
Fast string to integer conversion in Python という Q&A や Python Patterns - An Optimization Anecdote というエッセイにも書いてありますが、 数値文字列 (str 型) を整数 (int 型) に変換する int 関数はグローバルに定義されているため、 関数のルックアップに時間がかかるそうです。 数回の呼び出しなら影響はありませんが、百万回以上繰り返し実行すると意外と気になるはずです。 imap/mapitertools.chain を組み合わせることで、高速化を図れます。 (上記のスクリプトでは実装していませんが)

(*) プロファイラーの結果を見てみると、 calendar モジュールを読み込んでいます。 各国の言語によって月の略称 (calendar.month_abbr) は異なりますので、 これをごにょごにょするのに時間がかかるようですね。

終わりに

Apache のアクセスログを TSV 形式 (or LTSV) に変換するスクリプトを書いてみました。 すぐ出来るかと思ったら、Python 2.7 と Python 3.3 の互換性問題の解決に時間がかかったり、 ちょっと大きめのログファイルを流してみたら性能が出なかったりと、意外と学ぶことがありました。

当面の課題は解決できたので良いのですが、性能やポータビリティの向上を考えるなら、Go言語で書き直してみると良いかもしれませんね。

以前に書いた似たような記事:

コメントを投稿