Perl/CGI研究室 'PERL-LABO'

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

アクセスIN/OUTカウンターの研究

研究内容

アクセス元URLを知り、ランキング表示する方法(リファラーカウンター)と、 クリックされたリンクのリンク先URLを知り、ランキング表示する方法(リンククリックカウンター)が分かりました。 この2つをまとめると、「ある特定の相互リンク先サイトさんから、どれだけの人が来て、どれだけの人が行ったかを 一覧表示する」ということもできそうです。 インターネット上の人の往来を目に見える形にする、なかなか面白い機能ですよね。挑戦してみましょう。

この研究で、 これまでに作ったランキンググラフを表示する関数について、 1.関数を整理して見やすくする  2.機能を追加する  3.連想配列での引数の受け渡しを行う  ということを学べます。

詳細

最終的に欲しいもの

リファラーカウンターで得られる情報と、 リンククリックカウンターで得られる情報を1つにまとめて表示する。 具体的には、次のような表を表示したいのです。

No.サイト名IN回数OUT回数
1サイトA100120
2サイトA8070
3サイトC5020

INというのは、そのサイトから来た数(リファラー数)。 OUTというのは、そのサイトに行った数(リンククリック数)です。 表示は、INの多い順。 リファラーカウンターの表に、OUTの欄を加えたようなものになります。

さて今回は、この表を表示するプログラムをゼロから作るのではなく、 既存のリファラーカウンターとリンククリックカウンターを使って組み立てます。 また、最終的には、 「総合カウンター」に 組み込みたいので、それも念頭においておきます。

処理手順を考えます

取得できるデータは、各サイトのIN数と、それとは別に各サイトのOUT数です。 これらは、既存のCGIプログラムによってファイルに保存されていますので、それを読み込みます。 読み込んだら、それをIN数でソートしたものを表示していきます。 そして表示するとき、対応するOUT数を一緒に表示する。 そういう感じになります。

既存のプログラムとチョコットだけ違うんだよなぁ

さて、ここからがちょっと頭を使うところです。 これまで作ったCGIプログラムと完全に独立してこのプログラムを作成するのであれば、 ガシガシ作っていけばいいのですが、今回はそうではないです。 既存のプログラムを利用するのです。 ですから、どう作るか?ではなくて、 既存のプログラムをどう利用するか?ということを考えるわけですね。

今回作ろうとしている表は 「No. サイト名 IN回数 OUT回数」 というデータを表示するものですが、  「No. サイト名 回数」 という表を表示するプログラムは、これまでに作ったものが既にあります。 この 「No. サイト名 回数」 という表を表示するプログラムと、  「No. サイト名 IN回数 OUT回数」 という表を表示するプログラムは、 処理の多くが同じ内容。違いは少しだけです。 その "違い" はどこなのか、既存のプログラムをどう利用したらスマートに今回やりたいことができるのか。 既存のプログラムを利用する場合は、そういう視点で考えていきます。

既存の関数の改良プロセス

では、「既存のプログラム」を見てみることにしましょう。 「No. サイト名 回数」という表を表示するプログラムです。 (実際は読まずに、読み飛ばしてください。)

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

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

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

	# 連想配列から内部データを取得、削除
	my $startdate  = $data{'_STARTDATE'};
	my $enddate    = $data{'_ENDDATE'};
	my $totalcount = $data{'_TOTALCOUNT'};
	my $maxcount   = $data{'_MAXCOUNT'};
	deletebardata_counters(\%data);
	if ($totalcount == 0) { $totalcount = 1; }

	# URLまとめる処理
	matomeru_counters(\%data, $mapfname);
	
	# 表示名データファイルを読む
	my %dispnamemap;
	if ($namefname ne "") {
		%dispnamemap = plab::readhashfile($namefname);
	}
	
	# カウント順にソートしたキーの配列を作成
	# カウントが同じ場合はキーの名前順
	sub cmpfunc {
		my $v1 = $data{$b};
		my $v2 = $data{$a};
		if ($v1 != $v2) { return ($v1 <=> $v2); }
		return $a cmp $b;
	}
	@sorted_keys = sort { cmpfunc } 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";
	my %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 nowrap>割合</td>";
	print "<td></td><td class=pl></td></tr>\n";
	my $prevcount = -1;
	my $prevrank = -1;
	for (0 .. @sorted_keys - 1) {
		my $rank  = $_ + 1;
		my $name  = $sorted_keys[$_];		# キー
		my $count = $data{$name};		# カウント
		my $width = int($count * $pixelperval);	# グラフ長さ
		my $per   = sprintf("%.1f", $count / $totalcount * 100);# 割合
		if ($mincount && $count < $mincount) {
			last;
		}
		if ($prevcount == $count) {
			$rank = $prevrank;
		}
		else {
			$prevcount = $count;
			$prevrank  = $rank;
		}
		$name =~ s/>/>/g;
		$name =~ s/</</g;
		print "<tr>\n";
		print "<td nowrap class=pl align=right>$rank.</td>\n";
		print "<td nowrap class=pl width=5></td>\n";
		
		my $dispname = $dispnamemap{$name};
		if ($dispname eq "") {
			$dispname = $name;
		}
		
		if ($dolink) {
			print "<td nowrap class=pl>";
			print "<a href=$name target=_blank>$dispname</a></td>\n";
		}
		else {
			print "<td nowrap class=pl>$dispname</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";
}

うぅ、長いです、汚いです(^^; 既存のソースを再利用するとき、安直にやりたくなるのがコピー&ペーストですが、 仮に、「既存のソースをコピー&ペーストして、手を加えて新しい関数にする」ということをしたら、 こんなに汚いソースの複製が作られることになります。それだけは、絶対にしてはいけません。 収集がつかなくなり、すぐに破綻してしまいます。

ではどうするか。まず、この関数をもっと細かい関数に分けて、ダイエットしましょう。 この関数を作った時点では、この関数が「少しだけ違う機能をもつ関数」を作るときに 再利用されることを考えていなかったため、こんな汚いソースになっていますが改善の余地は大いにあります。

1つの長い関数を、複数の小さな関数に分けるという作業は、 プログラミングにおいて、とても重要な技術の1つです。 この技術に長けていれば、最初から長い関数なんて作らずにもっとスッキリした関数が作れたはず(^^; でも、技術というのは一朝一夕では身に付きませんので、 こういう機会にウンウン唸りながらトライして いくといいですね。いい頭の体操になります(^^

そうして、ダイエットしたら、次のようになりました。(小分けにした関数の内容は省略しました。)


# カウントをランキング&グラフを出力します
# 引数(ファイル名, 保存されているログ数, 表示するログ番号, 
#       URLとしてリンクするかどうか, 表示する最低カウント)
sub view_counters
{
	my ($fname, $nlog, $logno, $dolink, $mincount, $mapfname, $namefname) = @_;

	# データの準備
	
	my %data = readforview_counters($fname, $logno);	# データ読み込み
	my %bardata = deletebardata_counters(\%data);		# 連想配列から内部データを取得、削除
	matomeru_counters(\%data, $mapfname);		# URLまとめる処理

	# 以下、HTMLの出力
	
	printhtml_logsel_counters($fname, $logno, $nlog);	# 過去ログ閲覧用リンク
	printhtml_rankingheader_counters(\%bardata);		# 計測期間など
	
	# グラフ表示メイン部
	printhtml_rankinggraph_counters(\%data, \%bardata, $namefname, $dolink, $mincount);
}

こういう状態にすると、だいぶ見通しが良くなりました。 この関数は「No. サイト名 IN回数」のグラフを表示するものでしたが、 「No. サイト名 IN回数 OUT回数」を表示するには次のようにすればいいんじゃないか、 という考えが自然に沸いてきました。(赤字が追加部分。)

# カウントをランキング&グラフを出力します
# 引数(ファイル名, 保存されているログ数, 表示するログ番号, 
#       URLとしてリンクするかどうか, 表示する最低カウント,
#	カウントをまとめるデータファイル名, 表示用文字列データファイル名,
#	データ2ファイル名, カウント名1, カウント名2)
sub view_counters
{
	my ($fname, $nlog, $logno, $dolink, $mincount, $mapfname, $namefname,
	    $fname2, $countname1, $countname2) = @_;

	# データの準備
	
	my %data = readforview_counters($fname, $logno);	# データ読み込み
	my %bardata = deletebardata_counters(\%data);		# 連想配列から内部データを取得、削除
	matomeru_counters(\%data, $mapfname);		# URLまとめる処理

	# データ2の準備
	
	my %data2;
	if ($fname2 ne "") {
	  %data2  = readforview_counters($fname2, $logno);	# データ読み込み
	  my %bardata2 = deletebardata_counters(\%data2);	# 連想配列から内部データを取得、削除
	  matomeru_counters(\%data2, $mapfname);		# URLまとめる処理
	}

	# 以下、HTMLの出力
	
	printhtml_logsel_counters($fname, $logno, $nlog);	# 過去ログ閲覧用リンク
	printhtml_rankingheader_counters(\%bardata);		# 計測期間など

	# グラフ表示メイン部 ↓この関数を改良します
	printhtml_rankinggraph_counters
		(\%data, \%bardata, $namefname, $dolink, $mincount,
		 \%data2, $countname1, $countname2);
}

先にダイエットをしておいたので、「機能を足すために必要なこと」が分かりやすかったです。 ダイエットせずにこれをしていたら…どれだけ複雑なコードになっていたことか(^^;

さてここでは、 「引数を増やして、その引数があるかどうかで関数内で処理を切り替える」という方法を 採用しました。 具体的には、引数 $fname2 の有無で処理を切り替えるという方針です。 これは、今までも何度かとってきた方法なのですが、 しかし今回、この方法だと、引数が10個になってしまいました。 このように、関数のにたくさんの機能を持たせようとすると、 引数の数がどんどん増えてしまいます。 引数が多くなると、引数の順番を間違えたりする可能性も高まりますし、なにより分かりにくいです。 関数のもつ機能が増えて、便利に使えるようになっても、どんどん分かりにくくなっていく。 「こういうやり方で、本当にいいのかなぁ…」という感じがしてきますね…。

そこで、もっといい方法は無いのかというと…、あるんです。 今までトライしたことが無かったのですが、情報を、連想配列に入れて関数に渡すという方法です。

連想配列で情報を関数に渡す、とは

これまで、関数には次のようにして引数を渡していました。

func($var1, $var2, $var3);	# 関数呼び出し

sub func			# 呼び出される関数
{
	my ($var1, $var2, $var3) = @_;
	…
}

これを、連想配列を使って次のようにすることができます。

func(			# 関数呼び出し
	name1 => $var1,
	name2 => $var2,
	name3 => $var3
);

sub func			# 呼び出される関数
{
	my %arg = @_;

	my $var1 = $arg{name1};
	my $var2 = $arg{name2};
	my $var3 = $arg{name3};
}

初めて見ると、なんじゃこりゃ、という感じですが、これは次のように解釈されて、処理されます。 まず、関数呼び出し部分。

func(
	name1 => $var1,
	name2 => $var2,
	name3 => $var3
);

name1 というのは、任意の名前で、連想配列のキーになるものです。 ここでは " で囲っていないのですが、" はここでは省略できるので書いていません。 "name1" のように書いても同じになります。

=> というのは、初めて出てきたかも知れませんが、 カンマ , と同じものです。 連想配列にデータを代入するときなどに、見やすくするために , の代わりに => を使います。 見た目の問題ですね。

$var1 は、関数に渡す、キー name1 に対応するデータになります。 文字列や数値だけでなく、参照を使えば、配列や連想配列も渡せます。

あと、上の例ではてきとうに改行を入れて見やすくしていますが、 改行も見やすくするために入れただけなので、次のようにしてもまったく同じです。

func(name1 => $var1, name2 => $var2, name3 =>$var3);

または、=> は , と同じなので

func(name1, $var1, name2, $var2, name3, $var3);

こうすると、この関数呼び出しが実は (name1, $var1, name2, $var2, name3, $var3) という配列を 関数に渡しているんだということが分かりますね。

関数側の処理ですが、

	my %arg = @_;

このように配列を連想配列に入れると、配列が (キー1, 値1, キー2, 値2, ...) のような 連想配列的なデータになっているものとして処理がされるんでした。 もともと関数に引数を連想配列の形式で渡しているので、正しく連想配列が作られます。 (配列の要素数が2の倍数になっていないとか、そういうことがあるとエラーになってしまいます。)

連想配列になったら、中身を取り出すのは簡単ですね。

	my $var1 = $arg{name1};

連想配列に直接キーを指定するときは、" を省略できるので、書いていませんが、 $arg{"name1"} としても同じです。

このようにして、連想配列を使って関数に引数を渡すことができます。 これは、引数1つ1つに名前を付けて関数に渡している感じですね。 連想配列を間に挟む分、処理が遅くなりますので、何度も呼び出される関数にこれを使うのはよくありませんが、 機能がたくさんあって、引数の数が多くて、省略可能な引数もたくさんあって、というような場合には、 とってもプログラムが見やすく、分かりやすくなるのでお勧めの方法です!

今回は、私、初めてこの方法を使ってプログラムを作ってみました。

作成したCGIプログラム

# カウントをランキング&グラフを出力します
# 引数(	* … 必須のもの
#	  fname		=> * ファイル名,
#	  logno		=> 表示するログ番号,
#	  nlog		=> 保存されているログ数,
#	  dolink	=> URLとしてリンクするかどうか,
#	  mincount	=> 表示する最低カウント,
#	  mapfname 	=> カウントをまとめるデータファイル名,
#	  namefname 	=> 表示用文字列データファイル名,
#	  countname	=> カウントの表示名,
#	  fname2	=> カウント2のファイル名,
#	  countname2	=> カウント2の表示名,
#	  );
sub viewex_counters
{
	my %arg = @_;
	
	(省略)
}

# カウントをランキング&グラフを出力します
# 引数(ファイル名, 保存されているログ数, 表示するログ番号, 
#       URLとしてリンクするかどうか, 表示する最低カウント,
#		カウントをまとめるデータファイル名, 表示用文字列データファイル名)
sub view_counters
{
	my ($fname, $nlog, $logno, $dolink, $mincount, $mapfname, $namefname) = @_;

	viewex_counters(
	  fname		=> $fname,
	  nlog		=> $nlog,
	  logno		=> $logno,
	  dolink	=> $dolink,
	  mincount	=> $mincount,
	  mapfname 	=> $mapfname,
	  namefname 	=> $namefname);
}

解説

新しく作った、連想配列で引数を受ける関数が1つ目の viewex_counters です。 関数の中の処理は、特に新しいことはありませんでしたので省略しました。

これまで作ったプログラムがそのまま動くように、既存の関数 view_counters は、 viewex_counters を呼び出すようにして、残しました。 この方法で改良するなら、互換性もバッチリですね。

動作については、当サイトの総合カウンターを見てください。 「IN/OUT」というヤツが、今回作ったヤツになります。

応用について

今回作った関数は、「2つのカウントをもつデータをランキング表示する」ということに使える汎用の関数なんです。 今回は「IN」と「OUT」の表示に使いましたが、実はコレ、 「月間カウント」と「週間カウント」の表示とか、「現在カウント」と「累積カウント」の表示など、 いろんな用途に使えるものです。こうやって、「いろんな用途に使える便利な関数」を作っておくと、 後で役に立つ!と思いますし、こうやって、資産が増えていくと、 これを応用した新しいアイデアも出やすくなりますし、プログラミングが楽に、そして楽しくなっていきますよ!

分かったこと

  1. 関数は、細かい機能に分けてダイエットすると、とっても分かりやすくなります。
  2. 関数が受ける引数の数が多くて分かりにくいときは、連想配列で渡すようにするとGOODです。
Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

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