アクセスカウンタ

1.はじめに

ホームページを立ち上げたら、だいたいどれぐらいのアクセスが、 どのページが人気があるのか知りたくなるのが人情です。 たいていのプロバイダはアクセスカウンタ用のURLを用意してくれていますし、 無料のアクセスカウンタも多数あるので、ホームページにカウンタを設置するのは簡単です。

でも、全部のページにアクセスカウンタを付けようとすると、難しくなります。 このサイトのプロバイダであるBIGLOBEでも、用意してくれているカウンタの数は、 有料のカウンタサービスを使っても、1ユーザあたり最大5ページまでです。

2.アクセスカウンタの作り方一般論

アクセスカウンタを作る代表的な方法は、CGIでアクセスカウンタの画像を作り、 IMGタグでページに埋め込む事で、ウェブの初期から用いられてきました。 しかしこれはBMPやGIF等のグラフィックを扱わなければいけないので、 素人プログラマが自分で作ろうとすると、ちょいと難易度が高い方法です。

素人プログラマにとって一番簡単な方法はSSI(Server Side Include)を用いる方法です。 Apacheなどのサーバは、ウェブページに入れたコマンドを認識して、 サーバ側でコマンドを実行する機能があります。 これを使えば、文字列を出力する簡単なカウンタプログラムを書くだけで、 ウェブページの中に文字列としてカウンタを埋め込むことができます。 しかもIMGと異なり、ブラウザで画像を読み込まなくてもカウンタが動くことや、 外部からのパラメータ入力が不要でハッキングがされにくいなどの利点があります。 以前に、自分でサーバを全て立ち上げた時には、この方法でカウンタを動かしました。 でも残念ながらBIGLOBEではSSIをサポートしていません。

SSIに似た方法で、CGIでSSIと同様の機能を実行し、 ウェブページにカウンタを埋め込む方法があります。 でも、全てのページをCGIにしなければならず、ちょっとかっこ悪いし、 検索エンジンから外れてしまう可能性があります。

そんなこんなで、長年、カウンタを付けるのをあきらめていたのですが、 最近のAjaxのはやりをみて、ふと、Ajaxならできるかなと思いました。 調べてみると、Ajaxなんて難しいことをしなくても、 CGIで動的に生成したJavaScriptをウェブページに取り込んで文字列を出力すればいいことに気づきました。 JavaScript未対応のブラウザやJavaScriptを止めたブラウザには対応できず、 SSIに比べれば精度が悪いですが、ないよりましと作ってみました。

CGIとJavaScriptを組み合わせてカウンタを作る

まず、ウェブページに取り込むJavaScriptですが、これはとても簡単です。 例えば、以下のJavaScriptを実行すると、現在の場所に1234の文字列が書かれます。

document.write('1234');

ウェブページに書く内容も、とても簡単です。 例えば、以下の様なHTMLを書きます。 http://www.example.jp/count.cgiがJavaScriptを動的に生成するCGIのURLです。

あなたは<script type="text/javascript" src="http://www.example.jp/count.cgi"></script>番目のお客様です
注:ブラウザのキャッシュにより、srcの値が同じだとページが異なっていてもサーバへのアクセスが再実行されないようです。 本当は、src="http://www.example.jp/count.cgi?PageName"として、ページ毎にPageName部分を変えたほうがよさそうです。 でも、そうするとサイトのページ毎に記述を変えないといけないので、サイトページをつくるのがちょっと面倒です。

最後に大事なのは、カウンタのCGIプログラムです。 作り方はいろいろあると思いますが、私の作ったのは以下の様なものです。 CGIと相性がいいという理由でperlを使っています。

カウンタのCGIプログラム

#!/usr/local/bin/perl
$url = "http://www.example.jp/";    # サイトの基本url

# 日付取得(あまり意味はない)
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime( time() );
$year += 1900;
$mon++;

$counter = &countername;            # カウンタ名を決定するサブルーチンを呼び出し

# カウンタを読む
# 1度目の&countingは月単位のアクセス回数の収集です。
# 2度目の&countingでカウンタの値の読み出しと更新をしています。
if( $counter eq "" ) {
  $num = 0;
} else {
  &counting( sprintf("%04d%02d.txt", $year, $mon), $counter );
  $num = &counting( "countdata.txt", $counter );
}

# CGI出力
# まずは、CGIヘッダの出力です。
# JavascriptのContent-type:は、text/plainみたいです。
print "Content-type: text/plain\n";

# CGI結果がキャッシュされないようにします。
# ブラウザによって解釈できるヘッダが違うので、3通り指定します。
# 結果的にブラウザの「進む」「戻る」を繰り返してもカウンタが進んでしまいますが、
# そこはやむなし。
# ブラウザによっては無視されるかもしれないが、それもやむなし。
print "Cache-Control: no-cache\n";
print "Pragma: no-cache\n";
print "Expires: Sun, 10 Jan 1990 01:01:01 GMT\n";

# CGIヘッダの終わりを示す空行。
print "\n";

# CGI出力の本体です。
if( $num > 0 ) {
  print "document.write('$num');";
}

# --- サブルーチン ---

# 環境変数HTTP_REFERERからサイトのURLを除いたものをカウンタ名にします。
# CGIでは、環境変数HTTP_REFERERにはリンク元のURL、
# この場合、カウンタ値を埋め込むウェブページのURLになります。
sub countername {
  # 環境変数HTTP_REFERERの文字列を取得
  $para = "$ENV{HTTP_REFERER}";
  # 文字列の最後が "/" なら、省略された"index.html"を補います。
  if( substr($para, length($para)-1, 1) eq "/" ) {
    $para = $para."index.html";
  }
  # このサイトのURLから始まるかチェックし、サイト名を除去
  if( substr($para, 0, length($url) ) eq $url ) {
    $para = substr($para, length($url) );
    # パラメータに変な記号がないかチェックする。
    # なるべく厳密にチェックするのはハッキング対策です。
    if ( $para =~ m/^(?:[0-9a-zA-Z_][0-9a-zA-Z_\-\.]*\/)*[0-9a-zA-Z_][0-9a-zA-Z_\-\.]*\.html?$/ ) {
      # URLに対するファイルが存在するかチェック
      if( -e $para ) return ($para);
    }
  }
  return ("");
}

# カウンタ&ロギング 
# エラーの場合は0を返します。
sub counting {
  # ローカル変数定義。
  local ($filename, $countername, $num, $i);
  # サブルーチンの引数
  ($filename, $countername) = @_;
  $num = 0;
  # ファイルオープン
  # 既にファイルがある場合とない場合で、開き方を変えています。
  if( -e $filename ) {
    open(FH, "+< $filename") || return (0); 
  } else {
    open(FH, "+> $filename") || return (0); 
  }
  # 読み込みループ(ただし無制限の登録を避けるため10000回に制限)
  for ($i = 0; $i < 10000; $i++) {
    # ローカル変数定義。
    local ($pos, $data, $cnm, $cmt);
    # ファイルの最後に達していたら、新たな行を生成
    if( eof(FH) ) {
      # 数字の1の後の空白は、カウンタが増えた場合に大きな値を書き込むためです。
      printf(FH "1          ,$countername,%d/%d/%d\n", $year, $mon, $mday);
      $num = 1;
      last;                                  # ループ終了
    }
    $pos = tell(FH);                         # 現在のファイルポインタの位置を記憶
    $data = <FH>;                            # ファイルを1行読む
    ($num, $cnm, $cmt) = split(/,/, $data);  # カンマ区切りの要素を取出す
    if( $cnm eq $countername ) {             # カウンタが見つかった場合の処理
      $num ++;                               # カウンタ値増加
      # 記憶したファイルポインタ位置に戻り、カウンタ値を書き戻す
      seek( FH, $pos, 0);
      print FH substr( "$num " , 0, 10);     # カウンタの桁数は10桁に制限
      last;                                  # ループ終了
    }
  }
  # ファイルクローズ
  close(FH);
  return ($num);
}
注:HTTP_REFERERに値が設定されていない場合、ログを残したほうがいいかもしれません。

1つのファイルに全てのカウンタ値を書き込むようにしました。 テキストファイルなので、履歴のダウンロードは簡単です。 いちをcsv形式なので、他のソフトへの取り込みも容易だと思います。 排他制御はサボったので、処理が競合すると、値がおかしくなるかもしれません。

このプログラムでは、CGI環境変数HTTP_REFERERによって元のページを認識しているので、 ブラウザが参照元のURLを送ってくれないとうまく動作しませんが、 大体のケースで環境変数HTTP_REFERERは有効のようなのでこのままにしてあります。 CGIのパラメータを環境変数QUERY_STRINGから取り込んで使ったほうが確実性は高いかもしれません。

CGI環境変数のREMOTE_HOST、REMOTE_ADDR、REMOTE_USER等を使用たり、 Cache-ControlやPragmaやExpiresヘッダをうまく使うことで、 ユーザが続けて見た場合にカウントアップしないようにする事も出来そうですが、 そこまで本格的に作っても、カウンタの精度が向上するとは思えないので、サボりました。

その他

実際にCGIを動かす場合、それぞれのサーバ毎の仕様に従わなければなりません。 BIGLOBEのwww5d.biglobe.ne.jpのCGI仕様では、 CGIファイルとCGI設置ディレクトリのパーミッションは『705(rwx---r-x)』または『755(rwxr-xr-x)』でなければなりません。 セキュリティのためgroupやotherの書き込み権がある場合にCGIを起動しないようにガードしている様です。 perlのコマンドパスは/usr/local/bin/perlなので、それをCGIファイルの先頭、#!の後にに書きます。

ちなみに、このサイトのカウンタは、このとおりです。 この一覧表を出力するCGIは以下の通り、簡単な内容です。

#!/usr/local/bin/perl
# サイトの基本url
$url = "http://www5d.biglobe.ne.jp/~stssk/";

# ファイル読み込み
# @file変数にファイル全体が読み込まれる。
open(FH, "< countdata.txt") || die "Open Error"; 
@file = <FH>;
close(FH); 

# カウント数の大きい順にソート
# 行のカンマ区切りの中から、最初のデータを取り出して数字として比較します
@sorted = sort({(split(/,/, $b))[0] <=> (split(/,/, $a))[0]} @file);

# CGIのヘッダとHTMLのヘッダ
print "Content-type: text/html\n\n";
print "<html>\n";
print "<head>\n";
print "<title>Counter log</title>\n";
print "</head>\n";
print "<body>\n";
print "<h1>$url Accses Counter</h1>\n";

# CGI出力
print "<table border=1>\n";
print "<tr><td>File Name</td><td>Count</td></tr>\n";
foreach $data (@sorted) {
  ($num, $cnm, $cmt) = split(/,/, $data);
  print "<tr><td>";
  print "<a href=\"$url$cnm\">$cnm</a>";
  printf "</td><td align=right>%d</td></tr>\n", $num;
}
print "</table>\n";

# HTMLの終わり
print "</body></html>\n";

戻る戻る
[番目]記述内容について一切保障しません。リンクは自由に行ってかまいません。