i16@i16.jp あなたは 43219 人めです
Apple Store

postanalog:
ログ解析analog用ポストプロセッサ
(日本語検索語解析)
いわゆる文字化け対策です。


ポストanalogってanalogの次を狙うとかいう威勢のいい話ではなくて、analogでログ解析したら日本語の検索語が解析できなかったんでツールを作ったよ、というお話です。analogのポストプロセッサとして実装したのでポストanalog。 どうせanalogを使ってないと意味がないちょっとくっつけるだけのquick hackプログラムですから、ちゃんとしたプログラムとバッティングしないように普通付けないような出鱈目なネーミングにしてあります。
著作権は私にあり放棄しませんがライセンスはGPLっつーことで宜しく。 つまり利用料は私用商用を問わず無料ですがソース配布に協力する義務がありますし、 このプログラムの一部を別プログラムに利用した場合は、 そのプログラムもソース公開を強制され、 そうしないと著作権法違反になります。 ソース配布への協力としては、CDRを焼いて無償配布していただいてもいいですが、 作者の僕のこのWebページをリンクしたりURLを通知したりすれば簡単と思います。 GPL的にはそれだけじゃダメみたいですが、 このページが活きてるあいだにはこっからソースが手に入るんで。 URLだけに依存してると、 このページが何かの都合でなくなった場合にいきなり、 ソース配布という条件を満たせなくなって違法になってしまう、 というリスクがありますから この個人のサーバの稼動次第に依存しちゃうわけに行かないような場合には、 やはり独自にソース配布する方策を考えてください。
使い方

postanalog { -i | -q0 | -w0 } { file ... }

file
analogのオプションが何でもいいわけじゃないですから、 普通はフィルタとして使うと思いますが、ファイルを指定することもできます。 複数書けますが標準出力に繋いだものが出てきちゃうので複数書くのは変。
-i
アルファベットの大文字小文字を区別せず、 小文字に変換してすべて小文字として扱います。
日本語じゃない部分なので、 日本語をどうにかするという触れ込みのフィルタに組み込むのはどうかと思いましたが、 Googleという大文字小文字を区別しない検索エンジンが優勢なので、 使う側としてこのオプションがあるのが便利。 レポートも小文字になります。 analogから出てきたレポートの検索語句、検索単語の、 大文字で来た回数と小文字で来た回数を合算します。
-q0
検索語句のレポートで頻度が指定順位に満たない部分を省略します。
analogの本体は省略しないレポートを出させないと誤差が生じてしまいますから、 postanalogを使うのに、 analogを頻度が少ない部分をレポートさせない設定で使うのは誤りで、 頻度1の最下位まですべてレポートさせるオプション指定をする必要がありますが、 そうは言ってもレポート自体が不必要に長くなってしまう場合があるので、 analog本体にあるようなこの機能をpostanalogに持たせました。
デフォルトはゼロ0でこの場合は省略せずすべて表示。 上記オプションの0の部分を0じゃなくて順位の数字を指定します。 同じ頻度の場合には途中で切られちゃいますが、 行数が多くなりすぎないためのオプションなので、 指定順位以下について同じ頻度の別の単語が表示されていないという可能性はあります。
-w0
検索単語についても同様のオプション指定ができます。

  1. postanalogを使う場合には、 analogは日本語EUCでレポートを出させる設定にして使います。 そういう想定でコーディングしましたからそういう仕様です。 これ以外の場合にきちんと動作しない処理をしているかもしれないと思います。 analogを日本語EUC以外のレポートを出させて使っている場合に、 analogのこの不具合の影響を受けているとすれば、 postanalogをこの流れで使うのはpostanalogの仕様からは想定外なので、 そういう使い方は設定ミスです。 analogのオプションを変えるだけで済むことですから、 このフィルタ側でそのためにロジックを作って、 バグの入り込むかもしれない可能性を拡大してしまうのは避けて、 仕様ということにしました。 さらに膨大な労力を使って対応するより、 労力が余ってるなら本家のanalogの改良を手伝うか、 analogの次を狙うものを作ってしまって、 postanalogのようなフィルタをそもそも不要にしてしまう、 というのが正しい方向性というものだと思います。 既存のanalogのホコロビとか穴に、 穴があいてたらとにかく不便なので、 パッチつまり当て布をして継ぎはぎで使う、 という目的のpostanalogのようなプログラムが必要以上に複雑であったら、 根本的な哲学として何か間違っていると思います。 ユニクロのフリースに穴があいたという理由でエルメスのスカーフを被せるのは変だ。 いやこの例はそれはそれでかっこいいから例として不適切か。 でも普通はユニクロの補修ならユニクロがまた買えるぐらい金を掛けることは変で、 百円ショップにあるものでどうにかなれば直すしダメなら捨てちゃう と言う判断でしょう。
  2. 検索関係のレポート(検索語句レポート、検索単語レポート)は、 出現回数1回まで省略せず全て報告させておく必要があります。 集計レポートのHTMLを読んで、 各アクセスの文字コードを推測しつつデコードして、 同じ検索語で文字コードだけが違う場合について、 集計を合算して書き換えているだけですから。 いったん文字コード別に計算されてしまった出現回数の合計を計算するわけですから、 頻度が高いコードの回数は多くても、 同じ語句の頻度が低いコードでの検索の回数が低い場合があり、 これを省略してしまうと合計の回数が正確に計算できません。 全部出さないと誤差が出てしまいますし、 もっともらしい数字がいい加減な処理で算出されているかもしれない状態で、 レポートにはそういうことがあったのかなかったのかという情報がまったく含まれない、あやふやな状態になります。 「その他:」の項目がある場合には、 出現回数が低い場合の元レポートが出ておらず 「その他:」の項目に合算されてしまっているということを意味しているので 「設定ミス」です。 設定ミスをして、 検索語句と検索単語のレポートに「その他:」の項目が出現した場合には、 これを単語として処理してしまって表示が乱れるようですが、 設定ミスの場合の表示の乱れは設定者が気付けばよく、 最終レポートとして他人に見せるにはもともと不適切ですから、 設定ミスのときにしか出現しない項目の処理を改善してもあまり意味がないと考えて、 設定ミスのときの表示を適切にする処理は組み込んでいませんでした。 レポートが頻度1まで必ず含む必要はないと思うので、 postanalogの側で、頻度が低い場合の表示を適切に省略するような処理は、 組み込もうかなと考えて組み込みました。 postanalogが「その他:」の項目を生成します。 そういう処理がある場合には、 元レポートの「その他:」から生じた表示の乱れが隠れてしまう可能性があるので、 設定ミスを指摘するような処理を加えました。
  3. 検索関係については円グラフは非表示に設定して使います。 検索関係のレポートのうち、 同じ言葉の文字コートが違うものをまとめて項目を組み替えてしまいますから、 検索関係のレポートについてのグラフは同じ単語が複数回出るかもしれない、 あるいは集計が不正確でグラフの印象がおかしい、 などの可能性があり統計として無効になるためです。 こちらは設定ミスをしても意味不明になる以外は表示の乱れが起きませんが、 検索語句レポートや検索単語レポートに、 グラフが出ていたら設定ミスということです。
  4. NKF.pmは、 むかしの古い奴じゃなくて、 nkf2.0以降のユニコード対応のものをインストールします。 古いnkfではUTF-8の検索語が解析出来ませんし、 postanalogもユニコード対応のNKF.pmを前提にしてコーディングしています。
  5. 検索語はURLエンコードされて%XXのような形になっていると思いますが、 これをエンコードしているのはブラウザで、 ログ解析側の仕事ではなく、 サーバにそういうアクセスがあったためにログにそう書かれているわけです。
    \xXXのようなエンコードをするブラウザが有るという説がありますが、 それではURLエンコードではないので、 僕のサーバにそういうアクセスがあったことがないこともあって対応保留中。
    そういうエンコードをするブラウザがあったら詳細を教えてください。
    ブラウザもどきを自作するときのミスとか、 あるいはログファイルを作るサーバデーモン側で、 8ビットを出さずにそういう処理をしたログを吐いてるかなにか、 そのような理由でできちゃった解析すべきでない行かもしれないですし。
    と思ってたら結構来るようになったんで対応しました。
使い方の例
analogコマンドはココでは改行しちゃってますが同一行に続けて入力(^^;;

#!/bin/sh

ana() {
        analog +C"REFSITEFLOOR 1r" +C"SEARCHQUERYFLOOR 1r"
        +C"SEARCHWORDFLOOR 1r" +C"SEARCHQUERYCHART OFF"
        +C"SEARCHWORDCHART OFF" $1 |postanalog -i
        }

ana /var/log/httpd/access_log >/home/www/htdocs/analog/Report.html

 

postanalog
URLデコードは、 検索語を読みたいだけなので制御記号は基本的にはデコードしません。 ただしエスケープコードはJIS漢字コードの表示用なのでデコードします。 その他微妙にアドホックな対応。 「いくつかやってみて比べる」なんていう処理になっている部分もあります。 明らかに文字化けする可能性が上がるんだけど通しちゃうのもお行儀悪いし、 みたいな悩ましい部分もあります。 ふつう検索はUTF-8かEUCかSJISで来て、 JISコードで来る奴あんまり居ないと思うからまいっかというか。
その他にも微妙にアドホックな対応というかチューニングがしてあります。 (特にwikiが噛んでる場合に行の途中で文字コードが変わる場合があることの処理)
完璧じゃないですが原理的に完璧がありえない処理をしているわけですし、 (どうしてありえないか解らないかたは勉強をしてください) 必要以上に凝っても何だし、実用上まあまあこんなものかも。
「文字コードだけではどっちもありえなくはない」というような場合に、 記号ばっかりで検索して来るのは変だとか、 文字別に普通こんな字で探しに来るかというランクをつけるとか、 単独じゃなくて文字の組み合わせで、 これは普通に使う言葉だけどこれは見たことないからランク下げましょうとか、 プログラムに知識を組み込んで、 データベースを利用しつつファジーな評価関数で処理をして、 一番妥当と思われるデコードをする、 というのがベストと思いますが、 「とりあえず文字化けで何も読めない状態を改善するパッチ」なので、 あんまり凝りすぎないことにします。
2003-11-10
\x81のようなエンコードのRefererを吐くブラウザの検索語もデコードするように変更しました。(ソースコード的には1行足しただけですが)
2003-04-14
ログの中におかしな文字コードがあると検索単語レポートが一部飛んじゃう場合がある、 という不具合がありました。 おかしな文字コードが来たら表示してるんですが、 このときに制御コードとか複数バイトコードの1バイト目だけが来て、 2バイト目がつじつまが合わないといったような場合なのにそのまま表示したら、 当然表示が乱れてしまいます。 16進法で表示するように変更しました。
その他-i -q0 -w0のオプションをつけたり各種改造をしました。
2003-03-22
自分では使ってたプログラムですが、 誰でも自分で作って使ってると思っていたら、 文字化けして統計もおかしいレポートを読んでいるということなので、 それでは公開しましょうとWebを作成。
知らなかったのですが、 長尾さんがanalog-jpのメーリングリストに紹介してくれていました。


postanalog.txt ダウンロードしやすいようにファイル名の後ろに「.txt」をくっつけたんで、リネームして使ってください。


#!/usr/bin/perl -w
#入口に漢字を書いておく(エディタがコード判別しやすいように)
#postanalog by i16 -- http://i16.jp

$debug=0;

use NKF;

sub nkf1 { local($s)=@_;
	chomp($s);
	$q=nkf('-eI',$s);
	$uq=nkf('-eWI',$s);
	$us=nkf('-eSI',$s);
	$q=$us if $s eq nkf('-sE',$us);
	$q=$uq if $s eq nkf('-wE',$uq);
	return nkf('-eEIZ1',$q);
	}

sub numstr { #no param, global variables used.
	chomp();
	s/\s+$//;
	($num,$str)=split(/: /,$_,2);
	$str='' unless defined $str;
	$sonotaexists=1 if $str =~ /^\[その他\: [\,\d]+ [\xa1-\xfe]+\]/;
	$h='';
	while($str =~ s/^(.*)(wiki\.cgi\?(\w|%[2-9a-f][0-9a-f]|\xa1[\xa2-\xfe]|[\xa2-\xfe][\xa1-\xfe])+)//i) {
		$h.=nkf1($1).$2;
		}
	$str=$h.nkf1($str);
	$str=lc($str) if $ignorecase;
	}

$wordfloor=0; $queryfloor=0; $ignorecase=0; $sonotaexists=0;
while($ARGV[0] =~ /^-/) {
	if($ARGV[0] =~ /^-w(\d+)$/) {
		$wordfloor=$1;
		shift @ARGV;
		}
	elsif($ARGV[0] =~ /^-q(\d+)$/) {
		$queryfloor=$1;
		shift @ARGV;
		}
	elsif($ARGV[0] =~ /^-i$/) {
		$ignorecase=1;
		shift @ARGV;
		}
	else {
		printf "use: postanalog { -i | -q0 | -w0 }\n\nignorecase,querystring,word\n";
		exit;
		}
	}
$searchw='';
$merging='';
while(<>) {
	s/%22/\&quot\;/g;
	s/%26/\&amp\;/g;
	s/%3C/\&lt\;/gi;
	s/%3E/\&gt\;/gi;
	s/%20/\&space\;/g;
	s/%0B/\x0b/gi;
	s/\\x([8-9a-f][0-9a-f])/pack("C",hex($1))/gie;
	s/%([2-9a-f][0-9a-f])/pack("C",hex($1))/gie;
	s/\&space\;/\%20/g;
	$searchw=1 if /\<a NAME\=\"searchw\"\>/i;
	$searchw=2 if /\<a NAME\=\"searchq\"\>/i;
	$searchw=0 if !$searchw && /\<a NAME\=/i;
	if($merging) {
		if(/\<\/tt\>\<\/pre\>/i) {
			print <<"_STR_" if $sonotaexists;
analogの設定ミスで[その他:]として省略されpostanalogに入力されなかった検索語句があり、このレポートの順位や数字は不正確です。
_STR_
			$merging=0;
			$floor = ($searchw==1)?$wordfloor:$queryfloor;
			$searchw=0;
			foreach $w (keys %counts) {
				$map{$counts{$w}}=1;
				}
			$sonota=0; $sonotak=0;
			$tops=0;
			foreach $c (sort { 0+$b <=> 0+$a } keys %map) {
				foreach $w (sort keys %counts) {
					if($counts{$w}==$c) {
						if(!$floor || ($tops++ < $floor)) {
							printf("%12d: %s \n", $c, $w);
							}
						else {
							$sonotak++;
							$sonota += $c;
							}
						}
					}
				}
			if($sonota) {
				printf(<<"_STR_", $sonota, $sonotak, $floor);
%12d: [その他: %d 項目の表示を<a href="http://i16.jp/soft/postanalog.htm">postanalog</a>が省略しました。上位 %d 位のみ表示です]
_STR_
				}
			print;
			%counts=();
			%map=();
			$sonotaexists=0;
			next;
			}
		if($searchw==1) {
			&numstr();
			if($str =~ /^\[その他\: [\,\d]+ [\xa1-\xfe]+\]$/) {
				$counts{$str}+=$num;
				next;
				}
			$w=''; $x='';
			foreach $c (split(//,$str)) {
				if($c eq " "||(ord($x)==0xa1)&&(ord($c)==0xa1||ord($c)==0xa2)) {
print "($w)" if $debug;
					$counts{$w} += $num unless $w eq '';
					$w='';$x='';
					}
				elsif(ord($c)&0x80) {
					if(0xa1<=ord($c)&&ord($c)<=0xfe) {
						if($x) { $w.=$x.$c; $x=''; }
						else { $x=$c; }
						}
					else {
						printf("<<<<<<%02X>>>>>>\n",ord($c));
						$x='';
						}
					}
				elsif(0x21<=ord($c)&&ord($c)<=0x7f) {$w.=$c;$x='';}
				else {
					printf("<<<<<<%02X>>>>>>\n",ord($c));
					$x='';
					}
				}
			$counts{$w}+=$num if $w ne '';
print "($w)" if $debug && $w ne '';
			}
		elsif($searchw==2) {
			&numstr();
			$counts{$str}+=$num;
			}
		else {die"bad";}
if($debug) {
if($_ eq $num.": ".$str) { print $_,"\n";}
else { printf("<%s\n>%s: %s\n",$_,$num,$str);}
}
		next;
		}
	$merging=1 if $searchw && /^------------: --/;
	print;
	}



 


i16(愛一郎)
Apple Store