2011年11月24日木曜日

シェルってなんじゃ?(sedとsortでアクセスログのソート)


Apacheなどのアクセスログで複数のホストの同一日付のファイルをAWSTATSなどにかけることがあります。
その場合logresolvemergeなどを利用するのですが、logresolvemergeは各ファイルが内部でソートされていることを前提としています。
ログは書きこみプロセスなどの関係で時折内部で日時が前後することがあるため、ファイルの内部をソートしなおす必要があります。

スクリプト言語などのループなどを利用してもできるのですが、ここではシェルを使った方法を紹介したいと思います。

使うコマンドは
  • cat
  • sort
  • sed
です。

たとえばログの中身が
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET / HTTP/1.1" 304 -
のようになっているファイルが数十個あるとします。

ここでは例としてhoge1.log, hoge2.logの以下の2ファイルをサンプルとして使用します。
$ cat hoge1.log
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Dec/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [05/Nov/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [18/Apr/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [22/Nov/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -

$ cat hoge2.log
125.54.146.207 - app [27/May/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [01/Nov/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [13/Oct/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [11/Mar/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -


cat
まずこれらのファイルを連結します
$ cat *
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Dec/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [05/Nov/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [18/Apr/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [22/Nov/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [27/May/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [01/Nov/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [13/Oct/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [11/Mar/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -
連結しただけなのでまだ順番は変わりません。

つぎにその出力を普通にソートしてみます。
sortコマンドには以下のようにセパレータで区切られたフィールドと範囲指定でソートキーを優先順に複数定義することができる-kオプションがあります。

以下は、区切り文字を半角スペースとして1行をフィールド分割したときの2番目のフィールドの2文字目から3文字目を第一ソートキー、1番目のフィールドを第2ソートキーとすることを表します。

$ cat test.log
b A03z 2
a A12f 3
a B03d 1

$ sort -t " " -k 2.2,2.3 -k 1,1 test.log
a B03d 1
b A03z 2
a A12f 3

これを利用して以下のログファイルをソートしてみます。

$ cat hoge1.log
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Dec/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [05/Nov/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [18/Apr/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [22/Nov/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -

$ cat hoge2.log
125.54.146.207 - app [27/May/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [01/Nov/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [13/Oct/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [11/Mar/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -

$ cat * | sort -t " " -k 4.9,4.12 -k 4.5,4.7 -k 4.2,4.3 -k 4.14,4.15 -k 4.17,4.18 -k 4.20,4.21
125.54.146.207 - app [22/Nov/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [01/Nov/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [18/Apr/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [21/Dec/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [11/Mar/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [27/May/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [05/Nov/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [13/Oct/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827

うまくいきませんでした。
これはログを出力したマシンのロケールの問題で、月のフィールドが3文字コードになっているため、文字列では大小を比較できないためです。

こういった場合のためにsortには、-Mオプションが用意されており、Jan < ... <  Decという解釈ができるようになっているのですが、残念ながらフィールドとして独立していないと機能しないようです。またそれ以外のソートキーは文字列として解釈したいので、このオプションは結局使用できませんでした。


sed
仕方ないので、Jan〜Dec を 01〜12に置換してからsortする必要があるようです。
そこでsedを利用します。

sedは文字列の置換が得意なプログラムです。
ここでは/Nov/ を /11/と置換するようにします。

$ cat * | sed -e "s:/Jan/:/01/:" -e "s:/Feb/:/02/:" -e "s:/Mar/:/03/:" -e "s:/Apr/:/04/:" -e "s:/May/:/05/:" -e "s:/Jun/:/06/:" -e "s:/Jul/:/07/:" -e "s:/Aug/:/08/:" -e "s:/Sep/:/09/:" -e "s:/Oct/:/10/:" -e "s:/Nov/:/11/:" -e "s:/Dec/:/12/:"
125.54.146.207 - app [21/11/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/12/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [05/11/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [18/04/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [22/11/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [27/05/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/11/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [01/11/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [13/10/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [11/03/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -

sedではこのように-eオプションを連結することで、個々の置換条件を複数適用することができます。また、パラーんの最初のsの直後をセパレータとみなすので、/以外でもセパレータとして使用することができ、パターン内に/がある場合などに便利です。


sort
それではこの結果をソートにかけるようにパイプで連結してみます。
$ cat * | sed -e "s:/Jan/:/01/:" -e "s:/Feb/:/02/:" -e "s:/Mar/:/03/:" -e "s:/Apr/:/04/:" -e "s:/May/:/05/:" -e "s:/Jun/:/06/:" -e "s:/Jul/:/07/:" -e "s:/Aug/:/08/:" -e "s:/Sep/:/09/:" -e "s:/Oct/:/10/:" -e "s:/Nov/:/11/:" -e "s:/Dec/:/12/:" | sort -t " " -k 4.8,4.11 -k 4.5,4.6 -k 4.2,4.3 -k 4.13,4.14 -k 4.16,4.17 -k 4.19,4.20
125.54.146.207 - app [22/11/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [01/11/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [11/03/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [18/04/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [27/05/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [13/10/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [05/11/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [21/11/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/11/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/12/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -

うまくソートできました。

sed
これでもいいのですが、月がソート用に数字変換されたままですので、最後に元に戻してあげます。
$ cat * | sed -e "s:/Jan/:/01/:" -e "s:/Feb/:/02/:" -e "s:/Mar/:/03/:" -e "s:/Apr/:/04/:" -e "s:/May/:/05/:" -e "s:/Jun/:/06/:" -e "s:/Jul/:/07/:" -e "s:/Aug/:/08/:" -e "s:/Sep/:/09/:" -e "s:/Oct/:/10/:" -e "s:/Nov/:/11/:" -e "s:/Dec/:/12/:" | sort -t " " -k 4.8,4.11 -k 4.5,4.6 -k 4.2,4.3 -k 4.13,4.14 -k 4.16,4.17 -k 4.19,4.20 | sed -e "s:/01/:/Jan/:" -e "s:/02/:/Feb/:" -e "s:/03/:/Mar/:" -e "s:/04/:/Apr/:" -e "s:/05/:/Apr/:" -e "s:/06/:/Jun/:" -e "s:/07/:/Jul/:" -e "s:/08/:/Aug/:" -e "s:/09/:/Sep/:" -e "s:/10/:/Oct/:" -e "s:/11/:/Nov/:" -e "s:/12/:/Dec/:"
125.54.146.207 - app [22/Nov/2001:15:30:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [01/Nov/2010:05:29:01 +0900] "GET /moge/ HTTP/1.1" 304 -
125.54.146.207 - app [11/Mar/2011:14:54:28 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [18/Apr/2011:15:30:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [27/Apr/2011:16:34:01 +0900] "GET /change/hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [13/Oct/2011:13:08:26 +0900] "GET /hoge.php HTTP/1.1" 200 13827
125.54.146.207 - app [05/Nov/2011:15:29:01 +0900] "GET / HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:11:22 +0900] "GET /moge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Nov/2011:15:30:28 +0900] "GET /hoge.js HTTP/1.1" 304 -
125.54.146.207 - app [21/Dec/2011:15:30:28 +0900] "GET /moge.js HTTP/1.1" 304 -

これで完成です。