Categories
ruby

ruby + mongoDBでapacheログの解析システムを作る

備忘録です。
もうツールに操られるのは嫌なので、
rubyとmongoDBで生ログの解析ができるようにしようと思っています。

mongoDBを選んだのは、特に意味があるわけではないですけど、
大量のデータになったら、MySQLじゃきついんではないかなという
思い込みと、noSQL使ってみたかったというだけです。

まだまだプロトですが、
・ログをパースして、DBにぶち込むスクリプト(ruby_log.rb)
・簡易的に集計するスクリプト(count.rb)
だけ作りました。

パースの方は
・ApacheLogRegexを使って、ログをパース
・色々フィルターかけて、扱いやすい形に変換
・MongoDBにぶち込む
という流れです。
MongoDBについては、あえてDataMapperは使わないでやっています。
mongo_mapper使っちゃうと、MongoDBの良さが半減しそうな
感じがしたので、避けています。
どうせRailsとか使わないし。
(candyはちょっと使ってみたいと思ったんですけど、
うまく動作せず断念。。。)
リファクタリングすれば、もうちょっと綺麗に書けそうな気はします。
ほんとは設定系の内容は別ファイルにした方がよさそうですけどね。

集計の方はMap/Reduceを使ってみました。
いまいちMap/Reduceの使い方がよく分からなくて、
単純集計しか出来ていません。
あと、エンコード周りは面倒なので、後回しにしています。。。。
なので、検索キーワードはかなり微妙な集計結果になってるかもしれません。
もうちょっとMap/Reduceを調べて、複雑な集計をできるようにしようと思います。

ruby_log.rb

# -*- coding: utf-8 -*-
Encoding.default_external="UTF-8"
require 'mongo'
require 'apachelogregex'
require 'time'

#set the path of apache log file
LOGFILE = 'logfile.log'

    class LogFilter
        def main(obj)
            if(pattern(obj))
                res = self.file_type(obj)
            end
            return res
        end
        def file_type(obj)
            val = Hash.new()
            val['page'] = self.pages(obj['%r'])
            val['referer'] = obj['%{Referer}i']
            val['ip'] = obj['%h']
            val['ua'] = obj['%{User-Agent}i']
            val['ip_ua'] = obj['%h'] + "__" + obj['%{User-Agent}i']
            val['time'] = self.p_time(obj['%t'])
            search_kw = self.parse_kw(obj)
            if(search_kw)
                search_kw.each_pair do |x,y|
                    val['search'] = x.to_s
                    val['kw'] = y.to_s
                end
            else
                val['search'] = "none"
                val['kw'] = "none"
            end
            return val
        end
        def pattern(hsh)
        #set the file type to be removed
            file_paturns = [
                    /.gif/i,
                    /.png/i,
                    /.jpg/i,
                    /.swf/i,
                    /.css/i,
                    /.js/i,
                    /.ico/i,
                    /.mp3/i,
                    /.txt/i
                ]
        #set the user agent to be removed
            ua_fil = [
                    /bot/i,
                    /msn/i,
                    /ichiro/i,
                    /Hatena/i,
                    /Yahoo! Slurp/,
                    /findlinks/
                ]
            file_paturns.each do |reg|
                if(hsh['%r'].match(reg))
                    return false
                    break
                end
            end
            ua_fil.each do |reg|
                if(hsh['%{User-Agent}i'].match(reg))
                    return false
                    break
                end
            end
            return true
        end
        def pages(str)
            return str.gsub(/^GETs|^POSTs|^HEADs/,"").gsub(/sHTTP.+$/, "")
        end
    def p_time(str)
      str = str.gsub("[","").gsub("]","")
      mm = /^(d+)/([a-zA-Z]+)/(d+):([d:]+) ([+-]d+)/.match(str)
      post_time = Time.parse("#{mm[1]} #{mm[2]} #{mm[3]} #{mm[4]} #{mm[5]}")
      return post_time
     end
        def parse_kw(hsh)
            #set the search engine list
            list = {
                "google" => ["q", /google.co.jp/search?/],
                "google_com" => ["q", /google.com/search?/],
                "yahoo" => ["p", /search.yahoo.co.jp/search/]
            }
             str = hsh["%{Referer}i"]
             tmp = Hash.new

             list.each_pair do |x,y|
                    if(str.match(y[1]))
                        reg = /#{y[0]}=[^&]*/
                        tmp[x] = (str.scan(reg)[0]).gsub!("#{y[0]}=","")
                    end
            end
            if(!tmp.empty?)
                return tmp
            else
                return false
            end
         end
    end


fil = LogFilter.new
    con  = Mongo::Connection.new
    db      = con.db('log')
    logs = db.collection('Log')

# difinition of apache log format
    format = '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"'
    parser = ApacheLogRegex.new(format)

    i = 0
    File.foreach(LOGFILE) do |line|
        begin
                 res = fil.main(parser.parse(line))
                 if(res!=nil)
                    #p res
                    logs.insert(res)
                 end
        rescue ApacheLogRegex::ParseError => e
            puts "Error parsing log file: " + e.message
        end
    end

    puts "end file:#{LOGFILE}"

count.rb

# -*- coding: utf-8 -*-
Encoding.default_external="UTF-8"

require 'rubygems'
require 'mongo'
require 'uri'

class SingleMp
    def initialize(str)
        @str = str
        @con     = Mongo::Connection.new
        @db     = @con.db('log')
        @logs = @db.collection('Log')
    end
    attr_accessor :str

    def main
        self.printMp(self.getMp)
    end

    def getMp
        @map = "function() { emit(this.#{@str}, 1); }"
        @reduce = "function (k, vals) {" +
            "var sum = 0;" +
            "for (var i in vals) {" +
                    "sum += vals[i];" +
            "}" +
            "return sum;" +
        "}"
        @res = @logs.map_reduce(@map, @reduce)

        return @res
    end
    def printMp(obj)
        obj.find().each do |x|
            if(x['_id']!=nil)
                print (@str + ":t" + URI.unescape(x['_id']) + "t=>t" + x['value'].to_s + "n")
            end
        end
    end
end




mp_page = SingleMp.new('page')
mp_page.main

puts ("n---------------------nn")

mp_ua = SingleMp.new('ua')
mp_ua.main

puts ("n---------------------nn")

mp_ipua = SingleMp.new('ip_ua')
mp_ipua.main

puts ("n---------------------nn")
mp_ipua = SingleMp.new('referer')
mp_ipua.main


puts ("n---------------------nn")
mp_ipua = SingleMp.new('kw')
mp_ipua.main

puts ("n---------------------nn")
mp_ipua = SingleMp.new('search')
mp_ipua.main

*11/22 timeのパースがうまくいってなかったので、修正しました。
もっとスマートにパースできなかなー。

Categories
GoogleAnalytics

[ga]utm_系のパラメータを短い1つのパラメータで代用する方法

GAのパラメータって長いですよね。
utm_sourceだとか、utm_campaignだとか。
それはそれで階層化できて便利な反面、パラメータの管理が複雑になって大変になってしまうという負の側面もあります。
ポイントは4つもパラメータを付けなくてはいけないという点だと思います。
それによって、管理が複雑になってURLが長くなってしまいます。
短縮URLを使わずにURLをTwitterやFacebookとかに載せるのは難しい状態です。

以前、Web担当者フォーラムで
長くて汚いutm_*パラメータ付きURLをキレイにするGoogle Analytics用の少しマニアックなスクリプト
という記事があったので、これにインスパイアされて1つパラメータで4つのパラメータの代用をするスクリプトを書きました。
Web担の方はページが表示された後にリダイレクトされてパラメータ消えるという何が嬉しいのかいまいちわからない仕組みでしたが、こちらは逆にどこかのサイトにURLを張るときにパラメータ1つで4つ分の意味を持たせるというスクリプトです。

仕組みは単純です。
1.パラメータを見る
2.JS内の定義データと照合させる
3.マッチしたら、定義データの通りにutm_系のパラメータを展開させる
4.utm_付きのURLにリダイレクトさせる

例)
http://tanakanakana.happy888.net/async.html?twitid=neko
にアクセスすると、
http://tanakanakana.happy888.net/async.html?twitid=neko&utm_source=twitter&utm_medium=twitter&utm_campaign=neko&utm_content=neko2010_10_2
にリダイレクトされます。
当然、utm_系のデータはGAに記録されます。
http://tanakanakana.happy888.net/async.html?twitid=neko
はGAにデータが飛ぶ前にリダイレクトされるので計測されません。

こんな感じです。
現在は一つのJSファイルの中に定義データも含めていますが、
定義データはjsonでDBから吐き出すみたいな感じだと運用しやすいかもしれません。

ga_rd.js

var _ga_url = {
//config
    conf : {
        twitid : ["twitter", "twitter"],
        mixiid : ["mixi", "mixi"]
    },

    getQuery : function(name){
        if(location.search){
                var query = location.search;
                query = query.substring(1,query.length);
                var qArray = [];
                qArray = query.split("&");
                for(var i=0;i<qArray.length;i++){
                    var param = qArray[i].split("=");
                    if(param[0] == name){
                        return param[1];
                    }
                }
            }
        },

    get_param : function(str){
            var date = new Date();

            var param_str = this.getQuery(str)
            return "&utm_campaign=" + param_str + "&utm_content=" + param_str + date.getFullYear() +"_" + date.getMonth() + "_" + date.getDay()
        },


    main : function(){
        for (var i in this.conf){
            if((!(location.search).match(/utm_/)) && this.getQuery(i)){
                //alert(i);
                var str = "&utm_source=" + this.conf[i][0] + "&utm_medium=" + this.conf[i][1] + this.get_param(i);
                var url_str = document.URL + str;
            location.href = url_str;
            }
        }
    }
}
_ga_url.main();

conf 内で各パラメータのutm_sourceとutm_mediumを定義します。
utm_campaignはパラメータのvalue値、utm_contenはvalue値+日付にしています。
全部指定するのは面倒なので、とりあえずこうしていますが、
DBから吐き出す仕組みがあれば、全て指定するようにしてもいいと思います。