昨日の日記で宣言したように,ApacheのログをRubyでパースするなどしたよ.あくまでも,勉強会当日ではなくて,今日まったり書いたコードだよ.
第二問 ログ解析
Apacheのログを解析して以下を調べる
- '/index.php'へのアクセス回数
- 最初のMacユーザのアクセスの日時
- Goolge経由のアクセス数
- 曜日別アクセスのランキング
- ブラウザごとのアクセスランキング
手元にあったapacheのログがリファラを取ってなかったので,Google経由のアクセス数は調べませんでした.
準備
以下のように,ApacheのログのEntryを抽象化したApacheLog::Entryクラスを書いた.テストケースは,こちら.
そこそこ一般的だと思われる,以下のような形式のログが対象.
192.168.0.1 - - [08/Feb/2008:04:15:44 +0900] "GET /plagger/plugins/CustomFeed/Hoge.pm HTTP/1.1" 404 239 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
require 'date' module ApacheLog class Entry def initialize(args) @ip_address = args[:ip_address] @identity_check = args[:identity_check] @user = args[:user] @datetime = args[:datetime] @request = args[:request] @status = args[:status] @size = args[:size] @referer = args[:referer] @user_agent = args[:user_agent] end attr_reader :ip_address, :identity_check, :user, :datetime, :request, :status, :size, :referer, :user_agent def self.parse(line) match = line.match(log_pattern) raise "parse error\n at line: <#{line}> \n" if match.nil? captures = match.captures self.new({ :ip_address => captures[0], :identity_check => captures[1], :user => captures[2], :datetime => parse_datetime(captures[3]), :request => parse_request(captures[4]), :status => captures[5], :size => captures[6], :referer => captures[7], :user_agent => captures[8], }) end # private class methods def self.log_pattern / ^ (\S+) # ip_address \s+ (\S+) # identity_check \s+ (\S+) # user \s+ \[ (.*?) \] # date \s+ " (.*?) " # request \s+ (\S+) # status \s+ (\S+) # size \s+ " (.*?) " # referer \s+ " (.*?) " # user_agent $ /x end def self.parse_datetime(str) DateTime.strptime( str, '%d/%b/%Y:%T %z') end def self.parse_request(str) method, path, protocol = str.split { :method => method, :path => path, :protocol => protocol } end private_class_method :log_pattern, :parse_datetime, :parse_request end end
インスタンス変数を初期化するコードがもっさりしていて気にくわない.ログをパースしている正規表現はいろいろ穴があるのですが,そんなに正確性はいらないのでスルー.
実行
ちょっとだけ複雑なコードになったので,昨日みたいにirbでやるのではなくスクリプトを書いた.
require 'apache_log' require 'set' # 行ごとにログを読み込んでApacheLog::Entryオブジェクトにする entries = [] File.foreach('apache.log') do |line| entries << ApacheLog::Entry.parse(line.chomp) end # index.phpの数 puts "index.php count: " + entries.select {|e| e.request[:path] =~ /index\.php/} .size.to_s # はじめてMacでアクセスされた時刻 puts "Mac first access time: " + entries.find {|e| e.user_agent =~ /Macintosh;/} .datetime.to_s # Google経由でのアクセス数 #puts "Google count = " + # entries.select {|e| e.referer =~ /google/} .size.to_s entry_set = Set.new(entries) # 曜日ごとのランキング puts "Weekday ranking: " classified = entry_set.classify { |e| Date::DAYNAMES[e.datetime.wday] } classified.sort { |a, b| b[1].size <=> a[1].size }.each do |e| puts " #{e[0].slice(0,3)}: #{e[1].size}" end # ブラウザのランキング puts "Browser ranking: " classified = entry_set.classify { |e| e.user_agent} classified.sort { |a, b| b[1].size <=> a[1].size }.each do |e| puts " #{e[0]}: #{e[1].size}" end
Entryの検索は良くやりそうなのでApacheLog::Entryのコレクションクラスを用意して,findメソッドとかを書くと,より良さそう.いつぞやujihisaに教えてもらった,Set#classifyがかっこよすぎるね!
実行すると以下のような感じ.
> ruby apache_log_analyze.rb index.php count: 0 Mac first access time: 2008-02-27T21:46:14+09:00 Weekday ranking: Wed: 353 Thu: 73 Fri: 22 Tue: 22 Mon: 17 Sat: 9 Sun: 4 Browser ranking: Wget/1.10.2: 287 -: 86 Baiduspider+(+http://www.baidu.com/search/spider_jp.html): 15 iTunes/7.6.1 (Macintosh; N; Intel): 15 iTunes/7.6 (Windows; N): 10 # 略
ログを適当に500件抽出してテストデータにしたので結構偏ってます.このころ,友達がwgetでうちのサーバにいたずらしたらしいのですが,それが良くわかって良いですね!