開眼シェルスクリプト2012年5月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

5. 開眼シェルスクリプト 第5回

5.1. はじめに

 開眼シェルスクリプトも第5回になりました。
今回は、これまでのテクニックを駆使して、
アプリケーションを作ることに挑戦します。
お題はapacheのログを解析してアクセス数等をHTMLにするソフトです。
「車輪の再発明だ!」と言われてしまいそうですが、今回は
「ソフトをインストールして使いこなすまでの時間よりも早く作ってしまえ」
という立場で押し切ります。

5.1.1. 車輪を使って車輪を超高速再発明

 毎度おなじみガンカーズのUNIX哲学には、

「できる限り原型(プロトタイプ)を作れ。」

という項目がありますが、
これはコマンドを組み合わせたシェルスクリプトでさっさと動くものを作って人に見せろということです。
コマンドを使いまわす方法が手っ取り早いことは昔も今もそんなに変わっていません。
テトリスの上手な人はものすごいスピードでブロックを落としていきますが、
良く使うコマンドの組み合わせが一通り頭に入ると同じような感覚を味わうことになります。

 それに、コマンド自体は一つ一つ非常に有能で長い間使われてきた車輪です。
これを組み合わせてアプリケーションを作る行為自体は、
車輪を再発明しない工夫でもあります。
筆者はそれを再び広めたいのです。と言い訳して本題に入ります。

5.2. お題:お手製アクセス解析ソフトを作る

 アクセス解析ソフトとしては、Webalizerが有名で筆者も使っていますが、
もうちょっと気の利いたことをしたい場合に拡張するのは大変です。
グラフ以外はシェルスクリプトでさっと書けるので、自分で作ってしまいましょう。
グラフも、前回の応用でなんとかなります。

 解析対象は、USP友の会のウェブサイト(脚注:http://www.usptomonokai.jp)です。
このサイト、bash製という珍品ですが、ログはいたって普通です。
余計なことですが、このbashウェブシステムは私が半日で作り、
次の日@nullpopopo氏とネットワーク設定をして公開したものです。
車輪の再発明コストは相当低いと思います。

 ログは~/LOGの下に溜まっていくようになっていて、
root以外でも読み込み可能にしてあります。

↓リスト1: アクセスログ

1
2
3
4
5
6
[hoge@sakura LOG]$ 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

このデータを解析して、ブラウザで見やすいHTMLを作ることが、
お手製アクセス解析ソフトの目指すところです。

5.2.1. 準備

 作る前に、場所を作りましょう。適当な場所に、
リスト2のようにディレクトリを掘ってください。

↓リスト2: ディレクトリ構造

[hoge@sakura WWW]$ tree -L 1 WEB_KAISEKI
WEB_KAISEKI
|-- HTML  #HTMLのテンプレート置き場
|-- SCR   #シェルスクリプト置き場
`-- TMP   #作成したファイル置き場

「WEB_KAISEKI」というのがこのソフトの名前です。
(脚注:英語でなくてローマ字なのは業務上の癖なので突っ込まないでください。)

5.2.2. ログの整理

 まずSCRの下に、
3月号で作った「apacheのログをきれいにするスクリプト」を置きます。
前回Tukubaiコマンド(参照:http://uec.usp-lab.com)を紹介したので、
Tukubaiのコマンドを使って簡略化してリスト3に再掲します。
また、その他の部分も環境と目的に合わせて微調整してあります。

↓リスト3: apacheのログを整理するスクリプト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[hoge@sakura WEB_KAISEKI]$ cat SCR/HTTPD_ACCESS_NORMALIZE
#!/bin/bash

logdir=/home/hoge/LOG
dir=/home/hoge/WWW/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                                   |
#↑参考:https://github.com/ryuichiueda/SoftwareDesign/blob/master/201202/MONTH.sed
#時分秒のコロンを取る
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以降:今回不使用

 ここで使っているTukubaiコマンドは、 self です。
selfは、awkの文字を切り出す機能を単純化したコマンドです。
リスト4を見れば、awkのsubstrの動作と似ていることが分かると思います。
また、リスト1の15行目の 1/3 というのは1~3フィールド、
5/NF というのは5~最終フィールドのことです。

↓リスト4:selfの使用例

#第1Fと、第1Fの3文字目以降、第2Fの1文字目から2文字抽出
$ echo abcd 1234 | self 1 1.3 2.1.2
abcd cd 12
#次のawkと等価
$ echo abcd 1234 | awk '{print $1,substr($1,3),substr($2,1,2)}'
abcd cd 12

 その他、リスト3のスクリプトの変更点は次のとおりです。

 まず、7, 8行目は zcat $logdir/access_log*.gz と書いてもよいのですが、
ファイル数が非常に多くなるとエラーが出るのでそれを回避しています。
(こうしなくても10年は大丈夫なのですが。)
また、10行目で空データ """-" に変換してから
11行目でフィールド分割しています。
12行目のsedでは、 _ を全角の _ 、半角空白を _
一時的なデリミタである「あ」を半角空白に変換しています。
二つ以上の変換を一回のsedで行う場合は、
12行目のように-eというオプションを付けます。

このスクリプトを実行して、日付・時刻ソートされた以下のようなデータが得られればOKです。
第3フィールドにはIPアドレスやホスト名が記録されることがありますが、
今回は「IPアドレス」あるいは「IP」と表記します。

↓リスト5: ファイル「ACCESS_LOG」のレコード

1
2
3
4
5
6
7
#フィールド数は10列
#1:年月日 2:時分秒 3:IP 4,5:id 6:リクエスト 7:ステータス 8以降:今回不使用
[hoge@sakura WEB_KAISEKI]$ awk '{print NF}' TMP/ACCESS_LOG | uniq
10
[hoge@sakura WEB_KAISEKI]$ 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;)

5.2.3. 集計データをつくる

 さて、「きれいなデータ」ACCESS_LOGを作ったので、
次は自分の解析したい情報をそこから抽出します。
何をしようか考えたのですが、とりあえずWebalizerが出力する基本的な数値である
「Hits, Files, Pages, Visits, Sites」をちゃんと集計したいと思います。

項目 意味
Hits(ヒット数) access_logに記録されたレコード数
Files(ファイル数) Hitsのうち、正常にアクセスされた数
Pages(ページ数) 正常にアクセスされたページ(画面)数
Sites(サイト数) ヒット数の集計対象のレコード中にある、IPの種類の数
Visits(訪問数) ページ数の集計対象レコードから、30分以内の同一IPのレコードを重複として取り除いた数

 これらを時間単位で集計するシェルスクリプトをリスト6,7に示します。
これらのスクリプトを実行すると、TMP下にリスト8のようなファイルが出力されます。

↓リスト6: 集計スクリプト(hit,file,site数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# COUNT.HIT_FILE_SITE.HOUR: hit,file,siteの時間別集計
# written by R.Ueda (r-ueda@usp-lab.com)

cd /home/hoge/WWW/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:数

↓リスト7: 集計スクリプト(page,visit数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/bash
# COUNT.HOUR: page,visitの時間別集計
# written by R.Ueda (r-ueda@usp-lab.com)

tmp=/tmp/$$
cd /home/hoge/WWW/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-*

↓リスト8:出力

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[hoge@sakura 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

 ヒット数はただ単にログから年月日と時(時分秒の「時」)を切り出して数えるだけ、
ファイル数は、その処理の前に「正常」、
つまりステータスが200のレコードを抽出しています。
サイト数については、同時間内の重複を消してから数えています。

 リスト6で使われているcountはTukubaiコマンドです。
countは、文字通り数を数えるためのコマンドです。リスト9に例を示します。
オプションの 1 2 というのは、
第1フィールドから第2フィールドまでが同じレコードをカウントせよということです。
データは、第1、第2フィールドでソートされている必要があります。
uniq -c でも同じことができます。
余計な空白が入るのでパイプラインのなかでは扱いにくいですが。

↓リスト9:countの使用例

$ cat hoge
001 上田
001 上田
001 上田
002 鎌田
002 鎌田
$ count 1 2 hoge
001 上田 3
002 鎌田 2

 リスト6の23行目のsortに u というオプションがついていますが、
これは重複を除去するというオプション指定です。
sort -usort | uniq は同じことです。
安定ソートのオプション s は高速化のために付けています。

 ページ数については、記事のページを、
それ以外(画像、rssファイル、ajax用bashスクリプト)
と区別して抽出する必要があります。
このサイトでは、ページが呼び出されるときに必ず
TOMONOKAI_CMS.CGI というCGI(bash)スクリプトが呼ばれます。
urlだけ要求されたときは、
access_logには GET / HTTP1.0 などという記録が残ります。
(まれに GET // HTTP1.0 などと変則パターンがあって面倒です。)
リスト7の16行目のegrep(拡張正規表現の使えるgrep)で、
集計対象のページを抽出しています。
egrep 'AAA|BBB' という書式で、
「AAAまたはBBBを含むレコード」という意味になります。
また、14行目でurlから「?」以降の文字列(GETの値)
やその他不要なデータを消して誤抽出を防いでいます。
この部分は、自作するとかなり柔軟にカスタマイズできるが故に難しい部分ではあります。
面倒ならば拡張子だけ見ればよいと思います。

 フィルタされたログは、訪問数の集計でも使うことができるので、
17行目で $tmp-pages というファイルに保存されています。
teeは、標準入力をファイルと標準出力に二股分岐するコマンドです。

 訪問者数の計算は、ひねりがいります。
アルゴリズムを説明するために、
リスト7の26行目の処理が終わったあとのデータをリスト7に示します。

↓リスト7:sort後のデータの一部

1
2
3
4
5
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のレコードを取り除く必要があります。
その処理を行っているのが30, 31行目のawkです。
定義していないip, day, tmという変数をいきなり比較していますが、
awkは変数が出てきたときに初期化するので、
このようなさぼったコードを書くことができます。

 念のためこのawkが行っている処理を説明すると次のようになります。

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

この処理のあとは、毎時のレコード数をカウントするだけで訪問数になります。
(脚注:ただしリスト7の方法だと、
日をまたいで30分以内の閲覧が2カウントされます。)

5.2.4. HTMLを作る

 あとはデータをHTMLにはめ込みます。グラフを作ってみましょう。
5種類のデータも楽々出力・・・と言いたいところですが、
同じような処理の繰り返しでどうしてもコードの量がかさんでしまうので、
訪問数のグラフを描くところまでにします。

 先に、作るグラフのイメージを図1に示します。
縦に時間軸を置いて、上に新しい時間帯のデータが来るようにしましょう。
横軸の値は固定にしています。可変にもできますが、
訪問数はそんなに変化はしないので、やめておきます。
たくさん増えたら、喜んで作り直します。
また、座標のオフセット等の定数もハードコーディングですがご了承ください。

↓図1:訪問者数グラフ

_images/201205_1.png

 前回やりましたが、HTMLを作るときは、
HTMLのテンプレートを作りながらTukubaiコマンドの
mojihame等を使ってデータをはめ込んでいきます。
リスト8にテンプレート、リスト9にスクリプトを示します。
(脚注:BSD系の場合は、tacはtail、seqはjotで対応お願いします。)
スクリプトで行っていることは、横軸を作り、縦軸を作り、
棒グラフ用の座標を計算し、最後にそれぞれをmojihameするという処理です。

↓リスト8: HTMLテンプレート

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[hoge@sakura WEB_KAISEKI]$ 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>

↓リスト9: HTML生成スクリプト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[hoge@sakura WEB_KAISEKI]$ cat ./SCR/HTMLMAKE
#!/bin/bash
# HTMLMAKE: 訪問数のグラフを表示するHTMLファイルを作る
# written by R.Ueda (r-ueda@usp-lab.com) Feb. 26, 2012
tmp=/home/hoge/tmp/$$
dir=/home/hoge/WWW/WEB_KAISEKI

###データ採取
tac $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 > /home/hoge/visits.html

rm -f $tmp-*

 28行目のHTMLの出力先ですが、
apacheでHTMLが閲覧可能なディレクトリにリダイレクトしておけば、
ブラウザでの確認が可能になります。
また、cron等を使って、1時間に一度、
それぞれのスクリプトを順番に起動すれば、
自動で訪問数を集計するアプリケーションになります。

 項目を追加したければ、

  • グラフや表のデータをCGIスクリプトで作成
  • HTMLのテンプレートを記述
  • CGIスクリプトのmojihameを増やす

という作業をすることになります。日別の集計が必要な場合は、
リスト6,7のようなバッチのスクリプトを新たに作ればよいでしょう。

5.3. おわりに

 今回は、apacheのログを解析してグラフにするアプリケーションを作りました。
これだけ書いて制御構文がawkのif文1個だけで、
処理がすべて一方通行になっているのはシェルスクリプトの面白い性質だと思います。

 今回出てきたTukubaiコマンドはcountとmojihameでした。
mojihameの反則的威力は前回も紹介しましたが、今回はパイプで3個連結してみました。
countは、集計のための便利なコマンドです。
Tukubaiコマンドには、他にsm2などの足し算コマンドがあって、
集計などに使えますのでおいおい紹介します。

 次回はSQLの代わりにコマンドを使う方法を紹介します。
いわゆるNoSQLというやつですが、
シェルスクリプトを使うと極めて自然に実現できることを示したいと思います。

Pocket
LINEで送る