memcached/APCとネームスペース、トランザクション

PHPでmemcacheやAPCを使ったキャッシングを色々試してみて、気付いた事をメモ。

memcacheはクエリで取得した値のキャッシュに使い、それ以外の色々な事にはAPCを使うのが良いような気がする。APCはmemcacheより10倍程高速なようだが(Cache Performance Comparison | MySQL Performance Blog)、httpd再起動で消えてしまう為、大量にキャッシュして使いたいデータベースのキャッシュにはちょっと(PHP等の設定をよく変更するのでhttpdは結構再起動する)。memcacheはやや遅いが、データベースの負荷を軽減するのに良い感じ。

以下は、キャッシュ使ってて気付いた、ネームスペースとトランザクション絡みの考察。

ネームスペース

memcachedAPCは、ネームスペースのような機能をサポートしていない。あるオブジェクトをキャッシュしようと思えば、キーに紐付けて保存することになる訳だが、このままだと、例えば、あるテーブルに対するクエリの結果をキャッシュしようとした場合に、プライマリキーを利用してキーとし、クエリの結果オブジェクトをキャッシュに保存していく、といった使い方になる。ここで問題となるのが、複数のテーブルの行をキャッシュしていった場合に、あるテーブルに対応したキャッシュだけを削除したい場合にどうするのか?という点。キーを指定して保存/取得/削除する事しか出来なければ、複数のアプリケーションから同じmemcachedを使っていた場合等にも、キャッシュの特定の部分を纏めて削除する、といった事が出来ず使い勝手が悪い。

この問題を解決する為に、以下の方法で擬似的にネームスペースを実装してやることが出来る。

これにより、例えば、テーブル名に対応したネームスペース以下に、キーに対応したクエリ結果オブジェクトを保存してやり、削除時には、キーによる削除に加えて、ネームスペースを指定しての、ネームスペース以下の全削除も可能となる。

方法は、下記memcachedのFAQに載ってるそのまんま。

FAQ / memcached

PHPの場合、memcache用の下記のようなクラスを作れば、

class MyMemcache
{
    private $option = array('host' => 'localhost',
                            'port' => 11211,
                            'timeout' => null,
                            'cache_compress' => false,
                            'cache_expire' => 0);
    private $memcache = null;

    public function __construct()
    {
        $this->memcache = new Memcache();
        $this->memcache->connect($this->option['host'],
                                 $this->option['port'],
                                 $this->option['timeout']);
    }
    
    public function loadCache($namespace, $key)
    {
        $keyName = $this->createCacheKey($namespace, $key);
        return $this->get($this->memcache, $keyName);
    }

    public function saveCache($namespace, $key, $value, $option = null)
    {
        $keyName = $this->createCacheKey($namespace, $key);
        return $this->set($this->memcache,
                          $keyName,
                          $value,
                          $option !== null ? $option : $this->option);
    }

    public function deleteCache($namespace, $key = null)
    {
        if ($key === null) {
            return $this->increment($this->memcache, $namespace, 1);
        } else {
            $keyName = $this->createCacheKey($namespace, $key);
            return $this->delete($this->memcache, $keyName);
        }
    }

    public function flushCache()
    {
        $this->flush($this->memcache);
    }

    private function set(Memcache $memcache, $key, $item, $option)
    {
        return $memcache->set($key, $item,
                              $option['cache_compress'],
                              $option['cache_expire']);
    }

    private function get(Memcache $memcache, $key)
    {
        return $memcache->get($key);
    }

    private function delete(Memcache $memcache, $key)
    {
        return $memcache->delete($key);
    }

    private function increment(Memcache $memcache, $key, $value){
        return $memcache->increment($key, $value);
    }

    private function flush(Memcache $memcache){
        return $memcache->flush();
    }

    private function createCacheKey($namespace, $key)
    {
        $namespaceKey = $this->get($this->memcache, $namespace);
        if ($namespaceKey === false) {
           
            $namespaceKey = 0;
            $this->set(
                $this->memcache,
                $namespace,
                $namespaceKey,
                $this->option);
        }
        $k = is_object($key) ? sha1(serialize($key)) : sha1($key);
        return "$namespace:$namespaceKey:$k";
    }
}

$m = new MyMemcache();
$m->saveCache('hogeNamespace', 'fooKey', 'foo'); //hogeNamespaceにfooKeyで'foo'保存
$m->saveCache('hogeNamespace', 'barKey', 'bar'); //同様に'bar'保存
$m->saveCache('fugaNamespace', 'fooKey', 'foofoo'); //fugaNamespaceにfooKeyで'foofofo'保存
$foo = $m->loadCache('hogeNamespace', 'fooKey'); //hogeNamespaceのfooKeyの値を取得
$bar = $m->loadCache('hogeNamespace', 'barKey'); //同様にbarKeyの値を取得
var_dump($foo); //string 'foo'
var_dump($bar); //string 'bar'
$m->deleteCache('hogeNamespace'); //hogeNamespaceを削除
$foo = $m->loadCache('hogeNamespace', 'fooKey'); //削除されたnamespaceから値を取得
$bar = $m->loadCache('hogeNamespace', 'bar');
var_dump($foo); //boolean false
var_dump($bar); //boolean false
$foofoo = $m->loadCache('fugaNamespace', 'fooKey'); //fugaNamespaceのfooKeyの値を取得
var_dump($foofoo); //string 'foofoo'
$m->deleteCache('fugaNamespace', 'fooKey'); //fugaNamespaceからキーを指定して削除
$foofoo = $m->loadCache('fugaNamespace', 'fooKey');
var_dump($foofoo); //boolean false

これで、上記のように、ネームスペースを指定して保存/取得/削除が出来るようになる。

ポイントは、ネームスペースに対応したキーに適当な整数値を保存しておく点。これをバージョンナンバーとして使い、キャッシュにアクセスする際に、キーの一部にネームスペースとバージョンナンバーを含めておく。そして、あるネームスペースを削除したい場合は、当該ネームスペースのバージョンナンバーをインクリメントする。これにより、取得時に新たなバージョンナンバーがキーに含まれるため、古いキーの値にはアクセス出来なくなる。実際にキャッシュからデータが削除される訳ではないが、キャッシュの容量が一杯になれば、使用されていないキーから消されいくので、放って置けば良い。

以上はmemcachedの場合だが、APCでも同様にネームスペースを実装出来る。(APCにincrementメソッドは無いので、一旦取得して値をインクリメント後、保存するようなincrementメソッドを自前で用意すればOK。)

トランザクション

キャッシュにクエリの結果を保存するような使い方をしている場合、更新系クエリの後に、キャッシュの値も追加/更新/削除するように実装すると思うのだが、当たり前ながら、トランザクション中で更新を行っている場合、キャッシュの方もコミット後にまとめて更新しなければならない。ここで少し気を付けないといけないのが、トランザクション中で更新を行った時点からコミットまでの間の、影響の受けるキャッシュの値に扱いについて。

例えば、あるテーブルのid=2の値をキャッシュしてあったとして、これをトランザクション内で更新したとする。そのトランザクション内で、再度id=2の値を検索した場合、更新後の値を取得出来なければならないが、コミットまでの間、トランザクションの外からは古い値が見えないといけない。この為には、コミットまでの間、キャッシュには更新後の値も更新前の値も残っていないようにするか、もしくは、キャッシュには更新前の値を残すが、トランザクション内からは更新後の値が取得出来るように細工しなければいけない。後者はやや複雑になるのに比べ(キャッシュを扱うクラス内でバッファリングしておいてゴニョゴニョするとか)、前者は更新時点で影響を受けるキャッシュを一旦削除するだけで済み簡単である。キャッシュを削除するので、コミットまでの間、トランザクション内/外どちらからもデータベースへのアクセスが必要になってしまうが、これは仕方ないものとする。


整理すると、

  • 更新系クエリ発行
    1. 影響を受けるキャッシュを削除する。
    2. 更新内容をバッファリングする。
  • 参照系クエリ発行
    1. キャッシュが存在すればそれを使用する。
    2. さもなければ、データベースにアクセスして値を取得し、取得内容をバッファリングする。
  • コミット後
    1. バッファリングした処理をまとめてキャッシュに書き出す。


先程のMyMemcacheのコードに下記のように追加/変更を行う。saveCache()内ではsetは行わず、delete後バッファに処理を追加している。また、deleteCache()内でも、delete/increment後にバッファに処理を追加する。これで、トランザクション開始前にclearBuffer()、コミット後にwriteBackCache()を行えばOK。ちなみに、delete/incrementも(一旦実際に削除したのにも関わらず)バッファリングしておかなければならない。さもないと、例えばあるキーをsaveCache()後にdeleteCache()した場合に、writeBackCache()時点で、saveCache()の分の書き戻しでキャッシュに値が保存されてしまう。

    private $buffer = array();

    public function saveCache($namespace, $key, $value)
    {
        $keyName = $this->createCacheKey($namespace, $key);
        $this->delete($this->memcache, $keyName);
        $this->buffer[] = array('method' => 'save',
                                'key' => $keyName,
                                'value' => $value);
    }

    public function deleteCache($namespace, $key = null)
    {
        if ($key === null) {
            $deleted = $this->increment($this->memcache, $namespace, 1);
            $this->buffer[] = array('method' => 'increment', 'key' => $namespace, 'value' => 1);
            return $deleted;
        } else {
            $keyName = $this->createCacheKey($namespace, $key);
            $deleted = $this->delete($this->memcache, $keyName);
            $this->buffer[] = array('method' => 'delete', 'key' => $keyName);
            return $deleted;
        }
    }

    public function clearBuffer()
    {
        $this->buffer = array();
    }

    public function writeBackCache()
    {
        $result = true;
        foreach ($this->buffer as $buf) {
            if ($buf['method'] == 'save') {
                $saved = $this->set($this->memcache,
                                    $buf['key'],
                                    $buf['value'],
                                    $this->option);
                if(!$saved) {
                    $result = false;
                }
            } elseif ($buf['method'] == 'delete') {
                $this->delete($this->memcache, $buf['key']);
            } elseif ($buf['method'] == 'increment') {
                $this->increment($this->memcache, $buf['key'], $buf['value']);
            }
        }
        $this->buffer= array();
        return $result;
    }

第26回 Ruby/Rails勉強会@関西に参加

日本Rubyの会 公式Wiki - 第26回 Ruby/Rails勉強会@関西
http://jp.rubyist.net/?KansaiWorkshop26


遅くなったが、Ruby初級者向けレッスン by okkezさんの回答だけでも貼っとこう。そういえば、前々回の分が、okkezさんに添削されていたのだった。今更ですがありがとうございます。気付かなくて申し訳なかったです。


勉強会の内容については、

  • Rack, nginx, thinが面白そうなので、調べたい。
  • ujihisaさんの発表で出たvimscriptだが、vimscriptからRubyを使うことが出来るのか。emacs(elisp)からRubyが使えたりもするのだろうか。elispなんか全く分からないが。ちょっと調べた感じでは情報が見つからない・・・。

という感じでした。

1. ナベアツ問題

Macだと簡単にしゃべらせることが出来るようだ。楽しそう。でも、windowsでがんばってしゃべらせる気にはならないなあ。

MIN = 1
MAX = 100

class Nabeatsu
  def perform(min, max)
    min.upto(max) do |i|
      speech = ""
      speech << "Aho" if should_be_idiot?(i)
      speech << "Meaow" if should_be_animal?(i)
      speech << i.to_s if speech == ""
      puts speech
    end
  end
  
  def should_be_idiot?(num)
    num % 3 == 0 || num.to_s =~ /3/
  end

  def should_be_animal?(num)
    num % 5 == 0
  end
end

Nabeatsu.new.perform(MIN, MAX)
2. Bottles of Beer on the Wall

残りボトルが無くなった時に歌詞が変わるのに最初気付かなかった。

class Song
  @@initial_bottle_num = 99
  @@lyrics = ["%s of beer on the wall",
              "%s of beer",
              "Take one down and pass it around",
              "%s of beer on the wall"]
  @@third_line_for_last = "There are no more to pass around"
  @remaining_bottle_num = 0
  
  def initialize
    @remaining_bottle_num = @@initial_bottle_num 
  end

  def sing_all
    sing
    sing_all if 0 <= @remaining_bottle_num
  end

  def sing
    num  = bottle_count(@remaining_bottle_num)
    remain = bottle_count(@remaining_bottle_num - 1)
    lyrics = @@lyrics
    lyrics[2] = @@third_line_for_last if @remaining_bottle_num <= 0
    printf(lyrics.join("\n") + "\n\n", num, num, remain)
    @remaining_bottle_num = @remaining_bottle_num - 1
  end

  private
  def bottle_count(num)
    case
    when num <= 0
      num = "No more bottles"
    when num == 1
      num = num.to_s + " Bottle" 
    else
      num = num.to_s + " Bottles" 
    end
  end
end

song = Song.new.sing_all
3. 石取りゲーム

長い!!考え過ぎだろう。ROBOT_SELFISH_RATEの率で、スタート時の石の数が先手が勝つ個数で、かつプレイヤーが先手を選んだ時に、ゴネて自分を先手にする。また、ROBOT_MISTAKE_RATEの率で、相手が勝つような残り個数の時に、ミスって負けに転じる数を取る。0にすると全くミスらない。

require "observer"

module GameLogic
  def take(stone_count)
    if win?(stone_count)
      #取る数毎に、勝つか負けるかを取得
      max_takable = [MAX_TAKABLE_STONE, stone_count].min
      wins_or_loses = 
        (1..max_takable).to_a.inject({:wins => [], :loses => []}) do |h, i|
          win?(stone_count - i) ? h[:wins] << i : h[:loses] << i
          h
        end
      if rand < ROBOT_MISTAKE_RATE
        #負ける方を選択
        wins_or_loses[:wins][rand(wins_or_loses[:wins].size)]
      else
        #勝つ方を選択
        wins_or_loses[:loses][rand(wins_or_loses[:loses].size)]
      end
    else
      take_random(stone_count)
    end
  end

  def win?(stone_count)
    stone_count % (MAX_TAKABLE_STONE + 1) != 1 
  end

  def robot_selfish?(stone_count)
    win?(stone_count) && rand <= ROBOT_SELFISH_RATE
  end

  def take_random(stone_count)
    rand([MAX_TAKABLE_STONE, stone_count].min) + 1
  end

  def takable?(num, stone_count)
    !num.nil? && 0 < num.to_i && num.to_i <= [MAX_TAKABLE_STONE, stone_count].min
  end

  def continue?(input)
    !input.nil? && (input == "y" || input == "Y")
  end
end

class State
  include GameLogic
  attr_accessor :result, :stone_count, 
    :players_turn, :player_last_took, :robot_last_took

  def initialize(state = nil)
    if !state.nil?
      @result =state.result
      @stone_count = state.stone_count
      @players_turn = state.players_turn
      @player_last_took = state.player_last_took
      @robot_last_took = state.robot_last_took
    end
  end
end

class InitialState < State
  def apply(input = nil)
    @stone_count = rand(MAX_STONE_COUNT - MIN_STONE_COUNT) + MIN_STONE_COUNT + 1
    @result = :greeting
    SelectTurnState.new(self)
  end
end

class SelectTurnState < State
  def apply(input = nil)
    @players_turn = input.to_i
    @players_turn = nil if @players_turn != 1 && @players_turn != 2
    if @players_turn.nil?
      @result = :select_turn
      self
    else
      selfish = robot_selfish?(@stone_count)
      if selfish && @players_turn == 1
        @players_turn = 2
        @result = :selfish_turn_determination
      else
        @result = :turn_determination
      end
      TurnDeterminationState.new(self)
    end
  end
end

class TurnDeterminationState < State
  def apply(input = nil)
    @player_last_took = nil
    @robot_last_took = nil
    if @players_turn == 2
      @robot_last_took = take(@stone_count)
      @stone_count = @stone_count - @robot_last_took
    end 
    @result = :take_stone
    PlayState.new(self)
  end
end

class PlayState < State
  def apply(input = nil)
    @player_last_took = nil
    @robot_last_took = nil
    if takable?(input, @stone_count)
      @player_last_took = input.to_i
      @stone_count = @stone_count - @player_last_took
      if @stone_count <= 0
        @result = :game_over_player_lost
        GameOverState.new(self)
      else
        @robot_last_took = take(@stone_count)
        @stone_count = @stone_count - @robot_last_took
        if @stone_count <= 0
          @result = :game_over_player_won        
          GameOverState.new(self)
        else
          @result = :take_stone
          self
        end
      end
    else
      @result = :take_stone_error
      self
    end
  end
end

class GameOverState < State
  def apply(input = nil)
    if continue?(input)
      @result = :restart
      InitialState.new(self)
    else
      @result = nil
      nil
    end
  end
end

class TextUi
  attr_accessor :game, :response

  def initialize(game)
    @game = game
    @game.add_observer(self)
    @response = TextResponse.new
  end
  
  def update(state)
    print @response.render(state)
    @game.update(gets.strip)
  end
end

class TextResponse
  def render(state)
    send(state.result.to_s, state) unless state.result.nil?
  end

  def greeting(state)
    "石の数は #{state.stone_count} 個です。\n" + 
      "先手/後手を選択してください。(1:先手/2:後手)>"
  end

  def select_turn(state)
    "先手/後手を選択してください。(1:先手/2:後手)>"
  end

  def turn_determination(state)
    sprintf "あなたは %s です。何か押すとスタートします。>", 
      state.players_turn == 1 ? "先手" : "後手"
  end

  def selfish_turn_determination(state)
    "ダメです。あなたは後手です。何か押すとスタートします。>"
  end

  def take_stone(state)
    stone_count(state) + takable_stones + ">"
  end

  def take_stone_error(state)
    remaining_stone_count(state) + takable_stones + ">"
  end

  def game_over_player_won(state)
    stone_count(state) + "\nあなたの勝ちです。もう一回やりますか?(y:もう一回/n:終了)>"
  end

  def game_over_player_lost(state)
    stone_count(state) + "\n私の勝ちです。もう一回やりますか?(y:もう一回/n:終了)>"
  end

  def restart(state)
    "何か押すと最初から始めます>" 
  end

  private
  def takable_stones
    sprintf "何個取りますか?(%s)", (1..MAX_TAKABLE_STONE).to_a.join("/")
  end

  def stone_count(state)
    res = player_took(state)
    robot_took = robot_took(state)
    res += res != "" && robot_took != "" ? " / " + robot_took : robot_took
    res += res != "" ? " 取りました。" : ""
    res += remaining_stone_count(state) 
  end

  def player_took(state)
    state.player_last_took.nil? ?
      "" :
      "あなた: #{state.player_last_took}" 
  end

  def robot_took(state)
    state.robot_last_took.nil? ?
      "" :
      "私: #{state.robot_last_took}"
  end

  def remaining_stone_count(state)
    "残り #{state.stone_count} 個です。"
  end
end

class Game
  include Observable
  attr_accessor :state

  def initialize
    @state = InitialState.new
  end

  def start
    update(nil)
  end

  def update(input)
    @state = @state.apply(input)
    if @state.nil?
      exit
    else
      changed
      notify_observers(@state)
    end
  end
end

MIN_STONE_COUNT = 10
MAX_STONE_COUNT = 100
MAX_TAKABLE_STONE = 3
ROBOT_SELFISH_RATE = 0.7
ROBOT_MISTAKE_RATE = 0.1

game = Game.new
ui = TextUi.new(game)
game.start

今他の人達のエントリ見たら、なんかみんな短いな。

全然日記を書いていない

  • 「ふつうのHaskellプログラミング」を一通り読み終わったところで、ここでHaskellは一旦置いといてRubyをちゃんとやるべし、と思った、ので、その前にLazyLinesを写経し、ある程度理解してから、出来ればRubyで書き直しでもして勉強しようかと考え写経していたが、ふつけるで説明されていない部分のIOモナド周りの読解でハマり、そのままにしている・・・。うーん、もう少しHaskellやっとこうか・・・。教材はやさしい Haskell 入門かな。大阪でHaskell勉強会というのをやっているらしいが、蛍ヶ池か・・・。遠いなあ。阪大の人達でやってるって事なのかな。
  • Flex/ActionScript3をやろうと思ったのに、不思議な事に何時の間にかExt JSAPIを読んでいた。軽く使ってみた感じ、よい感じではあるけど、やっぱりFlexの方が作り易そう。個人的にはAS3の方が書き易いし、FlexBuilderが強力でデバッグも楽なのが最高。来月FLEXIBLE RAILSやろうと思う。
  • 神戸にIKEAが出来たので奥さんと行ってきた。大したものは買わなかったけど、しかし異様に安いな。40円の皿とか。そういうのを結構買った。家具は総じてチープな感じではあるけども、デザインがまともなので良い。本棚を買うかどうか迷ったがとりあえずやめた。
  • 奥さんが英語学習用に『FRIENDS』見てたので、自分も見てみた。米国ドラマのあの独特なノリが受け付けられるか?と思ったが、意外とすんなり見れるな。これはいいかも。
  • 先日のS2Dao.PHP5のキャッシュの件で、その後ひっそりとS2Dao.PHP5のコミッタになった、のだが、なかなか時間が無くて全然触れてない。来週はなんとか・・・。と、書く事でやりそうな気がする。関係ないけど、trunkのS2Container.PHP5はnamespace使ってる!PHPが5.2以下なので動かない!
  • Symfonyの1.1が出たので、1.0系からの移行はどうかなと思い調べてみたが、互換性の無い変更が非常に多い!1.0で作ったものを移行するのは止めといた方が無難な気が。ヘルパーがクラスになってたり、良さそうなんだけどなあ。
  • Lucene/Nutch/SolrとかHyper Estraierで遊んでいる。Nutchは、何がどうとはっきり言えないがもう一つな気がする。クローラはHyper Estraierのやつの方が楽だな。全文検索Lucene+Solrが使い易い感じかも。Hyper Esraierも良いけど、Luceneの方がより柔軟な感じか。SolrでRubyからでもPHPからでも大体使える(最初Luceneをrjb使ってRubyから直接触ろうとしたけど、それは上手くいかなかった。ちなみにJavaを書く気は全く起きない。)。他にも色々あるけど、試してみたくなるようなのは少ない。Hyper EstraierLuceneもドキュメントがちゃんとしてるのが良い。Sennaは、リレーショナルデータベースを使うアプローチが個人的にあんまり・・・なので試してない。
  • 今日、LDRの未読が50000エントリを越えていた。スター1個以下のやつは基本放置になってきたな・・・。以前はたまに未読0にしないと気持ち悪かったけど、もう諦めた。

iKnow!のSkype campaign入賞

Skype campaign結果発表〜!-Yoshimi - iKnow!

iKnow!のSkype campaignのiKnow!賞およびDictation賞、両方とも5位入賞とのことで、Skypeの1時間無料通話クーポンが合計2時間分送られてきた。5位というのは上位1000人以内に入ったということらしい。この程度で入賞するとは、ちゃんとやってる人は結構少ないということか・・・。なんにせよラッキー。

第24回 Ruby/Rails勉強会@関西に参加

日本Rubyの会 公式Wiki - 第24回 Ruby/Rails勉強会@関西
http://jp.rubyist.net/?KansaiWorkshop24


行ってきました神戸。とりあえず、神戸大の正門付近から見える街と海の景色が素晴らしかった。早めに着いたのでプラプラ歩き回っていたのだ。晴れてて良かった。あのキャンパスは六甲山の麓というロケーションのみで最早最高ですね。思えば僕の母校周辺の景観は割と酷かったな(とても大昔)。設備はNeXTとかnews使い放題で最高だったけど(年がばれる)。完全に猫に小判で、Mathematicaで適当なグラフとか描いて遊んでた。そういえば今日の演習室はiMacだったな。スティーブ・ジョブズ恐るべし。

さて、ともかくは、またRuby初級者向けレッスン第18回 by okkezさんの演習の回答から。ブログに書くとokkezさんが添削して下さるということだ。素晴らしい。

  • 演習1 テキスト解析
    • まず、「単語」とは、\wな文字か、\wな文字でも空白類でも無い文字の連続したものとした。例えば、"(7:00:00 PM)"は"(", "7", ":", "00", ":", "00", "PM", ")"に区切られる。最初、"(7:00:00"を単語とするような区切り方をしていたのだが、気持ち悪いのでやめた。
    • 行数は空行もカウントする。
    • 文字は空白類もカウントする。
char_count = 0;
word_count = 0;
line_count = 0;
chars = Hash.new(0)
words = Hash.new(0)
File.open("text.txt") do |f|
  f.each do |l|
    char_count += l.size
    l.scan(/./m) {|c| chars[c] += 1 }
    l.strip.scan(/\w+|[^\s\w]+/) do |w| 
      words[w] += 1
      word_count += 1
    end
  end
  line_count = f.lineno
end

printf("文字数 %d\n", char_count)
printf("単語数 %d\n", word_count)
printf("行数 %d\n", line_count)
puts "文字出現頻度(文字 頻度(%) 回数)"
chars.sort_by {|c, n| -n }.each {|c, n| printf("%-10s %4.2f%% %5d\n", c.dump, 100 * n/char_count.to_f, n) }
puts "単語出現頻度(単語 頻度(%) 回数)"
words.sort_by {|w, n| -n }.each {|w, n| printf("%-10s %4.2f%% %5d\n", w, 100 * n/word_count.to_f, n) }
  • 出力結果
文字数 6096
単語数 1238
行数 43
文字出現頻度(文字 頻度(%) 回数)
" "        16.44%  1002
"t"        8.76%   534
"e"        8.40%   512
"o"        7.10%   433
"n"        5.51%   336
"i"        5.38%   328
...(省略)
 単語出現頻度(単語 頻度(%) 回数)
,          5.82%    72
.          5.09%    63
the        3.88%    48
and        2.34%    29
for        1.62%    20
a          1.62%    20
to         1.53%    19
in         1.45%    18
of         1.45%    18
it         1.45%    18
as         1.37%    17
you        1.21%    15
'          1.21%    15
Chkrootkit 1.13%    14
is         1.05%    13
that       0.89%    11
--         0.89%    11
...(以下省略)

ファイルをeachで回すと、linenoがインクリメントされていくということを知った。また、scanする時の区切りの正規表現にmオプションを付けないと、行末の改行にマッチせず結果が変わってきてしまう。あと今回ファイルが小さいし1行毎に処理する必要は無いのだが、readして処理するのはやめておいた。


  • 演習2 ログ解析
require "Time"

index_count = 0
first_access_from_mac = nil
google_access_count = 0
days = Hash.new(0)
agents = Hash.new(0)
File.open("access.log") do |f|
  f.each do |l|
    time, request, referer, agent = /[\d+.]+[^\[]+\[([^\]]*)\][^"]*"([^"]*)"[^"]*"([^"]*)"[^"]*"([^"]*)"/.match(l).to_a.values_at(1, 2, 3, 4)
    index_count += 1 if request =~ /\/index.php/
    first_access_from_mac = time if first_access_from_mac.nil? && agent =~ /Macintosh/
    google_access_count += 1 if referer =~ /http:\/\/[\w.]+google/
    days[Time.parse(/(\w+)\/(\w+)\/(\w+)/.match(time).to_a.values_at(1, 2, 3).join(" ")).to_s.gsub(/\s.+/, "")] += 1
    agents[agent] += 1
  end
end

printf("/index.phpへのアクセス回数 %d\n", index_count)
printf("最初のMacユーザのアクセス日時 %s\n", first_access_from_mac)
printf("Google経由のアクセス回数 %d\n", google_access_count)
printf("最もアクセスの多い曜日 %s\n", days.sort_by {|d, n| -n }[0][0])
puts "ブラウザ毎のアクセスランキング"
agents.sort_by {|a, n| -n }.each {|a, n| printf("%5d %s\n", n, a) }
  • 出力結果
/index.phpへのアクセス回数 2652
最初のMacユーザのアクセス日時 10/Mar/2008:12:17:22 +0900
Google経由のアクセス回数 74
最もアクセスの多い曜日 Mon
ブラウザ毎のアクセスランキング
 1212 Mozilla/5.0 (Twiceler-0.9 http://www.cuill.com/twiceler/robot.html)
  908 Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
  829 e-SocietyRobot(http://www.yama.info.waseda.ac.jp/~yamana/es/)
  672 Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
  379 Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727) Sleipnir/2.6.2
  231 Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 1.1.4322) Sleipnir/2.6.2
  159 Bloglines/3.1 (http://www.bloglines.com; 2 subscribers)
  155 Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)
  149 Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
  149 Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; InfoPath.1; Tablet PC 2.0)
  135 Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)
  131 Apache/2.2.3 (Debian) DAV/2 SVN/1.4.2 PHP/5.2.0-8+etch10 (internal dummy connection)
...(以下省略)

その他の感想などは、疲れたので明日続きを書くことにする。