Perl/CGI研究室 'PERL-LABO'

Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

トータルカウントを別ファイルに保存

研究内容

トータルカウントも保存、閲覧できるような仕組みを導入しましょう。

詳細

一定期間毎リセットの弊害

ページビューカウンターは最初は設置してからのトータルカウントを保存、表示していました。 その後、一定期間毎に現在のカウントを過去ログに移動してカウントをリセットするっていう機能を付けました。 その結果、トータルのカウントは知ることができなくなっていました。 そのデメリットを考えても、一定期間毎にリセットした方がベターだと思ったのですが、 やっぱりトータルのカウントも知りたいですよね。 そのように改良してみましょう。

結果

トータルカウントを得る方法

方法を2つ考えました。

最初に考えたのは、一定期間カウント用ファイルに対してカウントアップしたあと、 トータルカウント用ファイルに対してもカウントアップをするっていう方法。 カウントを2回するわけです。 片方は一定期間毎にリセットされますが、もう片方はリセットしません。 このやり方ならプログラムもたぶん難しくありません。 でも…処理する量が単純に2倍になりますよね。 ファイルアクセスも2倍。 トータルカウントを知りたいというだけのために、 負荷が2倍? まあ、負荷といってもとても小さなものでしょうが、 なんか気持ち悪いです。 それならトータルカウント無しでもいいか…というのが この方法を考えたときの印象でした。で、ボツです。

もう1つの方法は、カウントをリセットするときに、 カウントを別のトータルカウント用ファイルに足すっていうものです。 手順としては、「トータルカウントをファイルから読む」「現在のカウントをファイルから読む」 「トータルカウントに現在のカウントを加える」「トータルカウントをファイルに書き込む」です。 この処理は、カウントをリセットするときにしか行われません。 カウントリセットは、多くても1日に1回です。なので、 サーバーへの負荷という点ではまったく問題無いです。 このやり方を思いついたときに、おっ!これならトータルカウントも 保存できるぞ!凄い!という感じでした。 いいアイデアが浮かぶと、嬉しくなりますね (^^

変更箇所

カウントリセットを行うときに、上に書いた通りの手順を行います。 この変更はそれほど難しくありませんでした。 この処理は、ファイル名に、data.t.cgi みたいに .t. を付けたものをトータルカウントファイルとして、 自動的に行われます。 加えて、カウント一覧表示画面に、[トータル] というリンクを加えました。 トータルのカウントを画面に表示するときには、単にトータルカウントのファイルの内容を 表示するのではなくて、トータルカウントファイルの内容に、 現在のカウントファイルの内容を加えたものを表示します。 現在のカウントファイルは、まだトータルファイルに足されていない分ですから。 ここまでやれば、完璧ですね!

作成したCGIプログラム

counters.pl
# v 1.06

# (c) PERL-LABO
# http://www.perl-labo.org/

# 文字列-カウント のペアを扱うライブラリ
# 過去ログの処理も行います

package plab;

require 'getformdata.pl';
require 'hashfile.pl';
require 'lock.pl';
require 'stdplab.pl';

# 引数(ファイル名, カウントを上げるキー,
#	IPアドレス重複チェックをするかどうか1/0,
#	保存するログ数, ログ回転日間隔)
# 戻値(カウント)
sub incl_counters
{
	local $fname   = $_[0];
	local $key     = $_[1];
	local $ipcheck = $_[2];
	local $nlog    = $_[3];
	local $logspan = $_[4];

	local $lockdir = $fname . ".lockdir";
	local $ip      = $ENV{'REMOTE_ADDR'};

	# 引数チェック
	if ($logspan <= 0) {
		return "ARG ERROR";
	}

	# ロック
	if (! plab::lock($lockdir)) {
		return "ERROR";
	}

	# データ読み込み
	%counters = plab::readhashfile($fname);

	# バージョン互換:データ取得開始日付記録
	if (! exists $counters{'_STARTDATE'}) {
		$counters{'_STARTDATE'}     = plab::getcurrentdatestring();
		$counters{'_STARTDATEINT'}  = plab::getcurrentdateint();
	}

	# 経過日数チェックとログ保存
	local $todaydateint = plab::getcurrentdateint();
	if ($nlog > 0 && $logspan > 0 && 
	    $todaydateint - $counters{'_STARTDATEINT'} >= $logspan) {
		# トータルファイルにカウントを追加します
		local %totalcount = readtotal_counters($fname);
		plab::writehashfile(getlogfname($fname, 't'), %totalcount);
		# ログローテート
		plab::rogrotate($fname, $nlog);
		# カウントのリセット処理
		undef(%counters);
		$counters{'_STARTDATE'}     = plab::getcurrentdatestring();
		$counters{'_STARTDATEINT'}  = plab::getcurrentdateint();
	}

	# データ取得終了日付記録
	$counters{'_ENDDATE'} = plab::getcurrentdatestring();

	# カウントアップ
	($count, $previp) = split(',', $counters{$key});
	if ($ipcheck == 0 || $ip ne $previp) {
		++$count;
		$counters{$key} = "$count,$ip";
	}

	# データ書き込み
	plab::writehashfile($fname, %counters);

	# アンロック
	plab::unlock($lockdir);

	return $count;
}

# トータルカウントを読み込みます
# トータルカウントファイルのデータに現在のデータを加えたものです
# 引数(現在のカウントファイル名)
sub readtotal_counters
{
	local $fname = $_[0];

	# 現在のカウントを読み込みます
	local %currentdata = read_counters($fname, 0);

	# トータルのカウントを読み込みます
	local %totaldata = read_counters($fname, 't');

	# 日付データを削除します

	# カウントを加えます
	while (($k, $v) = each(%currentdata)) {
		if (substr($k, 0, 1) ne "_") {
			$totaldata{$k} += $v;
		}
	}

	# 計測開始、終了日付を処理します
	if (! exists($totaldata{'_STARTDATE'})) {
		$totaldata{'_STARTDATE'} = $currentdata{'_STARTDATE'};
	}
	$totaldata{'_ENDDATE'} = $currentdata{'_ENDDATE'};

	# 連想配列の値を閲覧回数のみにする
	# ついでにトータルカウント、最大カウントを算出
	local $totalcount = 0;
	local $maxcount   = 0;
	while (($k, $v) = each(%totaldata)) {
		if (substr($k, 0, 1) ne "_") {
			$totalcount += $v;
			if ($maxcount < $v) {
				$maxcount = $v;
			}
		}
	}
	$totaldata{"_TOTALCOUNT"} = $totalcount;
	$totaldata{"_MAXCOUNT"}   = $maxcount;

	return %totaldata;
}

# データを読み込みます
# 引数(ファイル名, ログ番号)
# _STARTDATE に 計測開始日付
# _ENDDATE に 計測終了日付
# _TOTALCOUNT に トータルカウント
# _MACCOUNT に 最大カウント
# が入っています
# これらデータを削除するには deletebardata_counters を使います
# この関数で読み込んだデータを再びファイルに上書きしてはいけません
sub read_counters
{
	local $fname = $_[0];
	local $logno = $_[1];

	# ログファイル名を得る
	local $logfname = getlogfname($fname, $logno);

	# データ読み込み
	local %data = plab::readhashfile($logfname);

	# 連想配列の値を閲覧回数のみにする
	# ついでにトータルカウント、最大カウントを算出
	local $totalcount = 0;
	local $maxcount   = 0;
	while (($k, $v) = each(%data)) {
		if (substr($k, 0, 1) ne "_") {
			($count, $ip) = split(',', $v);
			$data{$k} = $count;
			$totalcount += $count;
			if ($maxcount < $count) {
				$maxcount = $count;
			}
		}
	}

	$data{"_TOTALCOUNT"} = $totalcount;
	$data{"_MAXCOUNT"}   = $maxcount;

	return %data;
}

# 連想配列の中の _ で始まるキーを削除します
# 引数(連想配列の参照)
sub deletebardata_counters
{
	local $dataref = $_[0];

	local($k, $v);
	while (($k, $v) = each(%$dataref)) {
		if (substr($k, 0, 1) eq "_") {
			delete($$dataref{$k});
		}
	}
}

# カウントをランキング&グラフを出力します
# 引数(ファイル名, 保存されているログ数, 表示するログ番号, 
#       URLとしてリンクするかどうか)
sub view_counters
{
	local $fname  = $_[0];
	local $nlog   = $_[1];
	local $logno  = $_[2];
	local $dolink = $_[3];

	# 閲覧するファイル名を取得
	local $logfname = plab::getlogfname($fname, $logno);

	# データ読み込み
	local %data;
	if ($logno ne "t") { %data = plab::read_counters($fname, $logno); }
	else               { %data = readtotal_counters($fname); }

	# 連想配列から内部データを取得、削除
	local $startdate  = $data{'_STARTDATE'};
	local $enddate    = $data{'_ENDDATE'};
	local $totalcount = $data{'_TOTALCOUNT'};
	local $maxcount   = $data{'_MAXCOUNT'};
	deletebardata_counters(\%data);
	if ($totalcount == 0) { $totalcount = 1; }
	
	# 閲覧回数順にソートしたURLの配列を作成
	@sorted_keys = sort { $data{$b} <=> $data{$a} } keys(%data);

	# 閲覧回数1回に対応するグラフの棒の長さを算出
	$maxval = $data{$sorted_keys[0]};
	if ($maxval == 0) { $maxval = 1; }
	$pixelperval = 100 / $maxval;

	# 以下、HTMLの出力

	# 過去ログ閲覧用
	print "<div class=plh>ログ</div>\n";
	print "<br>";
	print "<div class=pl>\n";
	local %form = plab::getformdata();
	view_counterA($fname, 0, $logfname, "現在");
	print " ";
	for (1 .. $nlog) {
		view_counterA($fname, $_, $logfname, "過去" . $_);
	}
	print " ";
	view_counterA($fname, 't', $logfname, "トータル");
	print "</div>\n";
	print "<br>";

	# グラフ
	print "<div class=plh>ランキング</div>\n";
	print "<br>";
	if ($startdate eq "") {
		print "<div class=pl>データがありません。</div>\n";
		return;
	}
	print "<div class=pl>\n";
	print "$startdate 〜 $enddate (計 $totalcount カウント)\n";
	print "</div>\n";
	print "<br>";
	print "<div class=pl>\n";
	print "<table cellpadding=1 border=0 width=90%>\n";
	print "<tr><td></td><td></td><td></td><td></td>\n";
	print "<td class=pl nowrap>回数</td><td></td>";
	print "<td class=pl>割合</td>";
	print "<td></td><td class=pl></td></tr>\n";
	local $prevcount = -1;
	local $prevrank = -1;
	for (0 .. @sorted_keys - 1) {
		local $rank  = $_ + 1;
		local $url   = $sorted_keys[$_];		# URL
		local $count = $data{$url};			# 閲覧回数
		local $width = int($count * $pixelperval);	# グラフ長さ
		local $per   = sprintf("%.1f", $count / $totalcount * 100);# 割合
		if ($prevcount == $count) {
			$rank = $prevrank;
		}
		else {
			$prevcount = $count;
			$prevrank  = $rank;
		}
		print "<tr>\n";
		print "<td nowrap class=pl align=right>$rank.</td>\n";
		print "<td nowrap class=pl width=5></td>\n";
		if ($dolink) {
			print "<td nowrap class=pl>";
			print "<a href=$url target=_blank>$url</a></td>\n";
		}
		else {
			print "<td nowrap class=pl>$url</td>\n";
		}
		print "<td nowrap class=pl width=5></td>\n";
		print "<td nowrap class=pl align=right>$count</td>\n";
		print "<td nowrap class=pl width=5></td>\n";
		print "<td nowrap class=pl align=right>$per%</td>\n";
		print "<td nowrap class=pl width=5></td>\n";
		print "<td nowrap class=pl width=100%>";
		print "<span class=plg style=\"width:$width%\"> </span></td>\n";
		print "</tr>\n";
	}
	print "</table>\n";
	print "</div>\n";
}
# ファイルの存在をチェックし、存在すればリンクを出力します
sub view_counterA
{
	local ($fname, $logno, $currentlogfname, $name) = @_;

	local $logfname = getlogfname($fname, $logno);
	if (-e $logfname) {
		local %form = plab::getformdata();
		$form{'log'} = $logno;
		local $query = plab::formdata2query(%form);
		if ($logfname eq $currentlogfname) {
			print "[<b>$name</b>] ";
		}
		else {
			print "[<a href=?$query>$name</a>] \n";
		}
	}
}

1;

実行結果

当サイト全体のページビューグラフです。 ページ上部に [トータル] というリンクが付いています。

当サイトのページビューグラフ

解説

変更点について

counters.pl に変更です。 変更箇所はカウントリセットを行うところ

# トータルファイルにカウントを追加します
local %totalcount = readtotal_counters($fname);
plab::writehashfile(getlogfname($fname, 't'), %totalcount);

ここでは、新しく加えた readtotal_counters 関数を呼んでします。 この関数は、 「トータルカウントをファイルから読む」「現在のカウントをファイルから読む」 「トータルカウントに現在のカウントを加える」までを行って、そのデータを返します。 ですから、後は「トータルカウントをファイルに書き込む」だけ。 readtotal_counters 関数の中は、特に新しいことはしていません。 あと、[トータル] というリンクを足す部分ですが、 [現在] [過去] などのリンクを足すプログラムと処理を共通にするために、 view_counterA 関数として独立させています。 ファイルの存在をチェックして、ファイルが存在していたら、 そのファイル内のカウントを画面表示するためのリンクを出力します。 ですから、トータルファイルが最初に保存されるまで、 [トータル] というリンクは表示されません。

動きました

今回は結構、気持ちよかったですね。 なにがって、単に動いたっていうんじゃなくて、 自分で考えたアルゴリズムが動いたっていうか。 このヘンがプログラムの面白いところですね。

あと、今回、Perl/CGIプログラミングに関する新しいことはでてきませんでしたが、 「一定期間でリセットされるデータは、リセットするときにトータルファイルに加えることで、 サーバーに負荷なくトータルも保存できる」ということを学びました。 これ、またどこかで役に立つかな?

Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

Copyright (c) 'PERL-LABO' All Rights Reserved.  リンクフリーです。