UECジャーナル

開眼☆シェルスクリプト アクセス解析ソフトを作る ― ログ集計とHTML出力の応用

今回のお題:お手製アクセス解析ソフトの作成

~/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)で行っている処理を説明すると次のようになる。

  1. ipと第1フィールドを比較
  2. dayを第2フィールドと比較
  3. 第4フィールドとtmの差が30分以上か調べる
  4. 1~3の結果、残すレコードであれば出力して、そのレコードの情報をip, day, tmに反映

この処理のあとは、毎時のレコード数をカウントするだけで訪問数になる。

あとはデータを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ユニバーサル・シェル・プログラミング研究所の登録商標。

last modified: 2014-01-13 16:01:13