Perl/CGI研究室 'PERL-LABO'

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

グラフを美しく

研究内容

ページビューカウンターもそろそろ完成? ではグラフをもう少し綺麗に表示するようにしましょう。

詳細

見た目も重要…

正直言って私はデザインとか凄く苦手です。このサイトを見てもらえれば分かる通りです (^^; でも現在のページビューカウンターのグラフ表示はあまりにもしょぼいのでちょっと変更しようと思います。 見た目も重要ですからね。 綺麗にというのもありますが、なによりも分かりやすく表示するのが大事だと思います。

結果

ライブラリの変更

見た目、つまりHTML出力部分のみを変更するつもりだったのですが、 そろそろページビューカウンターも実用になるレベルになってきたように思うので、 あらためてライブラリの見直し、少し修正を行いました。 詳細は後で説明します。

作成したCGIプログラム

pvcounter.cgi
#!/usr/bin/perl

require 'pvcounter.pl';
require 'counters.pl';
require 'getformdata.pl';
require 'html.pl';

$fname = 'pvcounter.txt';	# データファイル名
$nlog = 2;			# 保存する過去ログの数
$logspan = 2;			# ログを回転する日数間隔

print "Content-Type: text/html\n\n";

%form = plab::getformdata();

if ($form{'mode'} ne "graph") {
	print plab::count_pvcounter($fname, $nlog, $logspan);
}
else {
	$logno = $form{'log'};	# 表示するログ番号
	plab::printhtmlheader();
	plab::view_counters($fname, $nlog, $logno, 1);
	plab::printhtmlfooter();
}
pvcounter.pl
# v 1.06

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

package plab;

require 'counters.pl';

# 引数(ファイル名, 保存するログ数, ログ回転日間隔)
# ページビューをカウントします
sub count_pvcounter
{
	local $fname   = $_[0];
	local $nlog    = $_[1];
	local $logspan = $_[2];

	local $count;

	local $ref  = $ENV{'HTTP_REFERER'};

	if ($ref ne "" && !($ref =~ /\?/i)) {
		$count = plab::incl_counters($fname, $ref, 1, $nlog, $logspan);
	}

	return $count;
}

1;
counters.pl
# v 1.05

# (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 ($todaydateint - $counters{'_STARTDATEINT'} >= $logspan) {
		# ログローテート
		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;
}

# データを読み込みます
# 引数(ファイル名, ログ番号)
# _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 = plab::read_counters($fname, $logno);

	# 連想配列から内部データを取得、削除
	local $startdate  = $data{'_STARTDATE'};
	local $enddate    = $data{'_ENDDATE'};
	local $totalcount = $data{'_TOTALCOUNT'};
	local $maxcount   = $data{'_MAXCOUNT'};
	deletebardata_counters(\%data);
	
	# 閲覧回数順にソートした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();
	local $script = $ENV{'SCRIPT_NAME'};
	for (0 .. $nlog) {
		local $linkname;
		local $linklogfname = getlogfname($fname, $_);
		if (-e $linklogfname) {
			if ($_ == 0) { $linkname = "現在"; }
			else         { $linkname = "過去" . $_; }
			$form{'log'} = $_;
			local $query = plab::formdata2query(%form);
			if ($linklogfname eq $logfname) {
				print "[<b>$linkname</b>] ";
			}
			else {
				print "[<a href=$script?$query>$linkname</a>] ";
			}
		}
	}
	print "</div>\n";
	print "<br>";

	# グラフ
	print "<div class=plh>ランキング</div>\n";
	print "<br>";
	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";
	for (0 .. @sorted_keys - 1) {
		local $rank = $_ + 1;
		local $url = $sorted_keys[$_];		# URL
		local $val = $data{$url};			# 閲覧回数
		local $width = int($val * $pixelperval);	# グラフ長さ
		local $per = sprintf("%.1f", $val / $totalcount * 100);# 割合
		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>$val</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";
}

1;

実行結果

当サイト全体のページビューグラフです。 アクセスカウンターと同様、アクセスがとても少ないのでとても恥ずかしいのですが (^^; 今回の研究で、見た目はスッキリしたと思います。 見た目をキレイに、の結果がここにあります (^^;

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

解説

変更点について

各ファイルのプログラムの変更点についてちょっと説明します。

pvcounter.cgi

呼び出す関数名を変えました。 関数名は後から気分で変えたりしてたら他への影響が大きくて大変。 でも、まだ利用段階に入っていないうちなら、関数名を変更してもそれほど影響は ありません。逆に言えば、関数名を変えるなら今しかない! ということで、次のように関数名を変えました。

countpageview → count_pvcounter
printgraphwithhref_counters → view_counters

この、ほにゃらら_なんちゃら という関数名のスタイルってなんか分かりやすくないですか? ここでは、ライブラリファイル名を後ろに付けているんですが、なんとなく分かりやすいような気が しますのでこうしました。

pvcounter.pl

ここは、変わっていないですね。

counters.pl

read_counters 関数を追加しました。この関数でデータを読み込みます。 データファイルは、キー<>カウント,IPアドレス という形のデータで、 重複チェックのためのIPアドレスも一緒に保存されています。 これを削除する処理をしています。あと、トータルのカウントと、最大のカウントを算出して、 それを _TOTALCOUNT、_MAXCOUNT というキーでそれぞれ保存します。 この、アンダーバー _ で始まるキーのデータは、内部データでカウントには関係のないデータ、 というルールにしているんでした。こうやって、先に前処理してから 連想配列を返すことで、この関数を呼び出した関数側での処理を少なくしてあげようという 感じです。

もう1つ、deletebardata_counters 関数を追加しています。 これは、連想配列から、_ で始まるキーを削除するっていう関数です。 read_counters が返す連想配列には、いくつか _ で始まる内部データが入っていますので、 これを削除するときに簡単なようにこの関数を作りました。 最初が _ であることを確認するために、substr という関数を使っています。これは、次のように使います。

substr($str, 位置, 長さ)

これで、その位置の指定した長さの部分文字列になります。 位置というのは、最初の文字を 0 としたときの位置です。1 からではないので注意しましょう。 そんなわけで、最初が _ かどうかを調べるには if (substr($k, 0, 1) eq "_") 〜 とすればいいことになります。 で、連想配列からデータを削除するには delete 関数を使います。

さて、この deletebardata_counters 関数ですが、ちょっと新しいことをしています。 というのは、連想配列を連想配列のまま関数に渡しているんです! 連想配列は、関数の引数に例えば func(%hash) として渡すと、 全ての要素が展開されて キー,値,キー,値… となってしまうんでした。 ここでは、そうでなくて、連想配列を連想配列として受け取っています。 これには連想配列の参照というものを使います。連想配列を連想配列のまま関数に渡すには、 関数の引数で次のようにします。

func(\%hash)

連想配列の前に \ を付けることで、これが参照であることを示します。 参照というのは1つの値です。つまり、連想配列を1つの参照という値として関数に渡すということに なります。

関数側では、これを次のようにして使います。

$hashref = $_[0];	# 参照をコピーしています。
%$hashref;	# これで連想配列になります。
$$hashref{'KEY'};	# これで連想配列の値にアクセスします。

なんだか変数の前に付ける記号が増えてごちゃごちゃしますが、これだけならそんなに難しくないです。 今まで連想配列を使うのに %hash、$hash{キー} としていたのが、$ が1個付いて %$hash、$$hash{キー} となっただけです。これが連想配列の参照へのアクセスの仕方になります。 ただ、普通の連想配列と、連想配列の参照との区別をしっかりしないと混乱しますので、 変数名の後ろに ref とつけています。これは、参照ですよっていう意味です。 参照は英語で reference ですので。

これで、連想配列を連想配列のまま(正確には連想配列の参照として)関数に渡す方法が 分かりました。今回の研究のテーマとは関係無かったのですが、 これが今回の研究の1番重要な部分だったかも知れません (^^;

さて、最後ですが、今回の目玉である、グラフを表示する view_counters 関数です。 もともとは printgraphwithhref_counters という名前だったものですが、 今回の変更でだいぶ変わりました。 とはいっても、変わったのはHTML出力のところだけですね (^^;

動きました

さて、グラフ画面の方はどうでしょうか。 当サイトで作成したCGIプログラムということで、 当サイトのデザインとなんとなく同じデザインです。 ていうか、当サイトのデザインなんてデザインと呼べないようなものですけど (^^; もともとセンスが無いのでこんな感じですが、 このサイトはPerl/CGIの研究サイトですのでHTML表示部分に関しては これくらいでカンベンしてください (^^;

分かったこと

  1. 変数の頭に \ を付けると参照になります。連想配列の場合は \%hash
  2. 連想配列の参照には次のようにアクセスします。 %$hashref $$hashref{KEY}
Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

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