Apacheのログをパースしてみる

昨日の日記で宣言したように,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でうちのサーバにいたずらしたらしいのですが,それが良くわかって良いですね!