持ち逃げしたい
電話がかかってきた.
「愛知にあります○○なんですが,どうもお世話になります.実はですね,そちらに間違えて7000万円振り込んでしまいまして」
いや,豪快すぎだろ,それ.
« 2004年9月 | | 2004年11月 »
電話がかかってきた.
「愛知にあります○○なんですが,どうもお世話になります.実はですね,そちらに間違えて7000万円振り込んでしまいまして」
いや,豪快すぎだろ,それ.
今日から旅行である.
本当はゆっくり京都だけのつもりだったのだが,出足が遅かったせいで宿を確保できなかったのである.
そこで,近間ということで奈良に宿を取り,ついでにブラブラすることにしたのである.
新幹線にはそこそこ乗っているのだが,考えてみると,ほぼ北陸方面で,新幹線の代名詞とも言える東海道新幹線にはあまり縁がない.
今回は自由席の旅だ.時刻を気にしないで済むので,私は自由席が好きだ.(毎日遅刻しているような人間が時刻を守れるわけがない)驚いたことに東海道新幹線はバカバカ10分毎ぐらいに出ているので本当に何にも考えずに済む.たいへんありがたい.
しかし,席を確保した今,腹が減ってしかたがないのだが,何も売りに来ない.京都までこのままなのか?
ひからびるぞ,おい.
すっかり忘れていたのだが,とったのは旅館.外観がこぎれいなホテル風だったので,違和感なくホテルだと思っていたのだが,部屋はしっかり和室で,久しぶりの畳にそれだけで満足していた.
でもって,豪勢な和食の夕食!幸せだ~.
近くには東大寺があり,ライトアップされるので,テラスから眺めるといいですよ,とのこと.行ってみたら,階段がこんなだ.BGMはジャズ.......和風をねらいたいのか,洋風をねらいたいのか??
春日大社に見に行ったけど,やはり血まみれの行事ではなかったことをここに明記しておく.
新幹線,おもいっきり席取れなかったよ.
そもそも,自由席車両が3両しかないことに驚いてはいたんだけど.JRってそんなもんだったっけ.
まあ,2時間ぐらいだからいいや.
アラビア数字はたいてい半角で書くことにしているのに,普通の文章に混じった数字はATOKでは全角文字しか出てこなくていらついてたのですが,やっと設定を見つけました.
てっきり「半角全角変換」のところかと思っていたら,「追加する候補」ってところなんだもん.いやぁ,苦労した.
全角半角といえば,私は癖で,空白は半角が標準になるように設定しているんですが,これは多数派なんでしょうかね?
お話を書くときは全角空白が多くなるんだけど,もう,全角空白はShift+Spaceというのが染みついちゃっているので特段設定を変えることはないです.
昨晩,そろそろ寝ようかと思っていたら,PCのスピーカーがパタンと倒れた.薄型だし,もともと不安定な場所にあったので,不思議には思っていなかったのだが,直後,揺れが来た.
うわ,結構長い地震やなあとふと見上げると,MDケースとその横に置いている時計が落ちそうになっている.
落ちる~落ちる~と見つめていたら,とうとうMDケースが音を立てて落っこちてきた.
落下地点,枕の横.
危ない,危ない.
せっかくRubyで遊んでいるので,アクセスログを整形するcgiを作れんもんかと考えた.いや,その前に,さくらインターネットのレンタルサーバーって(スタンダード契約)アクセスログ参照できるのかなあ.
そーゆーわけで,試しのコードを書いてみる.
#!/usr/local/bin/ruby
$SAFE = 1
logfilename = "../../log/access_log_20041009"
logfile = open(logfilename)
print "Content-Type: text/plain; charset=EUC-JP\n\n"
10.times{
log = logfile.gets
print log
}
logfile.close
おお,うまくいくじゃないか.
そういや,過去ログの方は圧縮してあるけど,これは展開できんのかな.
え~と,gunzipはあるんか?プログラムはwhichで探すんだっけ.どうも,違和感あるんだよな,このUNIXコマンド.どうしてもwhereと打ちたくなってしまう.
まあ,ともかくだ.usr/bin/gunzipにあることは分かった.が,これ,cgiで動かせるんかな?
ともかく,ファイルを開いているところの2行を以下の通り差し替えてみる.
logfilename = "../../log/access_log_20040908.gz"
logfile = open("|gunzip -c #{logfilename}")
動くやんけ.
なんか,意図したことがすんなりいくと,逆にびっくりしてしまうな.ま,なんにせよ,これで,なんかできそうな気がしてきた.
評判がいいので買ってみた.語り口がやさしく,好感をもった.この場合のやさしいは「優しい」の方だ.丁寧なのである.分かりやすく伝えようという気持ちがにじみ出ているように思う.
後輩のIがわざわざ私におごりに来てくれるという.これ幸いと,上野にて合流した.長いこと合っていなかったので,見つけられる自信がなく,あやうく,同じぐらいの年頃の人待ち顔の人に声を掛けそうになったが,自分がそもそも人の顔を見分ける能力に劣ることを思い出してやめておいた.案の定,違う人だった.少ししたら,もっと似た人が現れ,それが本人だった.
無事,合流して,まずは国立科学博物館へ行った.ENIACを見るためである. 今まで見た展示会のたぐいで一番興味深く,おもわずいつもなら買わない目録まで買ってしまった.最後に残った印象が,ゲーム機の方に奪われてしまっていたのは許して欲しい.(懐かしくってさ)
その後,上野公園をうろついてから駅に戻ったのだが,「疲れた」とのこと.すっかり忘れてた――いや,体力ありそうだったので,遠慮しなかった――のだが,私はエスコートをすると,どうも相手を歩かせすぎるらしいのだ.(相手を筋肉痛にさせた覚えがある)
まあ,気を取り直して飲むべく池袋へ.
今回,店の選定は任されていたので,いくつか見当をつけておいたのだ.(おごりだし,払えんようなところだと困るしな)
すごすごと引き下がって,別な店へ.
おそるおそる,3店目に向かう.
3度目の正直でした.
が.
予約の団体客が必要のない盛り上がりを見せております.
そういう巡り合わせだったんでしょう.
まあ,飯はうまかったです.多謝.
最新のアクセスログを整形して表示することを考える.
まずは,ファイル名の指定である.
さくらインターネットの提供するApacheログファイルは,決められたディレクトリに「access_log_YYYYMMDD」の名前で格納される.YYYYは西暦4桁,MMは月の2桁表示,DDは日の2桁表示である.最新のファイルはこの名前,前の日のファイルは「access_log_YYYYMMDD.gz」と云う形で圧縮されている.
まず,ここで,TimeクラスやDateクラスでYYYYMMDDにする方法が分からず,関数を作るしかないかとあきらめ,以下の通り関数を作った.
#日付をYYYYMMDDの形式の文字列にする関数
def date_fmt(date)
date_year = date.year.to_s
date_month = sprintf("%02d", date.month)
date_day = sprintf("%02d", date.day)
return date_year + date_month + date_day
end
さて,ログファイルは,毎日5時前後に作成される.つまり,単純に今日の日付からログファイル名を指定すると,ファイルがまだ作成されていない可能性がある.ここでまた,条件設定だの繰り返し方だのですったもんだしたが,最新のログファイル名を得る関数を作る.
#最新のログファイル(圧縮なし)を取得する関数
def lastlog()
last = Date.today
last_s = date_fmt(last)
#今日の日付から初めて,
#最新のファイルが見つかるまでログファイルの名前を変える
logfilename = ""
i = 0
until FileTest.exist?(logfilename) do
last_s = date_fmt(last-i)
logfilename = LOG_DIR + "access_log_" + last_s
#ファイルが見つからず無限ループに陥るのを防ぐため,
#10回でやめる
i += 1
break if i == 10
end
return logfilename
end
なんか,関数をサブルーチンのように使っちゃってるけど,いいのかなあ.メインの方に書くとごちゃごちゃしてきたから分けたんだけど.
定数「LOG_DIR」は,ログファイルの格納ディレクトリの絶対パスで,「/home/[ユーザー名]/log/」という文字列である.
ファイルがなかったときの処理は,普通なら1日引くだけでいいはずなんだけど,何らかの理由で無かったときのために,10回繰り返している.もっとうまい処理方法はないのだろうか?
ここまでやっといてなんだけど,Dir.globで存在するファイル名を特定する方法の方が良かったんじゃなかろうか?
そこで,Dir.globでいろいろとやってみたんだけど,SecurityErrorが出る.う~ん,なんて名前のファイルがあるか分かってしまうような命令はよろしくないってことかなあ.
ともかく,最新ファイルは取得できるようになった.
最新のログファイル(圧縮なし)を取得する関数は,until文を使うとiを明示的にカウントアップしなければならないのが美しくないなあと思い,forで書き直した.
logfilename = ""
for i in 0..10
#ファイルが見つからず無限ループに陥るのを防ぐため,10回だけ判定
last_s = date_fmt(last-i)
logfilename = LOG_DIR + "access_log_" + last_s
break if FileTest.exist?(logfilename) #ファイルがあったらループを抜ける
end
文章がちょっと変.といっても,敬体にしそこなった日本語表現があちこちに出てくるってだけだから,気にならない人は気にならないだろう.あと,HTMLやCSSについての記述,例はツッコム人はツッコムだろうなぁ.
それをおいとけば,サンプルの解説は丁寧です.また,入門書ではあまり見かけない,セキュリティの確保についても解説があって重宝です.
いよいよ整形して,と思い,どう書こうか迷う.アクセスログ1行1行を空白で分割して,それぞれに適当な見出しを付けて表示すればいいだけなんだが,条件判断に使いそうな項目もある.そこで,クラスを作ってみることにした.
#アクセスログクラス
class Accesslog
def initialize(line)
@@all = line.chomp.split(/\s/)
datetime = @@all[3]
%r|\[(\d+)/(\w+)/(\d+):(\d+):(\d+):(\d+)| =~ datetime
@@day, @@month, @@year, @@hour, @@minute, @@second = $1, $2, $3, $4, $5, $6
end
def ip #IPアドレスを返す
return @@all[0]
end
def year #年を返す
return @@year
end
def month #月を返す(数値)
return MONTH[@@month.downcase]
end
def day #日を返す
return @@day
end
def hour #時を返す
return @@hour
end
def minute #分を返す
return @@minute
end
def second #秒を返す
return @@second
end
def requestURI #呼ばれたURIを返す
return @@all[6]
end
def status #ステータスコードを返す
return @@all[8]
end
def referer #リファラーを返す
return @@all[10]
end
def user_agent #ユーザーエージェントを返す
i = 11
ua = ""
#空白で区切った結果バラバラになったUA名をつなぐ
while @@all[i]
ua = ua + " " + @@all[i]
i += 1
end
return ua
end
end
……あのですね,クラスってこんな風に使うもんなんでしょうか?おかしなことをしている感がものすごくするんですが.ただ,メインの流れで書いとくよりも,どっかにまとめとく方が,万が一ログの形式が変わってもdefの中を書き換えるだけで対応できるなあと思ったんです.
ちなみに,MONTHは,英語の月名を数字にするためのハッシュ定数.こんな具合になってます.
MONTH = {"jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6, "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12}
『たのしいRuby』に書いてあった物を流用しました.ハッシュって,こんな時に便利なんだなーと飲み込んだ例文でした.
いよいよ,アクセスログを整形する部分を作る.
CGIクラスの使い方が分からなかったり,繰り返しを抜ける条件を間違ったり,例によってすったもんだあったあげく,作ったのがコレ.
logfile = open(lastlog())
ignore = Regexp.new(IGNORE_FILE + "$")
topuri = Regexp.new("^" + URI_SELF)
while logline = logfile.gets
line = Accesslog.new(logline)
unless ignore =~ line.requestURI
print "<dt>Datetime</dt><dd>#{line.year}年#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分#{line.second}秒</dd>\n"
print "<dt>IP Address</dt><dd>#{line.ip}</dd>\n"
print "<dt>Access URI</dt><dd>#{line.requestURI}</dd>\n"
print "<dt>Referer</dt><dd>"
case line.referer
when topuri
print "(サイト内)</dd>\n"
when "-"
print "(ダイレクトアクセス)</dd>\n"
else
print "<a href=\"#{line.referer}\">#{line.referer_unencode}</a></dd>\n"
end
print "<dt>User Agent</dt><dd>#{line.user_agent}<hr></dd>\n"
end
end
logfile.close
footer_out()
header_outは,ヘッダ部分を表示する部分.
以前に考えていたとおり,画像等へのアクセスは省きたかったので,ignoreという正規表現クラスを作って,リクエストされたファイル名の末尾と比べている.IGNORE_FILEは,今のとこ,IGNORE_FILE = "css|jpg|gif|js|mid"
こんな感じ.
出力してみたら,refererとuser_agentの文字列が「"」でくくってあったので,Accesslogクラスの初期化部分にちょっと追加.
@all.each{|s|
s.delete!('"')
}
リファラーの表示は外部からだけの物にしたかったので,caseで場合分けしている.それから,検索文字列をエンコードするべく,Accesslogクラスにreferer_unencodeメソッドを追加.
def referer_unencode #リファラーを返す(アンエンコード済)
ref = CGI.unescape(@all[10])
return NKF.nkf("-e", ref)
end
以下がヘッダ表示部.引数にページタイトルをもらって表示する.
#ヘッダー表示関数
def header_out(title)
cgi = CGI.new("html4") #HTML 4.0 Strict
print cgi.header("charset" => "euc-jp","language" => "ja")
print <<EOF
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ja">
<head>
<meta http-equiv="Content-Style-Type" content="text/css">
<title>#{title}</title>
<link rel="stylesheet" href="#{STYLE_FILE}" type="text/css">
</head>
<body>
<h1>#{title}</h1>
<dl class="log">
EOF
end
CGIクラスの使い方を詳説しているとこ,無いかなあ.いまいち分からんでなあ.
ちなみに,STYLE_FILEは表示をいじるための外部スタイルファイル.私はこのごろ,ほぼ,外部スタイルシートで表示をいじっていない.HTMLコードによけいな物が無くて綺麗だし,ページに統一感を出すのが楽だから.
フッタは以下の通り.
#フッター表示関数
def footer_out
print <<EOF
</dl>
</body>
</html>
EOF
end
出力結果
(cgiで表示した物を保存して,適当なところで後ろを省略したもの)
検索文字列の変換がうまくいかない.どうも,nkfだとUTF-8は扱えないらしい.今後の課題だなぁ.
アクセスログを眺めていて思ったんだが,ステータスコードでエラーを返している行を拾えば,便利なんじゃないだろうか.特に,404 Not Foundを拾うことができれば,サイト内のリンク切れをつぶすことができる.
そこで,スクリプトを改造,改造.
とりあえず,普通のログを吐くときとエラーログを吐くときに場合分けをするため,引数を取ることにした.ここで,CGIクラスとまた格闘してしまった(やっぱ,例文が欲しいよう).
begin
cgi = CGI.new
mode = cgi.params["mode"].to_s #モード取得
file = lastlog() #最新ファイル取得
logfile = open(file)
case mode
when "view"
log_view(file) #生ログ表示
when "error"
errorlog_view(file) #エラーログ表示
else
print "Content-Type: text/plain; charset=EUC-JP\n\n"
print "有効なモードを取得できません."
end
rescue StandardError
error_out()
rescue ScriptError
error_out()
end
error_out()は,エラーを表示してデバッグしやすくするための物で,『Ruby de CGI―Rubyで作るインタラクティブWebサイト』のものをそのまま持ってきている.
と,場合分けするところは比較的すぐにできたのだが,肝心のエラー表示部でエラく苦労してしまった.そのまま出力しても分かりにくいだろうからとステータスコード順に並べようとしたら,はまったのである.変数にとりあえずエラーの行を保持しようとハッシュのハッシュをいじっていたら,何がどれを指すやら分からなくなった上,型の違いではまった!う~ん,Rubyは型を気にしなくていいのね~と思っていたのが間違いの元でした.なんとか完成させたのが以下のコード.
def errorlog_view(file)
header_out("エラーログ")
logfile = open(file)
error = Hash.new
error_count = Hash.new
#エラー行を見つけてとりあえずハッシュに保持
while logline = logfile.gets
line = Accesslog.new(logline)
status = line.status
if /^[4|5]/ =~ status #ステータスコード400台と500台だけ処理
unless error.key?(status)
error[status] = Hash.new
error_count[status] = 0
end
error_count[status] += 1 #ステータスエラーのカウンター
#ステータスエラーの行を保持
error[status][error_count[status]] = line
end
end
error.sort.each{|status, value|
print "<h3>" + status.to_s + " " + STATUS[status] +"</h3>\n"
print "<ol>"
error_line = error[status]
error_line.sort.each{|count, line|
print "<li><dl class=\"log\">\n"
print "<dt>Access URI</dt><dd>#{line.requestURI}</dd>\n"
print "<dt>Datetime</dt><dd>#{line.year}年#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分#{line.second}秒</dd>\n"
print "<dt>IP Address</dt><dd>#{line.ip}</dd>\n"
print "<dt>Referer</dt><dd>"
case line.referer
when "-"
print "(ダイレクトアクセス)</dd>\n"
else
print "<a href=\"#{line.referer}\">#{line.referer_unencode}</a></dd>\n"
end
print "<dt>User Agent</dt><dd>#{line.user_agent}</dd>\n"
print "</dl><hr>\n"
}
print "</ol>"
}
footer_out()
end
型違いではまったのは,「error[status] = Hash.new」のところ.ハッシュのハッシュを使っているので,ハッシュを入れとかなければいけないのに,""を入れていた.これだと文字列なんだよね…….
lineの保持さえうまくいけば,表示するところは簡単.並べ替えて全部表示する.STATUSという定数はハッシュで,ステータスコードをエラーメッセージにするための物.
#ステータスコードをメッセージにするためのハッシュ定数
STATUS = {"400" => "Bad Request",
"401" => "Unauthorized",
"402" => "Payment Required",
"403" => "Forbidden",
"404" => "Not Found",
"405" => "Method Not Allowed",
"406" => "Not Acceptable",
"407" => "Proxy Authentication Required",
"408" => "Request Timeout",
"409" => "Conflict",
"410" => "Gone",
"411" => "Length Required",
"412" => "Precondition Failed",
"413" => "Request Entity Too Large",
"414" => "Request-URI Too Long",
"415" => "Unsupported Media Type",
"416" => "Requested Range Not Satisfiable",
"417" => "Expectation Failed",
"500" => "Internal Server Error",
"501" => "Not Implemented",
"502" => "Bad Gateway",
"503" => "Service Unavailable",
"504" => "Gateway Timeout",
"505" => "HTTP Version Not Supported"
}
400番台と500番台をここで拾うようにしたので,生ログ出力では拾わないことにした.
しまった,robots.txtとか/MSOffice/cltreq.asp~とか/favicon.icoは省いた方がよかったかなぁ.
ちなみに,robots.txtはロボット型検索エンジンのロボットさんが探しに来る物で,行儀のいいロボットならこのファイルの指示にしたがってくれる.MSOfficeなんたらかんたらは,IE+MS Office環境でWebディスカッション機能が有効になっていると探しに来るんだったと記憶する.で,favicon.icoはご存じ,IEなんかのお気に入りで表示されるアイコン.faviconのログは何か遊べそうだなあ.ちょっと調べてみるか.
favicon.icoについて調べていたが,ブラウザによる挙動の違いが定かには分からなかった.少なくとも,IE5系でお気に入りに入れた瞬間にログを残すことがあるらしいというのが分かったぐらい.他のブラウザだと,登録したブックマーク等から飛んだときにアクセスしようとする物もあるようだ.
しょうがないから,とりあえず,見に来たUAごとにカウントすることにした.errorlog_view()でエラー行をハッシュに保存したあとを以下のように変える.
#得られた行を並べ替えて表示
count_robot = Hash.new(0) # robots.txtを探しに来た回数を入れるハッシュ
count_fav = Hash.new(0) # /favicon.icoを探しに来た回数を入れるハッシュ
error.sort.each{|status, value|
print "<h3>" + status.to_s + " " + STATUS[status] +"</h3>\n"
print "<ol>\n"
error_line = error[status]
error_line.sort.each{|count, line|
#アクセスされたファイルによって処理を分ける
request = line.requestURI
case request
when %r|^/MSOffice/cltreq.asp|
when %r|^/_vti_bin/owssvr.dll|
# /MSOffice/cltreq.asp~ や /_vti_bin/owssvr.dll は無視
when "/robots.txt"
count_robot[line.user_agent] += 1
when "/favicon.ico"
count_fav[line.user_agent] += 1
else
print "<li><dl class=\"log\">\n"
print "<dt>Access URI</dt><dd>#{request}</dd>\n"
print "<dt>Datetime</dt><dd>#{line.year}年#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分#{line.second}秒</dd>\n"
print "<dt>IP Address</dt><dd>#{line.ip}</dd>\n"
print "<dt>Referer</dt><dd>"
case line.referer
when "-"
print "(ダイレクトアクセス)</dd>\n"
else
print "<a href=\"#{line.referer}\">#{line.referer_unencode}</a></dd>\n"
end
print "<dt>User Agent</dt><dd>#{line.user_agent}</dd>\n"
print "</dl><hr>\n"
end
}
print "</ol>\n"
}
#ロボットのアクセス回数を表示
print "<h3>robots.txtへのアクセス</h3>\n"
print "<ul>\n"
count_robot.each{|crawler,count|
print "<li>#{count}回:#{crawler}</li>\n"
}
print "</ul>\n"
#お気に入りアイコンへのアクセス回数を表示
print "<h3>favicon.icoへのアクセス</h3>\n"
print "<ul>\n"
count_fav.each{|ua,count|
print "<li>#{count}回:#{ua}</li>\n"
}
print "</ul>\n"
#フッタ表示
footer_out()
うわ,びびった~.1度でまともに動いたよ.珍しい.
今度はページ内を遷移する様子を表示する部分を作ろうか.
O係長,確かそれ,年休じゃなくて,代休の消化……
ページ内をどんな風に移動しているか表示することを考える.
欲しいのは,htmlを見に来た記録だから,以前作った生ログ表示と同じ条件で行を省く.その後の行の保持はエラーログ表示と似たような動作だ.1つのファイルの閲覧時間も把握できるといいから,次のファイルへのアクセス時間から前のファイルへのアクセス時間を引いて表示させることとする.
#ページ内遷移表示関数
#IPアドレス順に並び替えて,requestしたファイルを表示する.
def trace_view(file)
header_out("ページ内遷移")
logfile = open(file)
ignore = Regexp.new(IGNORE_FILE + "$")
#必要な行をとりあえずハッシュに保持
trace = Hash.new
trace_count = Hash.new
while logline = logfile.gets
line = Accesslog.new(logline)
unless ignore =~ line.requestURI #無視するファイルは処理しない
#必要行lineをtrace[ip][trace_count[ip]に入れていく.
save_line(trace, trace_count, line.ip, line)
end
end
#得られた行を並べ替えて表示
trace.sort.each{|ip, value|
print "<h3>" + ip + "</h3>\n"
print "<h4>" + line.user_agent + "</h4>\n"
print "<ol>\n"
access_time = Array.new
trace[ip].sort.each{|count, line|
access_time[count] = Time.mktime(line.year, line.month, line.day, line.hour, line.minute, line.second)
unless count == 1
period = access_time[count] - access_time[count - 1]
if period >= 60
minute = period.divmod(60)
print "<br>\n↓<br>\n↓ " + minute[0].to_i.to_s + "分"
print minute[1].to_i.to_s + "秒<br>\n↓</li>\n"
else
print "<br>\n↓<br>\n↓ " + period.to_s + "秒<br>\n↓</li>\n"
end
end
print "<li>" + line.requestURI
print "(#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分#{line.second}秒)"
}
print "</li>\n"
print "</ol>\n"
}
footer_out() #フッタ表示
end
save_lineは新たに作った関数で,引数に4つ取る.これは,前に作ったエラーログ表示部と共通で使う.
#必要行を保持する関数
#第1引数,第2引数はハッシュで,2つ揃ってハッシュのハッシュを作り,
#lineを保持する.
#termは,保存するときのハッシュのキーになる
def save_line(saver, saver_count, term, line)
unless saver.key?(term)
saver[term] = Hash.new
saver_count[term] = 0
end
saver_count[term] += 1 #ステータスエラーのカウンター
#ステータスエラーの行を保持
saver[term][saver_count[term]] = line
end
ありゃ?UserAgentがずれてないか?
表示を自分好みにする段階で,最初にこなければ行けないタグがどれで,くれ返さなければいけないのがどこで――と考えているうちに訳が分からなくなった.……美しくないなあ.
あと,秒数から分+秒の表記にするのがうまいこといっていない.これぐらい,メソッドありそうなんだけど,どんなもんだろ.
それに,ロボットの遷移と人の遷移は処理分けたいよなあ.
水曜日は定時で帰るよう通達がある.しかし,我が部署は通達破りで悪名をはせていた.
当然,今日もそうなるはずだった.というのは,17時半以降に仕事が入ることが確実だったからである.昼間が,暇で暇で仕方がないのがなお腹立たしい.
しかし,今日は通達の様相からして違った.
「台風の被害に遭わぬよう,即刻帰れ」
「今日は監事が見回ります」
以上の二点を受け,部長が,部署内に内線を掛けまくり,また,会う人会う人に今日は早く帰ってねと言い続ける.
最後通牒に,定時で電気消す宣言.
おかげで,今日やるはずだった仕事を放り出し,時間差出勤のせいで18時15分が定時だというのに,18時には帰路についていた.
まあ,降って沸いた幸運だと思うことにしよう.
珍しく,19時前に帰宅してしまったので,NHKを見たら,おお,今度の台風はすごいんすね.買い物に行きたかったけど,おとなしくしていよう.
今日はバグ取り等々をやることにする.
まず,エラー表示部.
エラー表示と言いつつ,robots.txtとかfavicon.icoへのアクセスを表示もすることにしたのだが,「エラーログの保持」→「エラーデータのあるステータスコードを見出しとして出力」→「エラーログの表示(ただし,robots.txtなどの場合は出力せず)」という流れにしていたため,「404 Not Found」と見出しを書いたあと,robots.txtとfavicon.icoしかエラーがない場合,見出しだけ出ているというお間抜けな状態だった.
考えてみれば,処理しないなら,そもそもエラーログを保持する必要もない.だから,リクエストファイルによる場合分けは先にしてしまうことにする.
それから,ハッシュのハッシュにしときながら,2つめのハッシュのキーは1, 2, 3...でしかなく,どう考えても,Arrayでこと足りるなと考えるに,難しく考えすぎたかと思い直し,コードを書き換えてみた.
#エラーログ表示関数
def errorlog_view(file)
#エラー行を保持するためのハッシュ
error = Hash.new
#エラーを表示するかどうかのフラグ.(status => 1 or 0)の形になる.
flg_error_view = Hash.new(0)
# /robots.txtを探しに来た回数を入れるハッシュ
count_robot = Hash.new(0)
# /favicon.icoを探しに来た回数を入れるハッシュ
count_fav = Hash.new(0)
header_out("エラーログ") #ヘッダ表示
logfile = open(file)
while logline = logfile.gets
line = Accesslog.new(logline)
#アクセスされたファイルによって処理を分ける
req_file = line.requestURI
ig_error = Regexp.new("^" + IGNORE_ERROR)
case req_file
when "/robots.txt"
count_robot[line.user_agent] += 1
next
when "/favicon.ico"
count_fav[line.user_agent] += 1
next
when ig_error #無視ファイルは何もしない
next
end
status = line.status
if /^[4|5]/ =~ status #ステータスコード400台と500台だけ処理
#必要行lineをerror[status]に入れていく.
#errorはハッシュで,{Key => Array}.
unless error.key?(status)
error[status] = Array.new
end
#ステータスエラーの行を保持
error[status] << line
end
end
#得られた行を並べ替えて表示
error.sort.each{|status, value|
print "<h3> " + status + " " + STATUS[status] +"</h3> \n"
print "<ol> \n"
value.each{|line|
print "<li> <dl class=\"log\"> \n"
print "<dt> Access URI</dt> <dd> #{line.requestURI}</dd> \n"
print "<dt> Datetime</dt> <dd> #{line.year}年#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分#{line.second}秒</dd> \n"
print "<dt> IP Address</dt> <dd> #{line.ip}</dd> \n"
print "<dt> Referer</dt> <dd> "
case line.referer
when "-"
print "(ダイレクトアクセス)</dd> \n"
else
print "<a href=\"#{line.referer}\"> #{line.referer_unencode}</a> </dd> \n"
end
print "<dt> User Agent</dt> <dd> #{line.user_agent}</dd> \n"
print "</dl> <hr> \n"
}
print "</ol> \n"
}
#ロボットのアクセス回数を表示
print "<h3> robots.txtへのアクセス</h3> \n"
print "<ul> \n"
count_robot.each{|crawler,count|
print "<li> #{count}回:#{crawler}</li> \n"
}
print "</ul> \n"
#お気に入りアイコンへのアクセス回数を表示
print "<h3> favicon.icoへのアクセス</h3> \n"
print "<ul> \n"
count_fav.each{|ua,count|
print "<li> #{count}回:#{ua}</li> \n"
}
print "</ul> \n"
footer_out() #フッタ表示
end
それから,生ログ表示部を作ったときに,「UTF-8を直せない」と言っていたのだが,uconvというものを知ったので,使ってみた.
RAAのuconvのところから,uconv-0.4.12.tar.gzをダウンロードし,展開した上で,すべてサーバー上の同じディレクトリにアップロードする.
それから,サーバーにシェルログインして,uconvのファイルを入れたディレクトリに移動.
ruby extconf.rbと打つと,Makefileができたよ~というメッセージが出るので,おもむろに
makeと打つと,めでたくuconv.soというファイルができあがった.
このファイルを,cgiスクリプトを置いているディレクトリに放り込み,require "uconv"!
でもって,文字コード変換をやっているreferer_unencodeを次のように書き換えた.
def referer_unencode #リファラーを返す(アンエンコード済)
ref = CGI.unescape(@all[10])
case NKF.guess(ref)
when 0
chr_in = "-J"
when 1
chr_in = "-S"
when 2
chr_in = "-E"
when 3
begin
return Uconv.u8toeuc(ref) + NKF.guess(ref).to_s
rescue
return NKF.nkf("-e", ref)
end
end
return NKF.nkf("-e" + chr_in, ref)
end
出力結果(検索文字列を含む物以外は,ばっさり削除)
......ギース様,大人気?――じゃなくて.
かなり変換できるようになったなぁ.(まんぞく,まんぞく)
ノーベル賞を取った日本人は12人いるそうだ.さあ,みんなでおもいだしてみよう.
詩人の川崎洋さんが亡くなった.
私がこの方を知ったのは,読売新聞のこどもの詩の欄で,はっきりと意識したのは,toschが「この人のコメントがおもしろい」と言ったからである.なるほど,そう言われてみればその通りだ.
勝手に45ぐらいだと思いこんでいたのだが,70を超えていたと初めて知った.それを知って,送られてくる無邪気な詩を読みながら目を細めている好々爺の姿が思われる.
ご冥福をお祈りします.
今日はサイト内遷移表示のバグ取り.
こないだ書いたコード"をよくよく見てみると,
trace.sort.each{|ip, value|
(略)
と書いてるくせに,中でもらっている変数がlineになっていた.これがおかしい元凶であるような気がする.
それで,書き換えてみた.書き換えの際,また繰り返すのがこの部分で,計算も繰り返しでする部分があって,1回目だけ通るのがここで,等と考えているうちにまた訳が分からなくなる.
それと,ハッシュのハッシュを配列のハッシュにしたので,その部分も書き換え.それと,無視するファイルも文末一致で除く物と,文中どこであれ除く物とに増やしたので,書き換えている.
#ページ内遷移表示関数
#IPアドレス順に並び替えて,requestしたファイルを表示する.
def trace_view(file)
header_out("ページ内遷移")
logfile = open(file)
ignore = Regexp.new("#{IGNORE_FILE_END}$|#{IGNORE_FILE}")
#必要な行をとりあえずハッシュに保持
trace = Hash.new
while logline = logfile.gets
line = Accesslog.new(logline)
#無視するファイルは処理しない
unless ignore =~ line.requestURI
#必要行lineをtrace[ip]に入れていく.
#traceはハッシュで,{Key => Array}.
unless trace.key?(line.ip)
trace[line.ip] = Array.new
end
#行を保持
trace[line.ip] << line
end
end
#得られた行を表示
trace.each{|ip, value|
print "<h3> " + ip + "</h3> \n"
value.each_with_index{|line, i|
if i == 0
print "<h4> #{line.user_agent}</h4> \n"
print "<ol> \n"
end
t_last = Time.mktime(line.year, line.month, line.day, line.hour, line.minute, line.second)
unless i == 0
period = conv_sec(t_last - $t_prev)
print "<br> \n↓<br> \n↓ #{period}<br> \n↓</li> \n"
end
$t_prev = t_last #t_prevに前の時間を保持
print "<li> "
print "#{line.requestURI}(#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分#{line.second}秒)"
}
print "</li> \n"
print "</ol> \n"
}
footer_out() #フッタ表示
end
conv_secは,引数にもらった数を秒数として,○時間○分○秒という文字列を返すメソッド.
# 秒数を引数にもらって,(秒,分,時)という配列を返す.
# 秒数によって,配列の長さは変わる.
TIME_CONV = [ 60, 60 ]
#助数詞
TIME_AN = [ "秒", "分", "時間" ]
def conv_sec(num)
t = Array.new
fmt_time = ""
t[0] = num.to_i
TIME_CONV.each{|s|
if t[-1] < s
break
else
t << t[-1] / s
t[-2] = t[-2] % s
end
}
t.each_with_index{|an, i|
fmt_time = an.to_s + TIME_AN[i] + fmt_time
}
return fmt_time
end
かえって分かりにくくなったかなあ.もし,日とか月とかに拡張するときも配列定数に書き加えるだけでいいかなあと思ったんだけど.
ちなみに,表示結果はこんな具合.うまくいっているような気はする.
今度はロボットのアクセスを分けてみよっと.
着ようと思った服が見つからず,頭をひねる.
なんせ,ワンルームマンションだ.置き場所だってそうはない.
……
……
……
もしかして.
おそるおそる覗いたベランダにあんぎゃ~,あんた,あの台風の暴風雨の中,そこで揺れてたんですか~.
ちょっと助けを求めてくれれば取り込んだ物を,とあり得ないことなど考えながら,何日目かの救助にあたったのであった.
ロボットのアクセスを分けることにする.特に,ページ内遷移は,人のアクセスだったら「どのぐらいの時間を掛けてるのかな~」と思うけど,クローラーなら一瞬でどんどん読み込んでいっちゃうんだろうから滞在時間なんていらない.
実のところ,だんだん慣れてきたので,コードを書くのはそれほど大変じゃなかった.大変だったのは,どのユーザーエージェントがロボットで,そのロボットはどういう検索エンジンのロボットかを調べることだった.調べている最中にメールアドレス収集ロボットのUAをいくつか知ったので,.htaccessではねておいた.
ま,そいつはともかく,調べた結果を,「ユーザーエージェント名」,「その説明」のタブ区切りテキストにした.このファイルの名前をrobotsinfo.txtにして,定数ROBOT_TXTに読み込んでおく.
それから,ロボット情報読み込みのためのモジュールを作っておく.
#クローラー情報関連読み込み関数
module Crawler
#クローラー情報メソッド:返り値はハッシュで,{ roboto_ua => comment }
def robot_info
r = Hash.new
robotfile = open(ROBOT_TXT)
while roboline = robotfile.gets
robo = roboline.chomp.split(/\t/)
r[robo[0]] = robo[1]
end
robotfile.close
return r
end
module_function :robot_info
#クローラーのUserAgent名を返すメソッド:返り値は正規表現
def robot_name
robot_ua = ""
robotfile = open(ROBOT_TXT)
while roboline = robotfile.gets
robo = roboline.chomp.split(/\t/)
robot_ua = robot_ua + robo[0] + "|"
end
robotfile.close
return Regexp.new(robot_ua.chop)
end
module_function :robot_name
end
いまいち,何をクラスにして,何をメソッドにして,何をモジュールにしたらいいのか分かってないんだよなぁ……
ともかく,ロボットの情報を書いたファイルを読み込んで,変数に入れていくメソッドを作ってみた.(robot_infoは,今回はまだ使わない)
似たようなことしてるんだけど,これって1つにできないのかなぁ.
ま,それは置いといて,あとは簡単で,呼び出し側に robot_re = Crawler::robot_name
と書いておいて,無視ファイルを決めているunless文にunless ignore =~ line.requestURI || robot_re =~ line.user_agent
と書き入れる.
これでおしまい.今回は簡単だったな.
紙媒体のリファレンスが欲しいと思って買ったんだけど,簡潔すぎて使いこなせてません.
なんせ,サンプルがない.
Rubyをバリバリ使っている人が,ちょっと思い出したいときに使うもんなんでしょうね,きっと.
こないだからアクセスログを整形することばかりに力を注いでいるんですが,気になるログがある.それは,「\x83W\x83\x87\x83W\x83\x87\x82\xcc\x8a\xef\x96\xad\x82\xc8\x96`\x8c\xaf」とかいう訳の分からない検索文字列をrefererに持つ物である.何か気味が悪い.しかし,調べても他のサーバーのアクセスログしか出てこなくて,何であるかは分からない.
誰か情報プリーズ.
実は,富山に帰る切符を取った日に地震が起き,取った切符の路線が思いっきり不通になってしまいました.越後湯沢まで行けてもなあ…….その先のほくほく線の復旧は1週間程度を予定しているようですが,帰る日に間に合うかどうか微妙なところだったので,米原経由に切り替えました.
このとき,JRのおねーさんに訊いていて思ったのですが,もし,地震の後に買っていたら手数料無しの払い戻しはなかったのかしらん.
そして,7000円ほど高かった.時間もかかるんだろう.
I君が「富山はどうだったんですか」と訊くので,「知らないよ,東京にいるのに知るわけないじゃないか」と言ったら,Yさんが「また,こんなこと言ってるよ」とのたまわった.
しかしだね,言わせて欲しい.
私は地震の起きた日,実家に電話を掛けている.
これで何をどう心配しろと言うんだ.
と,Yさんに文句を言ったら,「親子断絶?」と訊かれた.いや,断絶してたら帰えんねって.
17日あたりからずっと咳が止まらない.電話に出ている最中に咳が出るのには参っていたものの,他の症状がないので,放っておいたのだが,係の人が嫌そうな顔をするので観念して医者に行ってきた.
夕方から,なんだかのどが痛いような気もしないでもなくなってきたんだけど,医者が行ったからかもしれない.
やっぱり咳がときどき押さえがたくなるだけで,普段は肺を膨らませにくい(息が吸いにくい)気がするだけだ.
寝てりゃ直るんだろう.しぱらく大人しくしていよう.
それでは,本日はロボットの挙動を表示する部分をば.
どんな風に表示するかをまず考える.サイト内遷移の表示をベースに,滞在時間はいらんなとか,同じ検索エンジンのクローラーがいっぱい来るかもしれないから,それはひとまとめにしたいよな,と考え,
ロボットの説明
ユーザーエージェント名/IPアドレス
……
……
アクセスしたファイルのリスト
……
……
というのの繰り返しにしようと考える.そこで,作っておいたrobot_infoも利用して,以下のコードを書いた.
#サイト内遷移表示関数(ロボット用)
def trace_robot(file)
header_out("クローラー/オートパイロット-サイト内遷移")
robot_info = Crawler::robot_info #ロボット情報のハッシュ
ignore = Regexp.new("#{IGNORE_FILE_END}$|#{IGNORE_FILE}")
logfile = open(file)
#必要な行をとりあえずハッシュに保持
trace = Hash.new
while logline = logfile.gets
line = Accesslog.new(logline)
#無視するファイルは処理しない
unless ignore =~ line.requestURI
robot_info.each_key{|ua|
reg_robot = Regexp.new(ua) #クローラー情報にあるUAを正規表現にする.
if reg_robot =~ line.user_agent
#必要行をtrace[ua]に入れていく.
#traceはハッシュで,{Key => Array}.
unless trace.key?(ua)
trace[ua] = Array.new
end
#行を保持
trace[ua] << line
end
}
end
end
logfile.close
#得られた行を表示
trace.each{|ua, value|
print "<h3>#{robot_info[ua]}</h3>\n" #最初にロボットの説明を見出しにする.
#ユーザーエージェントとIPを表示する.
print "<h4>ユーザーエージェント【IPアドレス】</h4>\n"
print "<ol>\n"
#重複するUA/IPを削るための操作
ua_ip = value.collect{|rline| "#{rline.user_agent}【#{rline.ip}】"}
ua_ip.uniq.sort.each{|rua| print "<li>#{rua}</li>\n"}
print "</ol>\n"
print "<hr>\n" #区切り
#アクセスされたファイルを表示
print "<h4>リクエストファイル</h4>\n"
print "<ol>\n"
value.each_with_index{|line, i|
print "<li>#{line.requestURI}(#{line.month}月#{line.day}日"
print "#{line.hour}時#{line.minute}分)</li>\n"
}
print "</ol>\n"
print "</div>\n"
}
footer_out() #フッタ表示
end
最初,出力してみたら,見にくかったので,今回,CSSもちょっといじっている.出力サンプルいや,Googleクローラーのすごいこと,すごいこと.
しまった,気づいちゃいたんだけど,IPアドレスのままの物とDNSを引いてリモートホストに変換された物が入り交じってるけど,重複する物ってあるよな.
どうやったらIPを変換できるかなぁ.
勝手に「ウサギ本」と呼んでいる.
評判がよいので,読んでみたいなぁと思っているところに転がり込んできた図書券5000円.そりゃ,買いますわ,あんさん.
文書に漂うジョークの具合が何とも翻訳本くさく,微笑んでしまった.昔から翻訳物に慣れ親しんでいる身としては,嫌いじゃない.
内容は,ちょっぴり高度で,プログラミング未経験という人には向かないんだろうけど,丁寧なので,「かじりました~」ぐらいの人(=私ぐらいの人)ならついて行けるんじゃないだろうか.
現在,一生懸命勉強中.
ちょっと失敗したと思ったのは,買ってしまってから,Ruby1.8対応の2nd Editionが近日中に出版されると知ったこと.もしかしたら,そっちも買っちゃうかもなあ.
あ,言うまでもないことだが,これは,英語版.翻訳されるかどうか知らない.あと,作者のサイトから,PDF版も買えるようだ.(英語の取引になるけど)
ちなみに,1st Editionでかつ英語でかつ抄で良ければ,Ruby Centralで読める.
IPをリモートホストに変換するべく,前に作ったAccesslogクラスのipメソッドを書き換えた.
def ip #リモートホスト名を返す
ip = @all[0] #IPアドレス/リモートホスト
begin
#数字.数字.数字.数字という文字列だったら
if /^\d+\.\d+\.\d+\.\d+$/ =~ ip
ip = Resolv.getname(ip).to_s
end
rescue
end
return ip
end
あ,これを使うにあたってresolv.rbをrequireしてますよ.
なぜか,エラーになることがあるので,rescueして――結局,何でエラーになるのか分らないので,何もしていない.......へたれだ.
ipじゃなくてremote hostだよな,完全に名前に偽りありだ.
正規表現で回数指定の繰り返しを使えるのを知ったので,条件設定を
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ =~ ip
にしてみた.
それから,エラーの対処なんだけど,とりあえず,いったんエラーになったIPは処理しないようにしてみた.
begin
if /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ =~ ip #数字.数字.数字.数字という文字列だったら
#エラーになったことのあるipでなければDNSをひく
ip = Resolv.getname(ip).to_s unless @@error_ip.include?(ip)
end
rescue Resolv::ResolvError, Resolv::ResolvTimeout
@@error_ip << ip #DNSエラーになるipは以後,処理しない
end
他にも,Resolvでタイムアウトになるものの処理も高速化させたいのに,timeoutやらresolv-なんとか(忘れた)など使っていろいろやってみたけど,上手く捕まえられない.
あ,なるとちゃん(@@error_ipのこと)は,外でArray.newしてます.
« 2004年9月 | | 2004年11月 »