Perl/CGI研究室 'PERL-LABO'

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

検索キーワードを調査する

研究内容

検索エンジンからの訪問者様がどのような検索キーワードでサイトに来られたのか調査する研究です。

詳細

検索エンジンからの訪問…気になりますよね

前回、リファラーを調査するCGIプログラムを作りました。 訪問者様がどこのリンクをたどってやってきてくれたのかを調べるCGIプログラムですが、 そのCGIプログラムを設置してみて、気がついたことがあります。 それは、検索エンジンからの訪問者様がおられるっていうことです。 googleとか、yahooなどの検索エンジンがアクセス元になっているので分かります。 そうすると、ちょっと気になります…どんなキーワードでこのサイトがヒットしたのでしょう。 訪問者さんはどんな情報を知りたくてこのサイトに訪問したのかな、 知りたい情報はあったのかな…と、妙に気になってしまいます。 知りたい情報があったかどうかは、各ページに設置したページアンケートに 答えてくれることを期待するしかありませんが、どんなキーワードで検索したのかっていうのは 実は知る方法があるんです! そこで、検索キーワードを取得して、キーワードランキングを作る研究にトライしましょう。

検索キーワードを知る方法

検索キーワードは、リファラーを利用して知ることができます。 検索エンジンは、検索語を入力すると検索結果を表示しますが、 この時ありがたいことにURLの中に検索語が入っているんです。

考えてみると、検索はフォームからキーワードを入力して行いますよね。 フォームからのデータ送信は、GETメソッドとPOSTメソッドがあるんでした。 検索エンジンは、GETメソッドを使っているんです。 そのため、URLの後ろの ?〜 のところに、入力した検索語が含まれているっていうわけです。

もしPOSTメソッドを使っていたら、URLには何も付きません。 なんでGETメソッドにしているんでしょう? たぶん、検索結果そのものにリンクができるようにじゃないかな?と思います。 GETメソッドにしておけば、?〜 の部分を含めてアドレスをコピーすれば、 誰か他の人にその検索結果ページを教えてあげるとかできますよね。 あるいは、検索結果ページをブックマークしたりもできます。 理由はいろいろあると思いますが、GETメソッドの方がベターだっていうことですね。 おかげで、URLを調べることで検索語が分かるのですから、ありがたいことです。

さて、ここまでは基礎知識。 実際にリファラーから検索語を取得しようとすると、結構いろいろ問題があったりします。 今回はその研究となります。

結果

リファラーをCGIプログラムに渡す

検索語を調査するためには、検索語の情報が含まれているURL、つまり リファラーをCGIプログラムに渡す必要があるんでした。 このやり方は、リファラーランキングの研究のところで既に紹介しましたね。 Javaスクリプトを使って、次のようにします。

<script type="text/javascript">
	document.write(
		"<img src=searchword.cgi?ref=" + 
		 document.referrer + " width=1 height=1>"
	);
</script>

document.referrer のところがリファラーURLに置き換えられて、 searchword.cgi?ref=(リファラー) という形でCGIプログラムが呼び出されます。 そして、? から後ろの部分をCGIプログラムで取得することができます。

さて、実は、実際に試してみて分かったのですが、このやり方だと、 リファラーURLに & が含まれているときに、うまくいかないことがわかりました。

リファラーに & が含まれている場合

例えば、Googleで perl labo を検索した場合を考えましょう。 検索結果URLは、次のようになります。 (本当は他にもいくつか情報が付いているのですが、邪魔なので外してます。)

http://www.google.co.jp/search?lr=lang_ja&q=perl+labo

この検索結果ページから当サイトに訪問すると、リファラーはずばり、この検索結果ページのURLになります。 ですから、先ほどのJavaスクリプトによって、CGIプログラムは次のような形で呼び出されることになります。

<img src=searchword.cgi?ref=http://www.google.co.jp/search?lr=lang_ja&q=perl+labo>

ここで問題というのは、上の赤字にした & です。 この & って、GETメソッドでの情報の区切りでしたよね? これをこのまま、フォームからの情報受信のルールに従って読み取ると(getformdata 関数を使うと)、 赤字の & のところでデータが区切られてしまうんです。つまり、次のような2つのデータに分けられちゃいます。

ref = http://www.google.co.jp/search?lr=lang_ja
q = perl+labo

これは困りました。 リファラーURL全体が ref に入っているつもりで処理をしていたら、 こんな風にデータが区切られてしまって、正しくリファラーを取得できません。 なによりも怖いのは、自分が使っているキーの名前、ここでは ref だけなのですが、 これと同じキーを検索エンジンが使っていたら、データが衝突してしまって CGIプログラムは正しく動いてくれなくなってしまいます。 まずは、この現象を解決しなければいけませんね。 正しくリファラー全体を取得する方法を見つける必要があります。

正しくリファラー全体を取得する方法

1つは、もっとも正当と思える方法です。それは、フォームがそうしているように、 & とか = といったデータの区切りに使われている部分を、別の文字に置き換えるっていうやり方です。 %〜 というやつですね。これをURLエンコードというんでした。 フォームからデータを受け取る場合は、逆にデコードが必要なんでしたね。 このやり方をそっくり真似すれば、正しくリファラーを受け取ることができます。

で、そのやり方を考えたんですが、Javaスクリプトに URLエンコードをしてくれる関数などがあればいいのですが、 調べた限りではそのような関数は見つかりません。 それに、CGIプログラム側が複雑になるのはまあいいのですが、 CGIプログラムの呼び出し側にも複雑な手続きが必要っていうと、 CGIプログラムの設置に手間がかかりますから、できるだけ避けたいですよね。 ということで、この正当な方法はとりあえずボツにしました。

別の方法を考えてみましょう。 リファラーは、データとしてはちゃんと受け取れているんです。 区切り記号の問題で、getformdata 関数だとデータがバラバラになってしまうので 困るということなのです。 ですから、getformdata 関数を修正して、リファラーはそれ以上 区切り記号で分割しないということにするんです。 その方法ですが、採用したのは次のような方法です。 「指定したキーから後ろはそれ以上分割せずに1つの値として読み込む」です。 ここではリファラーを ref というキーにしていますから、ref というキーの後ろは1つの値である というように処理します。このやり方は、 & を含むデータが1つだけある場合で、それを必ずURL ?〜 の一番後ろで与えることができる、 という場合にだけうまく働きます。ですから、先ほどの正当な方法に比べると 強引だしイマイチですが、とりあえずリファラーを受け取るだけなら 問題ないです。

なお、CGIプログラムに渡すデータがリファラー1つだけなら、 もっとも簡単なのは、ref= といったものを付けずに直接 ?(リファラー) という形で リファラーを渡す方法です。CGIプログラム側では、$ENV{'QUERY_STRING'} がリファラーそのものに なりますから簡単です。でも今回はリファラー以外の情報も渡せるように、 この方法は採用しませんでした。

リファラーをデコードする

検索語が日本語の場合は注意が必要です。例えば、検索結果URLが次のようになります。

http://www.google.co.jp/search?ie=Shift-JIS&q=%83p%81%5B%83%8B%83%89%83%7B

検索エンジンはGETメソッドを使っているということを書きましたが、 ということは日本語などの全角文字はどうなっているかというと、 見ての通り、URLエンコードされて、%〜 というやつに変換されています。 なので、これをデコードしなきゃいけません。 でも、これは、getformdata 関数を使う限り、自動的にやってくれますので、 気にする必要は無いですね (^^

文字コードを変換する

ここで、今までは無かった問題がでてきます。 文字コードの問題です。

今まではブラウザから送られてくる情報の文字コードについては特に処理をしていませんでした。 正確には、HTMLページをShift-JISで作り、CGIプログラムもShift-JISで作る、ということを守れば、 ブラウザから送られてくる情報もShift-JISになっている、ということを利用して、全てShift-JISである という前提でいたんです。でも検索エンジンはShift-JISとは限りません。 文字コードについては深く研究していませんが、Shift-JISの2バイト文字には Perl(あるいは正規表現)で特殊な意味を持つ \ が含まれている場合があり、 そのためにトラブルが起こることがあるので、Shift-JIS以外の文字コードを使う方がむしろ普通のようです。 そんなわけで、検索エンジンのURLに含まれている日本語は、どの文字コードなのか分からないのです。 これを、Shift-JISに変換してやらないといけません。

ここで、Jcode の登場です! メールの送信のところで、Jcode を使ってShift-JISをJISに変換するっていうのをしましたね。 Jcode は、日本語文字列があるとき、それがどの文字コードかを自動的に調べて、 指定した文字コードに変換してくれるっていう凄いものです。 実際の使い方は簡単です。

use Jcode; # これは1回だけ呼びます

Jcode::convert(文字列, 'sjis');

文字コードの問題はとても複雑ですが、Jcode のおかげでこんなに簡単に解決してしまいます! ありがたや…。

ただし、もともと文字コードを判別するっていうのは完璧に行うのが難しい処理のようで、 時々変換に失敗することがあるようですが、それは仕方がありません。

URLの中から検索語を抜き出す

いよいよ大詰めです。 ここまでの処理で、日本語部分をShift-JISに変換した、検索結果URLが取得できています。 このURLから、検索語を抜き出すという処理です。これが出来れば、検索語調査はほぼ終わりです。

検索エンジンもフォームを使っているわけですから、そのURLは次のような形をしています。

〜?(キー)=(値)&(キー)=(値)&(キー)=(値)&(キー)=(値)&…

この中の、いずれかのキーが検索語を表すもので、それに対応する値が、求めている検索語です。 ですから、検索語を取得するためには、検索語を表すキーを知っていなければいけません。

実際に検索エンジンで検索を行ってみると、このキーが分かります。 例えば Google の場合は q です。Yahoo!Japan(の検索エンジンYST)の場合は p です。 Google の q は query の略かな? YST のキーはなんの略だろう? もしかして Google を意識したんでしょうか?q と p。面白いですね (^^ …と、面白がっている場合ではありません。このように、検索エンジンによって 検索語に対応するキーは違うんです。 なので、まず、検索エンジン毎に処理を分けないといけません。

URLから検索エンジンを知るには、ホスト名部分を調べればいいです。 例えば、google という文字列が含まれていたら Google ですし、 yahoo という文字列が含まれていたら Yahoo! です。 ドメインは、国によって、.co.jp だったり .com だったりしますので、 複数の国にある検索エンジンの場合はドメイン抜きの文字列で調べた方が良さそうですね。 実際には次のような感じで判別します。 ブラウザのユーザーエージェントからOSやブラウザのバージョンを調べたときの処理と同じ感じですね。

if    ($url =~ /yahoo/)  { Yahoo! です }
elsif ($url =~ /google/) { Google です }

あとは、検索エンジン毎のキー。こればっかりは、ひたすら調べるしかありません。 実際に自分のサイトが検索されるのを待って、 そのリファラーを記録しておいてそこから調べるとか。 とにかく、私も一生懸命しらべました。次の表がそれです。

URLに含まれる文字列検索エンジン(略称)キー
yahooYahoo! (YST)p
googleGoogleq または as_q
msn.co.jpmsn サーチq
goo.ne.jpgoo ウェブ検索MT
nifty.comnifty@searchText
excite.co.jpexcite ウェブ検索search または s
biglobe.ne.jpBIGLOBEサーチ Attayoq
infoseek.co.jpinfoseek 検索qt
aol.comAOLサーチquery または query_contain
fresheye.comフレッシュアイkw
jword.jpJWord検索name

これは結構貴重なデータじゃないでしょうか?そんなことないかな (^^; これで全部っていうわけじゃないと思いますが、主要な検索エンジンは大体これでOKだと 思います。ここにあげた検索エンジンで、95%以上は対処できるんじゃないかな?

これで、各検索エンジンの検索語を表すキーが分かりました。 キーが分かれば、ゴニョゴニョっとすれば、検索語が取得できます。 ふぅ。ここまで、かなり大変でございました…。 今回の研究、体力使います。

ちなみに、検索語のキーが分からない検索エンジンの場合、とりあえず日本語部分を探して それを検索語にするっていう強引な方法も考えましたが、それは採用しませんでした。

取得した検索語をファイルに保存

検索語を取得したら、それをファイルに保存して、 どんな検索語で検索されているのかっていうのが分かるように 画面に表示できるようにする作業が残っています。 ここでまた、自作ライブラリの登場です! ページビューカウンターから始まり、リファラーランキングでもその力を いかんなく発揮している、文字列指定式カウンターライブラリ counters.pl を 使えば、検索語取得後の処理はかーんたんです。 ファイルへの保存、トータルのカウント、一定期間のカウント、 カウントのリセット、ランキング表示、全部やってくれます。 なんか、数を数えることだけは得意になったなぁ…。

作成したCGIプログラム

searchword.cgi
#!/usr/bin/perl

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

require 'setting.cgi';
require 'searchword.pl';

plab::exec_searchword();
searchword.pl
# v 1.01

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

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

package plab;

use Jcode;

# 検索ワードを取得します
# 引数(デコードされていないURL)
# 戻値 成功の場合 配列で(1, 検索ワード, 検索エンジン名)
#      失敗の場合 配列で(0, デコードしたURL)
sub get_searchword
{
	local $url = $_[0];

	# ? が無ければ検索エンジンでは無いです
	if (! ($url =~ /\?/)) {
		return (0, $url);
	}

	# ? より手前を得る
	local ($head) = split('\?', $url);

	# 検索ワードが入っているキーを得る
	local ($skey, $skey2);
	if    ($head =~ /yahoo/)          { $se = "yahoo";    $skey = "p"; }
	elsif ($head =~ /google/)         { $se = "google";   $skey = "q"; 
						      $skey2 = "as_q"; }
	elsif ($head =~ /msn.co.jp/)      { $se = "msn";      $skey = "q"; }
	elsif ($head =~ /goo.ne.jp/)      { $se = "goo";      $skey = "MT"; }
	elsif ($head =~ /nifty.com/)      { $se = "nifty";    $skey = "Text"; }
	elsif ($head =~ /excite.co.jp/)   { $se = "excite";   $skey = "search"; 
						      $skey2 = "s"; }
	elsif ($head =~ /biglobe.ne.jp/)  { $se = "biglobe" ; $skey = "q"; }
	elsif ($head =~ /infoseek.co.jp/) { $se = "infoseek"; $skey = "qt"; }
	elsif ($head =~ /aol.com/)        { $se = "aol";      $skey = "query";
						      $skey2 = "query_contain"; }
	elsif ($head =~ /fresheye.com/)   { $se = "fresheye"; $skey = "kw"; }
	elsif ($head =~ /jword.jp/)       { $se = "jword";    $skey = "name"; }

	# 対応していない検索エンジンです
	if ($skey eq "") {
		return (0, $url);
	}

	# クエリー部分を取得します
	local $query = substr($url, length($head) + 1);

	# デコードします
	$query =~ tr/+/ /;
	$query =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/ego;
	$query = Jcode::convert($query, 'sjis');

	# 余分な空白を取り除きます
	$query =~ s/ +/ /g; # 連続した全角スペース
	$query =~ s/ +/ /g;  # 連続した半角スペース

	# & を先頭に加えて全てを &(キー)= の形にします
	$query = "&" . $query;

	local @data = split('&', $query);
	foreach (@data) {
		local ($key, $val) = split('=');
		if ($key ne "" && ($key eq $skey || $key eq $skey2)) {
			if (length($val) > 60) {
				# 長すぎるので文字化けの可能性あり
				return (0, $url);
			}
			else {
				return (1, $val, $se);
			}
		}
	}

	# 見つかりませんでした
	return (0, $url);
}

# リファラーから検索ワードを取得します
sub exec_searchword
{
	%form = plab::getformdata("ref");

	if ($form{'ref'} ne "")
	{
		# リファラ情報が渡されてきているので処理します
		$url = $form{'ref'};
		($ok, $val, $se) = plab::get_searchword($url);
		if ($ok) {
			incl_counters($::sw_fname, $val, $::sw_ipcheck,
				      $::sw_nlog, $::sw_logspan);
		}
	}
	elsif ($form{'v'} eq "graph")
	{
		# グラフ表示
		print "Content-type: text/html\n\n";

		if ($::sw_pswd ne "" && $::sw_pswd ne $form{'pswd'}) { exit; }

		plab::printhtmlheader($::sw_title);
		print "<div class=pla1>\n";
		print "$::sw_title \n";
		if ($::homeurl) {
			print "<small>[<a href=$::homeurl>トップページ</a>]</small> \n"; 
		}
		print "</div><br>\n";

		$logno = $form{'log'};	# 表示するログ番号
		plab::view_counters($::sw_fname, $::sw_nlog, $logno, 0);

		plab::printhtmlfooter("Plab Search Word v1.00");
		exit;
	}

	plab::toumeigif();
}

1;
getformdata.pl
# v 1.02

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

package plab;

# フォームデータを連想配列に格納します
# 引数(ここから後ろを分割せずに得るキー名)
sub getformdata
{
	local $nodivkey = $_[0];
	local $rawdata;
	local %formdata;
	local @inputs;
	local($input, $name, $val);

	if ($ENV{'REQUEST_METHOD'} eq "POST") {
		read(STDIN, $rawdata, $ENV{'CONTENT_LENGTH'});
	}
	elsif ($ENV{'REQUEST_METHOD'} eq "GET") {
		$rawdata = $ENV{'QUERY_STRING'};
	}

	if ($nodivkey ne "") {
		local $s1 = $nodivkey . "=";
		local $s2 = "&" . $nodivkey . "=";
		local $pos = index($rawdata, $s1);
		local $len = length($s1);
		if ($pos != 0) { $pos = index($rawdata, $s2); $len = length($s2); }
		if ($pos >= 0) {
			local $val = substr($rawdata, $pos + $len);
			$val =~ tr/+/ /;
			$val =~ s/%([A-Fa-f0-9][A-Fa-f0-9])/pack("C", hex($1))/eg;
			$formdata{$nodivkey} = $val;
			$rawdata = substr($rawdata, 0, $pos);
		}
	}

	@inputs = split('&', $rawdata);

	foreach $input (@inputs) {
		($name, $val) = split('=', $input);
		$name =~ tr/+/ /;
		$val  =~ tr/+/ /;
		$name =~ s/%([A-Fa-f0-9][A-Fa-f0-9])/pack("C", hex($1))/eg;
		$val  =~ s/%([A-Fa-f0-9][A-Fa-f0-9])/pack("C", hex($1))/eg;
		$formdata{$name} = $val;
	}

	return %formdata;
}

# 連想配列をGETメソッド用クエリ文字列にします
sub formdata2query
{
	local $n = @_;
	local $i;
	local $query;

	for ($i = 0; $i < $n; $i += 2) {
		$query .= "$_[$i]=$_[$i+1]";
		if ($i + 2 < $n) {
			$query .= "&";
		}
	}
	return $query;
}

1;
searchwordtest.cgi (テスト用CGIプログラム)
#!/usr/bin/perl

require 'searchword.pl';
require 'getformdata.pl';

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

%form = plab::getformdata("ref");
$ref  = $form{'ref'};
@r    = plab::get_searchword($ref);

print "URL : $ref<br>\n";
if ($r[0] == 0) {
	print "検索ワードは取得できませんでした。";
}
else {
	print "検索ワードは [$r[1]] です。";
}

実行結果

テスト

作成したCGIプログラムが正しく動いているかどうかを確認するためにテストプログラムを作成しました。 上の searchwordtest.cgi です。検索エンジンの検索結果ページのURLを入力して[検索ワードを取得]を クリックすると、取得した検索ワードを表示します。

URL  
当サイトの検索キーワード

当サイトに検索エンジンから訪問してくれた方々が検索に使用した検索キーワードの一覧です。 もしかして貴方の検索キーワードも載っているかな? すいません勝手に記録したりして…

当サイトの検索キーワード

解説

searchword.cgi

変数の設定を別ファイルに。それと、メインルーチンもライブラリの中に。 そのため、ただ1つの関数を呼び出すだけになっています。 これらは、なんだか私のPerlプログラミング作法みたいになってきました (^^; ただし、メインルーチンをライブラリに入れるのはそれが再利用可能だからで、 再利用しないのならこうする必要はありませんよ。

searchword.pl

get_searchword 関数で検索語を取得します。 戻り値は配列で、1つ目が成功/失敗の情報、 成功の場合は2つ目が検索語、3つ目が検索エンジン名となっています。 検索エンジン名はとりあえず返すようにしていますが今のところ利用していません。 関数の中身は、ここまで説明したとおり。 あ、説明してなかった部分としては、余分な空白を取り除く処理かな。 「perl labo」も、 連続したスペースを挟んだ 「perl      labo」も検索エンジンは同じ結果を返しますので、 これらを同じものだとみなすように、連続した空白を1つの空白に変換するようにしています。 s/ +/ /g というやつですが、+ というのはその文字が連続している部分っていう意味になるようです。 正規表現については良く分かっていません。研究が後回しになっていますが、 凄く便利なものですね。

exec_searchword 関数がメイン関数になります。 リファラーが与えられた場合はそれを処理します。 ?v=graph っていう形で呼び出された場合はグラフを表示します。 このへんは、ページビューカウンターやリファラーカウンターとほとんど一緒の処理で、 実際、一部のコードはコピーペーストで楽させてもらってます (^^;

getformdata.pl

久しぶりにこのファイルを修正しました。 通常は getformdata 関数を引数無しで呼び出しますが、 「このキーの後ろは分割せずに1つの値としてください」っていう意味で 引数にキー文字列を渡せるようになりました。 ですから、今回は getformdata('ref') という形で呼び出しています。

動きました〜

今回の研究も、既に作成したライブラリを使えば簡単かな?と思っていたんですが 実際はかなり大変でした。Perlプログラミングとしては新しい知識は特に増えませんでしたが、 検索エンジンの検索結果のURLにはちょっと詳しくなりました。 でも検索ワードを知るのって、確かに興味は凄くあったんですが、 サイト運営にどんなふうに役立てたらいいんでしょうね。 実際に検索ワードを知ることでどんなふうに自分のサイトをよりよいサイトに していったらいいんでしょうか。 CGIプログラムを作るばかりではなく、 取得したデータの有効な利用方法の研究っていうのも必要ですね。

改良報告

その後、さらに「検索語から余分なスペースを除去する」「未知の検索エンジンに対応するため、検索エンジンのホスト名を 記録する」「検索エンジン毎に検索語をカウントするオプションの追加」を行いました。 欲しい機能を自分で付けることが出来る。嬉しくなっちゃう瞬間ですね!

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

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