memcached/APCとネームスペース、トランザクション
PHPでmemcacheやAPCを使ったキャッシングを色々試してみて、気付いた事をメモ。
memcacheはクエリで取得した値のキャッシュに使い、それ以外の色々な事にはAPCを使うのが良いような気がする。APCはmemcacheより10倍程高速なようだが(Cache Performance Comparison | MySQL Performance Blog)、httpd再起動で消えてしまう為、大量にキャッシュして使いたいデータベースのキャッシュにはちょっと(PHP等の設定をよく変更するのでhttpdは結構再起動する)。memcacheはやや遅いが、データベースの負荷を軽減するのに良い感じ。
以下は、キャッシュ使ってて気付いた、ネームスペースとトランザクション絡みの考察。
ネームスペース
memcachedやAPCは、ネームスペースのような機能をサポートしていない。あるオブジェクトをキャッシュしようと思えば、キーに紐付けて保存することになる訳だが、このままだと、例えば、あるテーブルに対するクエリの結果をキャッシュしようとした場合に、プライマリキーを利用してキーとし、クエリの結果オブジェクトをキャッシュに保存していく、といった使い方になる。ここで問題となるのが、複数のテーブルの行をキャッシュしていった場合に、あるテーブルに対応したキャッシュだけを削除したい場合にどうするのか?という点。キーを指定して保存/取得/削除する事しか出来なければ、複数のアプリケーションから同じmemcachedを使っていた場合等にも、キャッシュの特定の部分を纏めて削除する、といった事が出来ず使い勝手が悪い。
この問題を解決する為に、以下の方法で擬似的にネームスペースを実装してやることが出来る。
これにより、例えば、テーブル名に対応したネームスペース以下に、キーに対応したクエリ結果オブジェクトを保存してやり、削除時には、キーによる削除に加えて、ネームスペースを指定しての、ネームスペース以下の全削除も可能となる。
方法は、下記memcachedのFAQに載ってるそのまんま。
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の値を検索した場合、更新後の値を取得出来なければならないが、コミットまでの間、トランザクションの外からは古い値が見えないといけない。この為には、コミットまでの間、キャッシュには更新後の値も更新前の値も残っていないようにするか、もしくは、キャッシュには更新前の値を残すが、トランザクション内からは更新後の値が取得出来るように細工しなければいけない。後者はやや複雑になるのに比べ(キャッシュを扱うクラス内でバッファリングしておいてゴニョゴニョするとか)、前者は更新時点で影響を受けるキャッシュを一旦削除するだけで済み簡単である。キャッシュを削除するので、コミットまでの間、トランザクション内/外どちらからもデータベースへのアクセスが必要になってしまうが、これは仕方ないものとする。
整理すると、
- 更新系クエリ発行
- 影響を受けるキャッシュを削除する。
- 更新内容をバッファリングする。
- 参照系クエリ発行
- キャッシュが存在すればそれを使用する。
- さもなければ、データベースにアクセスして値を取得し、取得内容をバッファリングする。
- コミット後
- バッファリングした処理をまとめてキャッシュに書き出す。
先程の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 JSのAPIを読んでいた。軽く使ってみた感じ、よい感じではあるけど、やっぱりFlexの方が作り易そう。個人的にはAS3の方が書き易いし、FlexBuilderが強力でデバッグも楽なのが最高。来月FLEXIBLE RAILSやろうと思う。
- 日本Rubyの会 公式Wiki - 第25回 Ruby/Rails勉強会@関西に参加したので感想を書こうと思いつつ、また書いてない。角谷氏のプレゼン(スはスペックのス〜RSpec(関西弁))が素晴らしく、俄かに壁紙を『アジャイルプラクティス』の表紙(ピーテル・ブリューゲル-叛逆天使の墜落-)にする。ちなみにアジャイルプラクティスは2/3辺りまで読んだ所。角谷氏のライブコーディングを見て、やっぱEmacsだろう、と思いEmacsの設定を見直す。これまでMeadowからsamba上のファイルを開いてたけど、重いのでPuTTYから仮想マシンのEmacs使うように環境を整備。Macじゃない為Growlが無いので、Snarlをインストールした、が、基本的にVMWare上のLinuxでコード書くので意味無し。勉強会の初心者レッスンで出てきたrefe2(RubyリファレンスマニュアルをEmacsで参照・anything.elとの連携(改訂版) - ’(rubikitch wanna be (a . lisper)))をインストール。refe2x素晴らしい!ついでにanything.elが凄い。更に、EmacsならSKKだろう、と思いSKK(SKKIME)を試すものの、2日ほど使ってみてSKKIMEに色々不具合を感じたので中止(SKKのシステム自体は良いと思うが・・・、でもあまりこれに慣れてしまうと、本格的に自分以外のマシンでの操作が出来なくなりそうで怖い。既にキーバインドは完全にEmacs(窓使いの憂鬱使用)なので、自分以外のマシンではコピー/ペースト等の操作すら危しい。)。
- Emacsと言えば、その角谷氏のプレゼン見る少し前に、vimでも使ってみようと思いvim使いの弟に聞いて試してみたが、vimに付いてるチュートリアルやって、はてなのこれだけは知っておけ! vim 勉強会を見るところまでやって、そこまで。Ctrl使いまくりで小指を酷使するEmacsもどうかと思うが、vimのモード切替にはなかなか馴染めなさそうで・・・。慣れるとvimの方がエディットは楽そうだが。という訳で、結局自分はEmacs派だと思い直し、『入門 GNU Emacs 第3版』でEmacs再入門中。
- 神戸に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 EstraierもLuceneもドキュメントがちゃんとしてるのが良い。Sennaは、リレーショナルデータベースを使うアプローチが個人的にあんまり・・・なので試してない。
- 今日、LDRの未読が50000エントリを越えていた。スター1個以下のやつは基本放置になってきたな・・・。以前はたまに未読0にしないと気持ち悪かったけど、もう諦めた。
iKnow!のSkype campaign入賞
Skype campaign結果発表〜!-Yoshimi - iKnow!
iKnow!のSkype campaignのiKnow!賞およびDictation賞、両方とも5位入賞とのことで、Skypeの1時間無料通話クーポンが合計2時間分送られてきた。5位というのは上位1000人以内に入ったということらしい。この程度で入賞するとは、ちゃんとやってる人は結構少ないということか・・・。なんにせよラッキー。
[misc] ASAHIネットの固定IPアドレスが有料化
という通知が来た。しかも月額840円とは高いな。これを機会に乗換えも検討してみようかな。
第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) ...(以下省略)
その他の感想などは、疲れたので明日続きを書くことにする。