Linux/Apache/アクセスログの数や国を見る

アクセスログの確認につかう、awk、perlのサンプル(2017-02-23、追記: 2017-03-02)


いつかは見ようと思ったアクセスログにようやく着手できました。
Google Analytics も見れるようにしたのですが、
Apacheのアクセスログと、Analytics の分析結果の違いも気になっていたので、
これを機に見てみました。


日ごとのアクセス件数の確認

まず、前提となる Apache のアクセスログの書式ですが
デフォルトの LogFormat はこうなっていると思います。

"%h %l %u %t \"%r\" %>s(以下省略)"

私の環境では以下のように、自分が加工しやすいように変えていますので
後述のスクリプト内で、
日付(%t)を egrep で拾うところや、
レスポンスコード(%>s)を awkで拾うところ($4)は読み替えてください。

"%{%y%m%d/%H:%M:%S}t %h %u %>s (以下省略)" 

そんなわけで、
ログから 日付で grep して、awkでカウントします。

# cat ./aclog_count.sh
#!/bin/sh

# アクセスログの場所を指定し、
# 今月の今日までのログを読み込む
ACLOG=/var/log/httpd/access_log*
min=1
ym=`date +%y%m`
max=`date +%d`

# 年月日指定する場合は以下をいじる
#ym=`date +%y11`
#min=1
#max=30

while [ $min -le $max ]; do
    ii=`printf '%02d' $min`

    echo "$ym$ii -----------------"

    egrep "^$ym$ii" $ACLOG | \
    grep -v "Googlebot" |\
    awk '
       BIGIN{
           cnt200 = 0
           cnt300 = 0
           cnt400 = 0
           cnt500 = 0
       }
       {
           if($4 ~ /2[0-9][0-9]/){
               cnt200++
           }
           else if($4 ~ /3[0-9][0-9]/){
               cnt300++
           }
           else if($4 ~ /4[0-9][0-9]/){
               cnt400++
           }
           else if($4 ~ /5[0-9][0-9]/){
               cnt500++
           }

       }
       END{
           printf("    200: %d\n", cnt200)
           printf("    300: %d\n", cnt300)
           printf("    400: %d\n", cnt400)
           printf("    500: %d\n", cnt500)
     }'
  min=`expr $min + 1 `
done

HTTPレスポンスについてざっくり説明すると、

-200番台が正常終了、

-300番台がリダイレクトなど、

-400番台がクライアントエラー(ページが見つからない、認証NGとか)

-500番台がサーバエラーです。

上の例では「grep -v Googlebot」で無視するログを指定していますが、
実際はここを調整して取捨選択します。例えば
「egrep -vi "robot|Googlebot|bingbot|siteexplorer"」等を除外するとか、
ブラウザ名を指定するとか、Refererで絞るとか。

~~検索ボットの分析スクリプトの例は後述

こんなイメージで、
大雑把にアクセス数を見てみたのですが、
それにしても 404 が多い・・・
どこぞのロボットの過去に消した記事へのアクセスとか
WordPressやDBの脆弱性を漁るためのアクセスとか

~~この辺が気になって、fail2ban を導入する ことになったのですが。

アクセスログから国の特定

流れとしては、各国のIP割り当てを調べて、
アクセス元のIPを調べて、
アクセス元がどの国かを調べる、という話です。

まずは、各国へのサブネット割り当て表を取ってきます。
表の中身については APNICのRIR書式説明(英語) を参照ください。

# wget ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest
# wget ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest
# wget ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest
# wget ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest
# wget ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest

これをIP/サブネット/国名の書式に加工します。
こんな感じのスクリプトを作りました。

 # cat subnet.pl
 #!/usr/bin/perl -w

 my $file;
 if($ARGV[0] eq '-') { $file = 'STDIN';}
 else{ open($file, "< $ARGV[0]") or die "$! [$ARGV[0]]\n";}

 while(<$file>){
     if(/^\w+\|(\w+)\|ipv(.)\|([\w\.:]+)\|(\d+)\|/){
         $cnt=$1; $ver=$2; $ip=$3; $num=$4;
         if($ver eq "4"){
             do {
                 if($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
                     $bin=$1*256*256*256
                     +$2*256*256
                     +$3*256
                     +$4; # IP表記から数値に戻す
                 }

                 # ホストIPからネットマスクの数をカウント
                 $bip=0;
                 for($p=$bin; $p%2==0; $p>>=1){$bip++;}

                 # 個数からマスクをカウント、小数点切り捨て
                 $sub=sprintf("%d", log($num)/log(2));

                 # ホスト、個数のどちらのマスクを使うか判定
                 if($sub>$bip){ $sub=$bip; }

                 printf("%s/%d %s\n", $ip, 32-$sub, $cnt);

                 $old = $num;
                 $num = $num - (2 ** $sub);
                 if($num > 0){
                     if($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
                    $b = $bin + $old - $num;
                        $ip = sprintf("%d.%d.%d.%d",
                           $b/(256*256*256),
                           $b/(256*256)%256,
                           $b/256%256,
                           $b%256
                        ); # 数値から、個数を足してIP表記に戻す
                     }
                 }
             } while($num > 0);

         }
         if($ver eq "6"){
             printf("%s/%d %s\n", $ip, $num, $cnt);
         }
     }
 }

(間違っているところあったらゴメンナサイ。
 1111000みたいにbitマスクを、頭の中で右に左に動かししながら書いたのですが・・・
 なお、Perlなら cpan で Net::Netmask とか手に入れるた方が早いかと)

で、上記の subnet.pl を使って、5つのファイルを変換します。

# for ff in delegated-*-extended-latest; do
./subnet.pl $ff >> list.txt
done

するとこんなファイルができます。

$ head list.txt
41.0.0.0/11 ZA
41.32.0.0/12 EG
41.48.0.0/13 ZA
41.56.0.0/16 ZA

いま作った list.txt を使って、IPアドレスから国を判別するのが
こちらのスクリプト。( list.txt のPATH はスクリプト内に直書きなので注意)

$ cat convert.pl.org
#!/usr/bin/perl -w

# 現状はIPv4だけで足りそうなので、
# v6は必要になったら考えます

my %ip_range;
my %ip_cntry;

# まず、サブネット 国名 一覧を読み込む
# こんな感じのファイルを読みたい
#   1.0.0.0/24 AU
#   1.0.1.0/24 CN
my $base_list = "./list.txt";
open($list, "< $base_list") or die "$! [$base_list]\n";
$i=0;
while(<$list>){
  if(/^([\w\.:]+)\/(\d+) (\w+)/){
    $ip=$1; $num=$2; $cnt=$3;
    if($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
        $bin=$1*256*256*256
        +$2*256*256
        +$3*256
        +$4; # IP表記から数値に戻す
        $ip_range{$bin} = $bin + (2 ** (32-$num));
        $ip_cntry{$bin} = $cnt;
        $i++;
    }
  }
}
# 引数にアドレスを渡した場合は、国判定してすぐ終了
if($ARGV[0] =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
    $bin=$1*256*256*256
    +$2*256*256
    +$3*256
    +$4; # IP表記から数値に戻す
    while(my ($start, $end) = each(%ip_range)){
        if($start <= $bin && $bin <= $end){
            printf("%s is %s\n", $ARGV[0], $ip_cntry{$start});
            exit;
        }
    }
    printf("%s is not found.\n", $ARGV[0]);
    exit;
}

# 次にアクセスログを読み込みつつ
# IPv4 アドレス部 を 国名に変換する
# アクセスログの書式に合わせて if 文内の正規表現を調整すること
my $file;
if($ARGV[0] eq '-') { $file = 'STDIN';}
else{ open($file, "< $ARGV[0]") or die "$! [$ARGV[0]]\n";}

$i=0;
while(<$file>){
    my $hit = 0;
    if(/^(.+) (\d+\.\d+\.\d+\.\d+) (.+)$/){
        $pre=$1; $ip=$2; $suf=$3;
        if($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/){
            $bin=$1*256*256*256
            +$2*256*256
            +$3*256
            +$4; # IP表記から数値に戻す

            foreach my $start (keys(%ip_range)){
                if($start <= $bin && $bin <= $ip_range{$start}){
                    printf("%s %s %s\n",
                            $pre,
                            $ip_cntry{$start},
                            $suf );
                    $hit++;
                    last;
                }
                $i++;
            }
        }
    }
    if(! $hit){
        # 変換できない場合なので、基本的にはここには来ない想定
        print $_;
    }
}

これを使って、

# ./convert.pl  /var/log/httpd/access_log | head 

あるいは標準入力から読込

# grep keyword /var/log/httpd/access_log | ./convert.pl -

国が英語二文字(JPとか)なので、それを調べるのがめんどくさい方は
こちらが強化版 です。※長くなるので分けました

これで念願の、国別アクセス状況を知ることができました。
それにしても、むかし掲示板荒らしを見たときに分かっていたものの、
想定外に多国籍であることが判明・・・そうだったのか・・・うーん、ぐろーばる

おまけ:URLの日本語デコード

Wikiで日本語のURLクエリが「%~」になりますが、nkf コマンドで日本語で戻せます。
が、いま手元に nkf が入って無い(が、perl はある)ので
戻し方をメモっておきます。 (echo は「の」をエンコードした文字、それをパイプで拾って日本語へデコードする例。)

echo "%E3%81%AE" |\
perl -e 'while(<STDIN>){ s/%([0-9a-fA-F]{2})/pack("H2",$1)/ge; print $_; }'

おまけ:検索ボットを数える

さきほど、grep -v で検索エンジンのアクセスは除外する、
なんて簡単に言いましたが、ログ見始めて分かりましたが、これが多い!
次々に新しいbotがくるのですが、主要なものだけでもどの程度のアクセスがあるのか、
参考までに調べてみることにしました。

以下、bot_count.sh
ACLOGにアクセスログを指定し、mm・min・maxに月日指定、
正規表現 regex にパイプ(|)区切りで検索単語を羅列して、
「egrep "^17$mm$ii" $ACLOG」はお手元のアクセスログの日付書式に合わせてください。

# cat bot_count.sh
#!/bin/sh

# アクセスログの場所を指定し、
# 今月の今日までのログを読み込む
ACLOG=/var/log/httpd/access_log*
mm=`date +%m`
min=`date +%-d --date "1 day ago"`
max=`date +%-d`

# 月日指定する場合は以下をいじる
# 今回は 2月の統計をとりたいので、変数を上書き
mm="02"
min=1
max=28

regex="Googlebot|webmeup-crawler|bingbot|siteexplorer|msnbot|MJ12bot|Baiduspider|Yahoo! Slurp|Uptimebot|SemrushBot"

while [ $min -le $max ]; do
    ii=`printf '%02d' $min`

    echo "$mm$ii -----------------"

    # 一行目の mm ii の部分はアクセスログの書式に合わせて整形する
    egrep "^17$mm$ii" $ACLOG | \
    egrep -i "$regex" |\
    awk -v keyword="$regex" '
    {
        # 各キーワードごとの件数をカウントする
        # (変数keyword で、正規表現指定。今回はロボットですね)
        if(match($0, keyword) ){
            keyword_list[substr($0,RSTART,RLENGTH)]++
        }
    }
    END{
        # 配列 keyword_list の
        # 添え字i は キーワード、中身はヒット数(アクセス数) になる
        for (i in keyword_list) {
            printf("  %16s : %d\n", i, keyword_list[i]);
        }
    }' | tee robot_list$mm$ii.txt

    min=`expr $min + 1 `
done

# 過去何回アクセスしてきているかを調べる
echo "--- total days ----------------"
awk -F ":" '{
     a[$1]++;
  } END{
     for (i in a){
         printf("%16s :%02ddays\n", i, a[i]);
     }
  }' robot_list$mm* |\
 sort -t: -k2 |\
 tee robot_list_days.txt

実行イメージはこちら

# ./bot_count.sh
0201 -----------------
           bingbot : 12
       Baiduspider : 1
         Uptimebot : 2
   webmeup-crawler : 63
         Googlebot : 6
           MJ12bot : 2
0202 -----------------
(中略)
0228 -----------------
          bingbot : 2
      Baiduspider : 2
        Uptimebot : 3
     siteexplorer : 9
        Googlebot : 70
          MJ12bot : 23
--- total days ----------------
      siteexplorer  :06days
        SemrushBot  :07days
            msnbot  :08days
   webmeup-crawler  :10days
      Yahoo! Slurp  :13days
           MJ12bot  :21days
         Uptimebot  :26days
       Baiduspider  :27days
           bingbot  :28days
         Googlebot  :28days

他にも botっぽいのは要るのですが、ざっと見ただけでもこれだけあります。
数は大したことないにしても、結局毎日アクセスしてきているのですね。

あとは、botだと分かるものは良心的で、
User-Agentは自己申告だから、
どこぞのブラウザを名乗っていても実はbotかもしれません。

Google Analytics と比べて

Analitycs は JavaScript や Cookie やらを使っているはずなので
それらをオフにしているWeb閲覧者や閲覧ツール・ロボは統計に乗らない筈です。
それらを踏まえると、以下の内容も納得かと。

まず目につくのは、検索エンジン・ロボットの類。
アクセスログだと大量に見つかるので、これらを排除して分析する必要があります。
逆に、ロボットやツールからの負荷・攻撃とかは
Analyticsよりもアクセスログから探す方が良いでしょう。
・・・随分むかしに消したはずのページにず~っとアクセスがあって、
どうやら robots.txt で Disallow するまで永遠に続く模様。

セッション数、つまり
1ユーザが複数ページをうろついて、出ていくまでが1セッション?というカウントは
Analystics を使うと便利で見やすいですね。
これをアクセスログから追うならセッションを可視化するネタを仕込む必要があります。
(URLクエリやらCookieやらにセッションを識別する目印を仕込むとか)
Analystics の「行動フロー」画面とか、ちょっと感動しました。

ようやく自分で調べることができた海外からのアクセスは
ロボットを除外しても、Analyticsで表示された以外の国からも来てますね。
Linux周りとか、私と同様に藁にも縋る思いで検索しまくっているのかも・・・
(や、自分のサイトにも英語表記とか、あると便利なのかな、と思ったのですが、
 どうやら英語だけでは全然足りないようなので・・・Google翻訳がんばれ!)


余談ですが、
これが性能調査のためのログ解析とかだと、
もう少し細かく統計取って、グラフ化までを自動化します。
例えば
時分秒単位の平均応答時間、200/300/400/500番台の応答数を
CVSファイル化して、
Excelに読み込ませてグラフ化すると見やすいです(読込、グラフ化までもVBAで自動化する)

  • 秒単位も必要なのは、アクセス数には波があって、波が高い瞬間のデータが要るからです
  • 自動化が重要なのは、結局何回も計測&分析を繰り返すことになるからです

色々書いたり、貼ったりして長くなってしまいましたが・・・

結論:

Analysticsは便利、だけど

私はシェル & Perl大好き、CUI 万歳な人なので、

アクセスログを直接見た方が、なんかこう、楽しいです。


  最終更新のRSS