~/LOG/以下に保存されているApache HTTPd Serverのアクセスログを解析して、ブラウザから閲覧できるHTMLファイルを作成することを今回のお題とする。
$ ls -ltr access_log* | tail -n 5 -rw-r--r-- 1 root root 82408 2月 22 03:32 access_log-20120222.gz -rw-r--r-- 1 root root 61438 2月 23 03:32 access_log-20120223.gz -rw-r--r-- 1 root root 70638 2月 24 03:32 access_log-20120224.gz -rw-r--r-- 1 root root 60125 2月 25 03:32 access_log-20120225.gz -rw-r--r-- 1 root root 744255 2月 25 22:02 access_log $
ソフトウェアは次のディレクトリ構造を持つものとする。
$ tree -L 1 WEB_KAISEKI WEB_KAISEKI |-- HTML # HTMLのテンプレート置き場 |-- SCR # シェルスクリプト置き場 `-- TMP # 作成したファイル置き場 $
まずSRCにログを自在に加工する(2) ― ログの整形と集計で作成したApacheのログを整形するスクリプトを配置する。スクリプトはOpen usp Tukubaiを使用するように内容を書き換えてある。
$ cd ~/WEB_KAISEKI/ $ cat SCR/HTTPD_ACCESS_NORMALIZE #!/bin/sh -vx logdir=~/LOG dir=~/WEB_KAISEKI echo $logdir/access_log*.gz | xargs zcat | cat - $logdir/access_log | sed 's/""/"-"/g' | sed 's/\(..*\) \(..*\) \(..*\) \[\(..*\)\] "\(..*\)" \(..*\) \(..*\) "\(..*\)" "\(..*\)"$/\1あ\2あ\3あ\4あ\5あ\6あ\7あ\8あ\9/' | sed -e 's/_/_/g' -e 's/ /_/g' -e 's/あ/ /g' | #1:IP 2,3:id 4:日時 5-9:リクエスト以降 self 4.4.3 4.1.2 4.8.4 4.13.8 1/3 5/NF | #1:月 2:日 3:年 4:時:分:秒 5:IP 6,7:id 8-12:リクエスト以降 sed -f $dir/SCR/MONTH | #時分秒のコロンを取る awk '{gsub(/:/,"",$4);print}' | #年月日の空白を取る awk '{print $3$1$2,$4,$5,$6,$7,$8,$9,$10,$11,$12}' | #1:年月日 2:時分秒 3:IP 4,5:id 6:リクエスト以降 sort -s -k1,2 > $dir/TMP/ACCESS_LOG #1:年月日 2:時分秒 3:IP 4,5:id 6:リクエスト 7:ステータス 8以降:今回不使用 $
self(1)がOpen usp Tukubaiのコマンドだ。awk(1)における文字の切り出しに特化したような作りになっている。
このスクリプトを実行して次のようなデータが得られればよい。
$ awk '{print NF}' TMP/ACCESS_LOG | uniq 10 $ tail -n 2 TMP/ACCESS_LOG 20120225 221853 72.14.199.225 - - GET_/TOMONOKAI_CMS/CGI/TOMONOKAI_CMS.CGI_HTTP/1.1 200 16920 - Feedfetcher-Google;_(略) 20120225 221946 210.128.183.1 - - GET_/TOMONOKAI_CMS/HTML/rss20.xml_HTTP/1.0 200 10233 - Mozilla/4.0_(compatible;) $
次に解析したいと考えるデータを整形したデータから抽出する。ここではとりあえずWebalizerが出力する基本的なデータであるHits、Files、Pages、Visits、Sitesの集計を実施する。
これらを集計するスクリプトは次のようになる。
#!/bin/sh # COUNT.HIT_FILE_SITE.HOUR: hit,file,siteの時間別集計 cd ~/WEB_KAISEKI/TMP ###ヒット数 self 1 2.1.2 ACCESS_LOG | #1:年月日 2:時 count 1 2 > HITS.COUNT #1:年月日 2:時 3:数 ###ファイル数 awk '$7==200' ACCESS_LOG | self 1 2.1.2 | #1:IP 2:時 count 1 2 > FILES.COUNT #1:年月日 2:時 3:数 ###サイト数(時間別) self 1 2.1.2 3 ACCESS_LOG | #1:日付 2:時 3:IP sort -su | count 1 2 > SITES.COUNT #1:年月日 2:時 3:数
#!/bin/sh # COUNT.HOUR: page,visitの時間別集計 tmp=/tmp/$$ cd ~/WEB_KAISEKI/TMP ###ページ数 #ステイタス200、メソッドGETのデータだけ awk '$7==200 && $6~/^GET/' ACCESS_LOG | self 1/3 6 | #1:年月日 2:時分秒 3:IP 4:リクエスト #プロトコルや?以降の文字列を削る sed -e 's;_HTTP/.*$;;' -e 's;\?.*$;;' | #集計対象を検索 egrep 'GET_//*$|TOMONOKAI_CMS\.CGI$' | tee $tmp-pages | self 1 2.1.2 | count 1 2 > PAGES.HOUR ###訪問数 #1:年月日 2:時分秒 3:IP 4:リクエスト self 3 1 2 2.1.2 2.3.2 $tmp-pages | #1:IP 2:年月日 3:時分秒 4:時 5:分 #$4,$5を分に換算(頭にゼロがあっても大丈夫) awk '{print $1,$2,$3,$4*60+$5}' | #1:IP 2:年月日 3:時分秒 4:分 #IP、年月日、時分秒でソートする sort -k1,3 -s | awk '{if(ip!=$1||day!=$2||$4-tm>=30){ print;ip=$1;day=$2;tm=$4}}' | self 2 3.1.2 1 | #1:年月日 2:時 3:IP sort -k1,2 -s | count 1 2 > VISITS.HOUR rm -f $tmp-*
$ tail -n 1 ./*.HOUR ==> ./FILES.HOUR <== 20120225 21 125 ==> ./HITS.HOUR <== 20120225 21 189 ==> ./PAGES.HOUR <== 20120225 21 51 ==> ./SITES.HOUR <== 20120225 21 34 ==> ./VISITS.HOUR <== 20120225 21 25 ...
count(1)はOpen usp Tukubaiのコマンドだ。数を数えるコマンドで、1 2という引数は第1フィールドから第2フィールドまでが同じレコードをカウントせよ、という指定になっている。この場合、第1および第2フィールドは整列されている必要がある。
$ cat tran 001 上田 001 上田 001 上田 002 鎌田 002 鎌田 $ count 1 2 tran 001 上田 3 002 鎌田 2 $
sort(1)の-uは重複を排除するという指定で、sort | uniqと同一の意味になる。
ヒット数はただ単にログから年月日と時(時分秒の「時」)を切り出して数えるだけ、ファイル数はその処理の前に正常(ステータス200)のレコードを抽出、サイト数については同時間内の重複を消してから数えている。ページ数については対象のページからカウントする必要がない画像やCSSファイルなどを除外してカウントしている。
フィルタされたログは訪問数の集計でも使うことができるので、ここでは一旦$tmp-pagesというファイルに保存している。訪問者数の計算は少々頭をひねる必要がある。まず、sort(1)を実施した後の処理途中のデータは次のようになっている。
95.108.246.253 20120212 203105 1231 95.108.246.253 20120212 235718 1437 95.108.246.253 20120213 150603 906 95.108.246.253 20120213 150605 906 95.108.246.253 20120213 151252 912
左から順にIPアドレス、年月日、時分秒と並び、最後に時分秒を分に直した数字が入っている。この最後のフィールドをレコードの上から比較していって、30分以上離れていない同一IPのレコードを取り除く必要がある。その処理はawk(1)で実施している。awk(1)で行っている処理を説明すると次のようになる。
この処理のあとは、毎時のレコード数をカウントするだけで訪問数になる。
あとはデータをHTMLにはめ込んで表示させればよい。ここでは縦を時間軸として、新しいデータが常に上に表示されるような訪問数のグラフを描くことにする。
$ cat HTML/TEMPLATE.HTML <!DOCTYPE html> <html> <head><meta charset="UTF-8" /></head> <body> <div>訪問数</div> <svg style="height:1000px;width:400px;font-size:12px"> <!--VALUEAXIS--> <!--背景帯・軸目盛・目盛ラベル--> <rect stroke="black" x="%2" y="20" width="15" height="1000" style="fill:lightgray;stroke:none" /> <line stroke="black" x1="%2" y1="15" x2="%2" y2="20" /> <text x="%3" y="10">%1</text> <!--VALUEAXIS--> <!--横軸--> <line stroke="black" x1="50" y1="20" x2="350" y2="20" /> <!--TIMEAXIS--> <!--目盛・目盛ラベル--> <line x1="45" y1="%1" x2="350" y2="%1" style="stroke:white;stroke-width:1px" /> <text x="0" y="%1">%2日0時</text> <!--TIMEAXIS--> <!--縦軸線--> <line stroke="black" x1="50" y1="20" x2="50" y2="1000" /> <!-- VISITS --> <!--グラフ--> <line x1="50" y1="%1" x2="%2" y2="%1" stroke-opacity="0.6" style="stroke:red;stroke-width:2px" /> <!-- VISITS --> </svg> </body> </html>
$ cat ./SCR/HTMLMAKE #!/bin/sh # HTMLMAKE: 訪問数のグラフを表示するHTMLファイルを作る tmp=/tmp/$$ dir=~/WEB_KAISEKI ###データ採取 tail -r $dir/TMP/VISITS.HOUR > $tmp-data ###数値(縦)軸。原点は20px下 seq 0 9 | awk '{print $1*10,$1*30+50,$1*30+45}' > $tmp-vaxis #1:ラベル 2:目盛座標 3:文字列座標 ###時間軸。原点は50px左 #tmp-data: 1:日付 2:時 3:数 awk '$2=="00"{print NR*2+20,$1}' $tmp-data | self 1 2.7 > $tmp-taxis #1:縦座標 2:日 ###訪問数をくっつけてHTMLを出力 #1:日付 2:時 3:数 awk '{print NR*2+20,$3*5+50}' $tmp-data | #1:縦座標 2:値 mojihame -lVISITS $dir/HTML/TEMPLATE.HTML - | mojihame -lVALUEAXIS - $tmp-vaxis | mojihame -lTIMEAXIS - $tmp-taxis > ~/visits.html rm -f $tmp-* $
cron(8)を使って1時間ごとにスクリプトを起動すれば、自動で訪問数を集計するアプリケーションになる。
これだけの処理をしておいて、制御構文がawk(1)のif構文ひとつだけというのが、興味深いところといえる。処理がすべて一方通行になっており、上から下へを読み進められるようになっている。
Software Design 2012年5月号 上田隆一著、「テキストデータならお手のもの"開眼☆シェルスクリプト" : 【5】アクセス解析ソフトを作る ― ログ集計とHTML出力の応用」より加筆修正後転載。
※ usp Tukubaiはユニバーサル・シェル・プログラミング研究所の登録商標。