2012年5月31日木曜日

子育て日記:勉強が始まった。イヤイヤまっさかり。

ボーズ1号

  • ボーズ1号(約5歳)は相変わらず戦隊モノの日々だ。ゴーカイジャーからゴーバスターズに変わってすぐに落ち着くことを期待していたが、いまだに過去の戦隊モノのことのばかり考えている。
  • 昨年の夏頃からひらがなを書けていたが、文字のサイズも形もバラバラでほとんど読めたものではなかった。数カ月前から近所の塾に通わせ、毎日宿題をやるようになったら、かなりまともになってきた。要は、文字を構成する基本的な要素(縦棒、横棒、丸、曲がりなど)を書くことができていなかったということだ。手塚治虫もマルをうまく書けなくなった時に引退を考えたと言っていたはずだが、文字を書く前に線を書けなければダメだったのだ。
  • どうも数字に弱いように見える。言葉では1から100までと更に100以上も言えるのだが、3桁の数字は書けない。いまだ時計も読めない。論理的に思考することが、苦手なのかな。すこしずつ何とかしていこう。
  • しかし、私が小さい頃は幼稚園時代に文字も数字も習った覚えがない。全部小学校からだったように記憶している。下手に早く勉強をしてしまうと小学校に入った時にサボりぐせがついて、逆効果なんてことにならなければ良いが。ま、私も先生の話を聞くのは苦手(集中時間が極端に短い。興味があることに目移りする。)だったから、教科書を勝手に読んで勝手に理解しているようなことが多かったのでなんとかなるだろうが。

ボーズ2号

  • ボーズ2号はもうすぐ「魔の2歳」で反抗期まっさかりだ。とにかくお母さんへばりが強く、イヤイヤでよく泣くものだから嫁さんもいつも爆発している。
  • 言葉の方は順調に色々と覚えつつあるようで、お兄ちゃんの言葉をいつも真似ているし、「主語+目的語+述語」の形であれば少し希望を伝えられるようになった。助詞はまだ入らないが。日本語の発音は1号よりもうまい気がする。声が太いのもあるが、「し、ち、き」とか言えているように思う。(ただし、1号は2歳まで外国にいたので聴力が相当良いようだ。日本語にない発音をなんなくできたりする。)

2012年5月30日水曜日

東京奇譚集(村上春樹)を読んだ

2005年に刊行された村上春樹著「東京奇譚集」を読んだのでとりあえず感想文。

 5つの短編から構成され、共通することは以下のとおり。
  • 主人公の身の周りに起きた偶然や奇妙な出来事があり、それをきっかけに主人公が少し救われること
  • 必ずしも東京周辺を舞台としているわけではないが、主人公が東京居住者であること
 村上春樹としては何か完成形を作るための試行錯誤的な作品のように思われる。ただ、90年代以降テーマにしてきた物語による癒しというのが確実に形になってきている感触がよくわかり、現実世界を生きている読者としては心の癒しとまではいかないが、一種のリラックスした気分になれたのは事実。
 作者は「アンダーグラウンド」で現代の日本人はジャンクをかき集めて作った物語によって心のバランスを保つしかないのではないかといっていたのが、最近読んだ「海辺のカフカ」と同様に今作品もその延長上にあることが確認できた。今後の作品に更に期待していきたい。(1Q84をもう一度読みなおそうかな。。)
 また、作者が意図しているかどうか不明だが、短篇集の中に具体的な名前(人の名前、地名、曲名、などなど)や事象がたくさん書かれているので、読んでいる側は物語のどこかに自分とのなんらかの共通項(登場人物の名前や地名、年齢など色々な部分で)を見つけられる可能性が高い。そのため読者が(若干の)感情移入がしやすいという効果があるのではないだろか。著名に「東京」を付すことで想定読者を限定しているので余計に著者がそのような効果をねらったと考えるのは私の考え過ぎか。
 最後に「品川猿」が最も印象深く感じた。主人公は女性であるが、色々な点で不思議なほど私の個人的経験や最近身の回りで起きたことと符合しており、ストーリーはまさにジャンクなのだがなぜかストーリーに引きこまれ、適切なエンディングもあり、読んでいる間は現実世界と小説の世界がつながっているかのような感覚でした。

Powershellクロージャあり〼。その2

PowerShell覚え書き


お題

前回のブログでPowerShellのクロージャ機能を使ってみました。せっかくなので、バリエーションとして一つの環境(クロージャ)を複数の関数から共有するようなサンプルを作りました。(Paul GrahamのANSI Common Lispからのパクリです。)前回と同じくアキュムレータなのですが、リセット機能ありのバージョンです。
function Accum()
{
$i=0
$closure={
        $obj = New-Object PSObject
        
        $fn = { $script:i = 0 }
        Add-Member -inputobject $obj -membertype scriptmethod -name reset -value $fn

        $fn = {$script:i++ ;$i }
        Add-Member -inputobject $obj -membertype scriptmethod -name stamp -value $fn

        return $obj
    }.GetNewClosure()
    return &$closure
}

$a=Accum #疑似コンストラクタ

"stamp => "+$a.stamp()
"stamp => "+$a.stamp()
"stamp => "+$a.stamp()
"reset "+$a.reset()
"stamp => "+$a.stamp()
"stamp => "+$a.stamp()


実行結果は以下の通り。
    stamp => 1
    stamp => 2
    stamp => 3
    reset
    stamp => 1
    stamp => 2


メモ

  • 着想はGetNewClosure()はブロックの持つメソッドなので、ブロック内でオブジェクトを作って複数のメソッドを用意すれば環境を複数のインターフェース間で共有できるということです。
  • 関数Accum内でクロージャ定義だけでなく、&でのクロージャ呼び出しを返しているので、利用者側には一種のオブジェクトのように見え、メソッドの呼び出しもスムーズです。
  • クロージャを使うことでカプセル化ができたので、オブジェクト指向的な使い方ができますね。(誰かのブログでclass定義用のライブラリを作っている人がいましたが、クロージャを使ってたのかな。。)
  • ブロック内の変数のスコープの書き方が面倒な点は相変わらずです。なにかよい方法はないのか。


クロージャで面白いことがまだできそうです。どこかでもう少し他のサンプルを作りたいと考えています。

2012年5月29日火曜日

Powershellクロージャあり〼。その1

PowerShell覚え書き


お題

PowerShell2.0からクロージャ機能があるとのことで、アキュムレータを作ってみました。

function Accum([int]$init)
{
    $i=$init
    { $script:i++; $i}.GetNewClosure()
#    return { $script:i++; return $i}.GetNewClosure() #ちゃんとreturnをつけるとこうなる。
}

"`$i=" + $i #スクリプトスコープ

"Get Accumlator init=100"
$a=Accum 100

"call by `&"
1..3|%{ &$a }

"`$i=" + $i #スクリプトスコープ

"set `$i = 1000"
$i=1000
"call by `&"
1..3|%{ &$a }

"call by `."
1..3|%{ . $a }

"`$i=" + $i #スクリプトスコープ


実行結果は以下の通り。
    $i=
    Get Accumlator init=100
    call by &
    101
    102
    103
    $i=
    set $i = 1000
    call by &
    104
    105
    106
    call by .
    107
    108
    109
    $i=1000


メモ

  • クロージャ作成方法はご覧のとおりスクリプトブロックのGetNewClosure()メソッドの呼び出しです。
  • クロージャのブロック内で「$script:i++」とスコープを指定しているのは親ブロック側の変数を変更してやるためです。ただし、本当は親ブロックの変数なのでスクリプトスコープにする必要はないのですが、Set-Variableでやるのも面倒でこうしました。実行結果の通りスクリプト内で$iに値を設定しても影響を受けないので、クロージャ内の環境に保持されているはずです。
  • 「$a=Accum 100」で変数$aにクロージャを代入(変数を束縛?)して、その後そのクロージャを呼び出していますが、PowerShellの場合にその呼び出し方は2種類あるようです。
    1. 「&$a」 ヘルプのabout_functionsにはこちらが書かれていて、ネット上もそのようにやっている例が散見されました。一般的にはこちらを使用することが多いようです。(追記:&はローカルコンピュータ上のInvoke-Commandと同義で、スクリプト ブロックの文字列をコマンドとして評価または実行する)
    2. 「. $a」 いわゆるソース呼び出しなのですが、こちらも確かにクロージャとして動作しています。また、こちらの呼び出し方の場合にはクロージャのスコープが親スコープと同一となるようで上述したscriptスコープの指定がなくても動くみたいです。ソース呼び出しの場合にはスコープが変わるというのは言われてみればそうかもしれません。でも、クロージャのスコープ自体は不思議なものですね。


とりあえずクロージャが動くようなので、どこかでもう少し別のサンプルを作りたいと考えています。

関連記事

クロージャあり〼その2

加筆


  • クロージャという言葉を調べてみると、無名関数のことをクロージャと呼んだりしている場合もあり、上記の説明では「環境を含んだ関数」という意味で記述しているので厳密な意味についてはご勘弁を。
  • マーチン・ファウラーは無名関数にせよ環境込みの関数にせよ「意識しなくてもいいぐらいに手軽にクロージャを使えること」が大事だと書いていた。そういう意味ではPowerShellのクロージャは合格点がもらえるレベルかどうか疑問です。今のところ。
  • 2012年5月28日月曜日

    Powershellでアクセスログ統計作成(MSMQとジョブ版)その1

    Awk好き技術者の書いたPowerShell

    背景

     以前のブログでWEBサーバのアクセスログ統計を取るスクリプトを作成しましたが、処理時間の問題がありました。サーバログを1日単位等でバッチ的に処理をするのは時間的に厳しく、PowerShellが適用できない場合がありそうということです。今回はその続きとして発想を変えてみたいと思います。(なにもそこまでPowerShellに肩入れすることはないのですが、乗りかかった船なのでちょっとだけ。。)
    • バッチ処理ではなく、常駐型にする。(特に重い部分だけでも随時処理しておけば使用に耐えうる。)
    • 最近はWebサーバが複数台構成になることが多いので、WEBサーバでできることはそれぞれのサーバで処理する。
    • 最終的な集計結果をまとめる処理は別のマシンで処理する。
    • サーバ間のデータの連携はWindowsServerに標準搭載されるMicrosoftMessageQueuing(MSMQ)を使用する。
     ま、簡単に言うと個人的にMSMQとPowerShellのジョブの機能を使ってみたかっただけです。
     いきなり全部やろうとすると、うまくいかないので段階的に進めていきます。

    お題(ステップ1)

     シングルサーバ構成で良いので、MSMQを使ってプログラム間でログ的なオブジェクトを引き渡すようなプログラムを作成します。今回はログデータではなく単純なCSV形式のデータとします。基本的な流れは以下のとおり。
    1. MSMQ作成
    2. MSMQへのメッセージ送信
    3. MSMQからのメッセージ受信
    4. MSMQ削除
    [void][Reflection.Assembly]::LoadWithPartialName("System.Messaging")
    
    $quename=".\private$\SampleQueue"
    $musicians=@"
    name,part,type
    Joao Girlberto,Guitar,Genius
    U-Zhaan,Tabla,Well-Trained,
    Jeff Beck,Guitar,Genius
    吾妻光良,Guitar,Well-Trained
    Kassin,Bass,Talented
    Harry Hosono,Bass,No Type
    "@
    
    "==================================================================="
    if ([System.Messaging.MessageQueue]::Exists($quename)){
      "delete old queue"
      [System.Messaging.MessageQueue]::Delete($quename)
    }
    
    "create a new queue"
    $q = [System.Messaging.MessageQueue]::Create($quename)
    
    $q.SetPermissions("everyone", 
        [System.Messaging.MessageQueueAccessRights]::FullControl,            
        [System.Messaging.AccessControlEntryType]::Set)
    
    "==================================================================="
    $q2 = New-Object System.Messaging.MessageQueue $quename
    
    $msg = New-Object System.Messaging.Message "SampleMessage"
    $msg.label = "Sample Message"
    
    "Send Messages to MSMQ"
    $musicians|
        convertFrom-csv|
        % { convertTo-xml $_ }|
        % {
            $msg.body=$_
            "SEND: " + $msg.body.objects.object.property[0].innerText
            $q2.send($msg)
        }
    
    "==================================================================="
    function ConvertFrom-Xml($xml) {
        foreach ($object in @($xml.objects.object)) {
            $PSObject = New-Object PSObject
            foreach ($property in @($object.property)) {
                $PSObject |
                Add-Member NoteProperty $property.name $property.innerText
            }
            $PSObject
        }
    }
    
    "Receive Messages from MSMQ"
    $q3 = New-Object System.Messaging.MessageQueue $quename
    $ts = New-Object TimeSpan 3e7    # wait timer 3 seconds
    
    1..$q3.GetAllMessages().length |
    %{
        $rmsg = $q3.receive($ts)
        
        #[System.Xml.XmlDocument]$xml=$rmsg.body    
        $rmsg.BodyStream.Position = 0
        $sr = New-Object System.IO.StreamReader( $rmsg.BodyStream )
        [System.Xml.XmlDocument]$xml=$sr.ReadToEnd()
    
        ConvertFrom-Xml $xml
    }|
    ? {( $_.part -eq "guitar" )}|
    Select name
    
    "==================================================================="
    "Delete Queue"
    [System.Messaging.MessageQueue]::Delete($quename)
    
    

    ちょっとだけ解説

    • MSMQはOSのデフォルト(今回はWindowsServer2008)ではインストールされないので、機能の追加が必要ですのでご注意ください。
    • キューの操作はSystem.Messaging.MessageQueueの該当メソッドを使用するだけなので比較的簡単です。
    • 入力データであるCSV形式のヒアドキュメントをimport-csvで読み込んで、XMLに変換してキューに登録しています。オブジェクトをそのままシリアライズするような方法が見当たらなかったので。でも、こうしておけばサーバマネージャから直接キューの内容が見れるのでかえって良かったです。(JSONでもできそう。)
    • オブジェクトをXMLに変換するコマンドレットConvertTo-XMLはありましたが、受信時に必要となる逆の機能が見つからなかったので簡単な関数ConvertFrom-XMLを作りました。
    • すべてのメッセージを受信したあと、パイプラインでWhereやSelectをやってみました。具体的には、ヒアドキュメントで定義されたミュージシャンのうちギターに該当する4名の名前が表示されます。


    感想

    • MSMQの操作はあっけないぐらい簡単でした。ここれはスタンドアロン環境でやっていますが、複数サーバ間でもおそらく難しくないでしょう。
    • 一番苦労したのが、MSMQからのメッセージの取り出しの部分です。メッセージのボディ($rmsg.body)が$nullになってしまい、MessageQueue.FormatterとしてXMLやBinaryなどでやってもNGでした。けっきょく代替手段としてBodyStreamを使って読み込んでいます。何か良い方法はないのでしょうかね。
    • 今回は簡単なデータだったのでパフォーマンスのことは考えていません。
    • まずはCSVデータについて動いたので、次回はIISアクセスログで同様のことをチャレンジします。ただし、いつになるか不明ですが。

    2012年5月22日火曜日

    PowerShellでtail -fのようなことをやってみた

    PowerShell覚え書き

    お題

    PowerShellでUnix上のtail -fみたいなことができるかやってみる。(目的はログファイルの垂れ流し)
    param( [Parameter(Mandatory=$true)][String]$filename)
    
    $lines = (gc $filename).length
    gc $filename -wait |
        % -begin {$i=0} {if( $i -ge ($lines - 10)) {$_} ; $i++}
    
    

    解説

  • とりあえず最後の10行だけ表示して、そのあとGet-Contentの-waitパラメータでパイプラインを開きっぱなしにしました。
  • パイプライン処理のところでAWKの組み込み変数NRに相当するものがないので、変数でカウントしました。(この機能が本当にないのなら将来追加されることに期待。)

    結果

    動かしてみたところ、うまくいった!と思ったら、ファイルの最後の行が改行文字で終わっていないと無駄に空白行がはいってしまうことが発覚しました。(最後の標準出力の部分の問題かと思い、pipelineでOut-GridViewとかやってみてもダメでした。)
    Get-Contentがパイプラインで次に渡す際に自動的に改行文字を入れているように見えるのですが、そんな仕様なのか-waitのバグか微妙なところです。
    とりあえず、ログファイルの垂れ流しには十分なので、これでおしまい。
  • 2012年5月20日日曜日

    過去形ってなんや?

    自然言語のド素人が考えてみた

    背景

    shiroさんのブログを読んで、何年か前に中国語を学んだ時に引っかかっていた日本語での過去形とは何を意味するのか改めて考えてみました。自然言語については体系だった学術研究はされているでしょうが、私は完全に門外漢なので素人なりに考えてみたことを書きとめておきます。

    過去形に関する個人的な疑問点

    中国語の基礎で「了(le)」という表現を学んだ際に以下のような2通りの使われ方があると教科書にありました。(詳しくは、動詞の直後の場合と文末の場合で異なったり、意外性のニュアンスがあったりしますが、詳細は忘れました…というかサバイバルのための勉強だったので細かく覚えているはずがない。)

  • 状態の変化を意味する。
  • 動作の完了を意味する。
  • 中国語学習者にとって後者はいわゆる過去形なのでわかりやすいのですが、前者は多少わかりにくいニュアンスを含んでいます。例えば、「下雨了」というと「さっきまで降ってなかったのに今雨が降ってきた」という状態の変化を意味するという感じです。
    実際にいくつかの前者の例文があったのですが、不思議なことにそれらの例文を日本語に訳すといわゆる過去形になってしまうものがかなりあることに気が付きました。例えば日本語に訳すとこんな例文です。
    「明日の朝、雨が降ったらタクシーで病院に行こう。」
    「結局あの服は買わないことにした。」
    「トイレットペーパーがなくなった。」
    確かにこれらの場合は現在や未来のことを日本語でも「過去形」で表現しています。はて、どうしてだろうと思い始めたのがかれこれ8年前です。

    「よろしかったでしょうか?」に対する世間の批判

    自然言語は現実の世界で使用されている限り変化し続けるものなのですが、共通に使われる言語であるが故に急激に使用頻度が増えてしまった表現については批判的な意見が出てくるのが面白いところです。「日本語よ、お前も苦労してるな」と言ってあげたいぐらいに集団的アレルギー反応を起こすこともあります。(コンピュータ言語でもはやりのフレーズとかありそうですね。)
    数年前にコンビニ店員等によく使われるようになり、その後世の中の批判を浴び少しずつ頻度が減ってきている感がある「よろしかったでしょうか?」(例:お箸、一膳で〜。)という表現があります。批判された理由はこんな感じです。

  • 終わっていないことを過去形で言うのは気持ち悪い。
  • 謙遜したつもりかもしれないが、逆に他人に責任を押し付けているようで妙にイラツイてしまう。
  • 昔から一部では使われていた表現なのですが、確かにアルバイト社員が取ってつけたかのように言うと悪い印象を与えるようです。アルバイト社員が定形文句として言っているか、正社員がニュアンスを込めて言っているのかは言葉を聞く立場の人にはわからないという部分がそもそもの誤解のような気もします。

    仮説

    仮にですが、日本語の過去形表現も中国語と同じく、元来「状態の変化」を含んでいると考えても良いように思います。そうすれば、中国語の「了」と日本語の関係も素直に理解できるし、コンビニのでの「よろしかったでしょうか」も「何も購入していない → 購入する(お金を払うことで商品の所有権を得る)」の状態変化として理解できます。上記の幾つかではこの仮説が問題なさそうに思います。また、そのニュアンスを加えることによって話す側には「(状態変化を理解していることを相手に示すことで)丁寧に話している」意味を追加しているというのが最近のトレンドとなりつつあると。とりあえず、現時点ではそう仮定しておき今後また思い出したら検証したいと思います。

    -------

    追記

    日本語の過去形でよく使う例を思いついたので追加。「この中に☓☓な人がいます。さて、どの人でしょうか?」のようなクイズで答えを言う時に「答えはAさんでした!!」というように過去形で言うのは馴染みがあります。これは、答えが不明な状態から答えが確定した状態への変化と解釈できますね。やっぱり過去形って過去と言うよりも状態遷移を表すのかなあ。

    2012年5月19日土曜日

    Powershellで青空文庫から欲しい作品を一括取得する

    Awk好き技術者の書いたPowerShell

    背景

     最近は少し大人になったのか日本の古い小説を読みたいなと思うことがあります。中学高校時代にはもちろんある程度は読んでいたのですが、 元来国語が苦手だったので以降一切読んでませんでした。
     ということで、青空文庫からある作者の作品をダウンロードするスクリプトを書いてみました。
    param( [Parameter(Mandatory=$true)][String]$author)
    $aozoraListExt_SJIS_URI= "http://www.aozora.gr.jp/index_pages/list_person_all_extended.zip"
    
    function Unzip-Files([string]$zip,[string]$dest)
    {
        $shellApplication = new-object -com shell.application 
        $zipPackage = $shellApplication.NameSpace($zip) 
        $destinationFolder = $shellApplication.NameSpace($dest) 
    
        # CopyHere vOptions Flag # 4 - Do not display a progress dialog box. 
        # 16 - Respond with "Yes to All" for any dialog box that is displayed. 
        $destinationFolder.CopyHere($zipPackage.Items(),20) #zipファイルを指定のフォルダに解凍
    }
    
    function Download-AozoraFile([string]$uri)
    {
        $zipfile = [System.IO.Path]::GetTempFileName() |
            Rename-Item -NewName { $_ -replace 'tmp$', 'zip' } –PassThru
            #拡張子がzipでないとshell.applicationでエラーになるため拡張子変更
        $unzipDestination = $zipfile -replace '.zip',''
        mkdir $unzipDestination >$null
    
        (New-Object System.Net.WebClient).DownloadFile($uri,$zipfile)
        Unzip-Files $zipfile $unzipDestination
        gci $unzipDestination  |
            %{ cat $_.PSPath  -encoding String } #SJISからUTF8に変換
    
        rm -r $zipfile,$unzipDestination >$null
    }
    
    mkdir $author >$null 2>&1 #作者名でフォルダ作成
    "#全作品リスト取得中"
    Download-AozoraFile $aozoraListExt_SJIS_URI|
        convertFrom-csv | #作品リストの読み込み
        ? {($_.姓+$_.名 -eq $author) -and ($_.テキストファイルURL -match "^http.*\.zip$")} |
        % {
            $outputFile=$author + "\" + $_.作品名 + ".txt"
            Download-AozoraFile $_.テキストファイルURL > $outputFile
            $outputFile
        }
    

    解説

    • 青空文庫のサイトには全作品に関するcsvがダウンロードできるので、そのファイルから作者名で作品を絞込み、該当するテキスト版の作品ファイルをそれぞれダウンロードしています。
    • Powershellにはzip/unzipの機能がないために、Windowsの機能を使用しました。なんでないんでしょうね。
    • PowershellのconvertFrom-csvやimport-csvの制約として、文字コードがUTF8前提となっているのでSJISからコード変換しています。
    • 一時ファイルは[System.IO.Path]::GetTempFileName()で作成できましたが、一時ディレクトリは自分で名前を作りました。なにかよい方法がないかな。
    • 書き捨てのスクリプトなので異常系など考えていませんので悪しからず。


    感想

     苦戦したものの、メインのところはなんとか頑張ってパイプラインで処理するようにできました。(作品リスト取得以降は絞り込みやデータのダウンロードなどすべて並列に処理するので結構早いです。
     今回は作者名で作品のtxt版を一括ダウンロードしましたが、青空文庫の作品リストには作品や著者のカナや英名、底本に関する情報、入力者や公開日や作者の生年没年なども記載されています(もちろんXHTML版のURIもあり)ので、色々な使い方があると思います。
     あとOut-GridViewコマンドレット(PS3では拡張されているらしい)を使って作品一覧を表示したり、Write-Progressコマンドレットでダウンロード状況を表示したりできますが、いまひとつパッとした使い方が思いつかなかったので止めておきました。

    2012年5月16日水曜日

    Powershellのパイプライン処理の時間計測

    Awk好き技術者の書いたPowerShell

    背景

     先日、WEBサーバのアクセスログの統計解析のスクリプトを書いてみて、書き心地はなかなか悪くなかったのですがパフォーマンスがかなり問題になりました。
    とりあえず、続きにパイプライン処理の性能をざっくり把握したいと考えてちょっとやってみました。

    お題

     文字列を含んだファイルを開いて、パイプラインで渡す時間を計測する。用意するファイルは100万文字☓1行〜1文字☓100万行の数パターンとする。
    $maxSize= 1e6
    $file=".\test_" + $pid + ".txt"
    
    function test-performance([int]$length,[int]$repeat)
    {
        1..$repeat | %{ "Z" * $length } > $file
        $totalSec = (Measure-Command { cat $file | %{ $_ } > $null}).TotalSeconds
        "{0:###,###}文字×{1:###,###}行:{2:##0.00}秒 -> {3:##0.000}ミリ秒/行" -f $length,$repeat,$totalSec,($totalSec /$repeat * 1000)
        rm $file
    }
    
    for ($i = $maxSize ; $i -ge 1 ; $i /= 10){
        test-performance $i ($maxSize / $i)
    }
    

    解説

     評価の対象は { cat $file | %{ $_ } > $null}の処理時間。

    結果

    • 1,000,000文字×1行:0.01秒 -> 14.701ミリ秒/行
    • 100,000文字×10行:0.02秒 -> 1.731ミリ秒/行
    • 10,000文字×100行:0.03秒 -> 0.306ミリ秒/行
    • 1,000文字×1,000行:0.14秒 -> 0.142ミリ秒/行
    • 100文字×10,000行:1.17秒 -> 0.117ミリ秒/行
    • 10文字×100,000行:11.57秒 -> 0.116ミリ秒/行
    • 1文字×1,000,000行:112.65秒 -> 0.113ミリ秒/行
    • ※環境:Xeon E5506@2.13GHz ☓2プロセッサ、メモリ2GB、WindowsServer2008 SP1 (32bit)

    やってみてわかったこと

  • やはり1万件を超えるようなデータの読み込み&パイプライン処理については慎重に考えるべき。
  • (書き捨てのスクリプトのつもりが、遅くてチューニングに時間がかかってしまうなんてことになるかも。)
  • 上記計測の趣旨とは異なりますが、件数の多いテストファイルの作成には読み込み以上に時間がかかった。大量にデータを書き出す処理は更に要注意。
  • 2012年5月14日月曜日

    PowershellでWEBサーバのアクセスログ統計1

    Awk好き技術者の書いたPowerShell

    背景

     Windows環境で開発をしていると「こんなのUnix環境だったらスクリプトを書いてすぐなのになぁ。。」ってなことが時々あります。MS系OSもバッチ,WSH,PowerShellと変わってきて、PowerShell3.0では性能も相当改善されるので今後はいわゆるスクリプト言語として適用される場面も増えそうな気配がします。(ほんとうにyet another script languageですね!!)
     私はこれまでPowerShellをバッチファイルの代わりにしか使用していませんでしたが、今回はもっとUnix的な考え方で使ってみることにしました。
    たまたま最近IISのアクセスログ統計を作る機会があったのですが、その際はcygwin+bash+gawk+sedで終わらせてしまいました。今回はその雛形部分をPowerShell2.0で作ってみました。

    お題

  • IISのアクセスログからPV数をカウントする。画像ファイルやHTTPの戻りステータスが200だけを対象とする。結果は日本標準時の日付別で表示する。
  • bashとawkでいつもやっているパイプライン処理はなるべく変えない。
  • パフォーマンスはあまり期待しない。でも極力優先する。

  • $yesterday=(Get-Date).addDays(-1).ToString("yyMMdd") 
    $yesterdayMonth=(Get-Date).addDays(-1).ToString("yyMM") 
    $inputFiles=".\input\u_ex"+$yesterday+".log"  #IISのログファイルを指定する(ここでは昨日データ)
    
    #入力するアクセスログファイルのカラム名を配列として定義する。
    $fields=
    -split ("DATE TIME SV_NAME SV_IP METHOD URL_STEM URL_QUERY PORT USERNAME CL_IP U_AGENT HOST " + 
    "STATUS SUB_STATUS WIN32_STATUS TIME_TAKEN" )
    
    #カラム名の配列の逆引き用に連想配列を定義する。("SV_IP"→配列番号3みたいに。)
    $fieldIndex = $fields |% -begin{$i=0;$h=@{}} {$h.$_=$i++} -end{$h}
    
    #日時分秒を日本標準時に変換 
    function ConvertTo-JST
    {
        Process{
            $col=$_.split()
            $newDateTime=-split (Get-Date ($col[$fieldIndex.DATE] + " " + $col[$fieldIndex.TIME])).addHours(9).ToString('yyyy/MM/dd HH:mm:ss')
            $col[$fieldIndex.DATE]=$newDateTime[0]
            $col[$fieldIndex.TIME]=$newDateTime[1]
            $col -join " "
        }
    }
    
    #アクセスログからパラメータ指定されたカラムのみを選択し空白区切りの文字列として返す
    function Select-Columns
    {
        Begin{
            for ($i=0; $i -lt $args.length; $i++){
                if ( $fieldIndex[$args[$i]] -eq $null ){
                     #ここでパラメータチェックする(未実装)異常終了ってどうするの?
                }
            }
        }
        Process{
            $cols=$_.split();$new_cols=@()
            for ($i=0; $i -lt $args.length; $i++){$new_cols += $cols[$fieldIndex[$args[$i]]]}
            $new_cols -join " "
        }
    }
    
    #同一文字列でカウントし、結果を新しいカラムとして追加
    function Add-Counter
    {
        Begin{
            $count=@{}
        }
        Process{
            $count[$_] ++
        }
        End{
            foreach ( $key in $count.Keys){
                $key + " " + $count[$key]
            }
        }
    }
    
    #日別PV数レポート作成
    function Out-Report1()
    {
        $outputFile=".\report\SAMPLE_REP1_DAILY_PV_" + $yesterdayMonth + ".txt"
        (Get-Date).ToString() + " " + $outputFile
        
        #cat $inputFiles -ReadCount 1000|% {$_}| #1回あたりの読み込み件数を増やすと約10%早くなった
        cat $inputFiles |
            ? {$_.split().length -eq $fields.length}| #カラム数が合わない行は取り除く
            ? {$_.split()[$fieldIndex.URL_STEM] -notmatch ".*\.(gif|jpg|png|css|js)$"}| #抽出条件:画像ファイルやcss,js以外
            ? {$_.split()[$fieldIndex.STATUS] -match "^200$"}| #抽出条件:STATUS OKだけ
            ConvertTo-JST| #日時分秒を日本標準時に変換 
            Select-Columns "HOST" "PORT" "DATE" | #レポート対象項目の選択
            Add-Counter| #同一行で集計しカウンター(COUNT)を追加
            sort >>$outputFile
    }
    
    (Get-Date).ToString() + " Start"
    Out-Report1 #日別PV総数
    (Get-Date).ToString() + " Finished!"
    

    ちょっとだけ解説

  • ログファイルのフォーマット定義は最初の部分でグローバル変数$fieldsと$fieldIndexでやっている。
  • 統計のメイン処理は関数Out-Report1でパイプ処理でやっている。

  • やってみてわかったこと

    【良かったこと】
  • 若干表記の違いはあるけど、shellスクリプトの気持ちで書けた。PowerShellの流儀としてはログフォーマット定義はグローバル変数でなく、オブジェクトのプロパティ(Set-MemberでNotePropertyでやる)の方が自然なんでしょうが、やってみたらとんでもなく遅くなり結果的にベタな文字列処理になったのが少し不満なところです。でも、連想配列、配列、Begin、End、パターンマッチングなどはが自然にあり、楽しかったです。はい。
  • 枠組みとしてはできたので、ちょっと直せば以下の様な色々なパターンの統計もできますね。(実際にいくつかやりましたが、あまりこらなければどれも簡単)
    1. PV数の多いページのURLとそのアクセス回数
    2. 時間帯別や曜日別のPV数
    3. HTTTPStatusが400番台だったページの多かったものを表示(リンク修正漏れチェックなどで使用)
    4. 一日あたりのユニークユーザ数(クライアントIPやUSERNAMEから判断)
    5. ページ別のレスポンス時間(平均、最大、最小、標準偏差)
    6. PV数が多いブラウザの種類とバージョン(UserAgentで判断)
    7. PV数が多いOSの種類とバージョン(UserAgentで判断)
    【改善すべき点】
  • レスポンス性能がとても悪い。
    • 環境としては2年ほど前に購入したXeon E5506@2.13GHz ☓2コア、メモリ2GB、WindowsServer2008 SP1 (32bit)でやりましたが、結果は以下のとおり。
    • 入力ファイルサイズ150MB(55万件) → 処理にかかった時間3分40秒 ※Cygwin+bash+gawkでやったら10倍以上早かった。
    • 根本的原因はどうもGet-Content(=cat)の処理が遅いことパイプライン処理でのオブジェクトの引渡しが遅いことと文字列をsplitして判断していることのようで改善は難しいのですが、統計処理の初期データを作成するところを別にしてやり、不要データの削除(および、ブラウザやOSの種別の判断など)を事前にやっておけば、数十秒の応答時間になりそうです。事前処理はGet-Contentの-waitパラメータを使って、バックグラウンドジョブとしてリアルタイムに事前処理をやることもできます。(これはこれでおもしろそうですね。ジョブは他のマシンで動作させることもできるので複数台のWEBサーバで同時に動かすことも可能です。)
    • Windows8に搭載される予定のPowerShell3.0ではパフォーマンスは格段に良くなっているのでそちらにも期待しましょう。
  • グローバル変数の扱いが悩ましい。
    • ログファイルのフォーマットをグローバルスクリプトスコープの変数に格納しましたが、実際の統計処理のパイプラインの流れの中では表示項目を絞ったり、カウンタを追加したりして、フォーマットは変化します。Set-Memberでやればオブジェクトの属性としてもたせられるのですが、パフォーマンスが悪いのでそれも難しい。なにか他にいいやり方がないか思考中です。
     ま、個人的にいうと数万件以上のデータに対する逐次処理である程度の処理性能(数秒とか数分とか)を求めるのであれば、PowerShell2を何も考えずに使うのは今のところ少し厳しいかなという感じです。ただし、せっかく面白い機能が色々とあり、PS3での性能改善のこともあるので余裕があったら今のうちに色々と遊んでおくのは良いかなと思います。パイプライン処理は40年?も前から実装されている機能ですが、マルチコア環境の分散処理に向いていたりして今後もまだ有望な処理方式です。将来はパイプライン処理を使ってオンライン業務を構築する日なんかも来るかもしれませんしね。

    関連ページ

     パイプライン処理の時間計測で単純な文字列のパイプライン処理時間計測をやりましたので参考まで。
     パイプライン処理の並列性でパイプライン処理が並列に処理されていない(ことがある)ことを書きました。

    2012年5月13日日曜日

    ドナルド・ダック・ダン死去

    本日R&Bベーシストのドナルド・ダック・ダンが亡くなりました。享年70。しかも東京で。昨日までブルーノートで演奏していたとのことですが、急な知らせで驚きました。オー、ジーザス。
    私は高校〜大学時代に60年代から70年代のソウルやR&Bをよく聞いていました。その中でもスタックスレーベルといえば老舗中の老舗で、ダックダンのいたブッカーT&MG'Sをバックに歌うオーティスレディングは大好きでした。黒人のR&Bを世界に知らしめた彼らの功績は本当に偉大です。また、管楽器やスティーブ・クロッパーのギターと一緒に弾く彼のぶっといフェンダープレシジョンベース(弦を張り替えたことがないといいう逸話がありましたね。)の音は、最高にかっこ良いソウル・ミュージックの演出家だったと思います。ぶっとい音楽は永遠に不滅です。
    合掌。

    2012年5月10日木曜日

    Why I sing the blues

    40歳代中頃にもなってナゼに?

    こんにちは。tesです。
    30年前、私が中学生のころシャープのポケットコンピュータで初めてBASICのプログラムを書いて動いた時って本当にワクワクしました。 高校生になり、兄のエレキギターを勝手に借りてCCRのプラウド・メアリーのイントロをひけるようになった時も。 大学生になって大学間でインターネットがつながっていることを知り実際にマシン間でファイル転送ができた時も。
    その後もインドに行ったり、中国に住んだり、システムエンジニアをやったり、結婚して子供ができたり、ワクワクすることがたくさんあったわけです。

    でも、ふりかえってみたら持って生まれた性格か私はそういう初期衝動みたいな純粋なものを表現することを全くやって来ませんでした。簡単にいうとこれまでの人生はROM状態だったわけです。
    さあ、人生の後半戦、ボチボチそんなワクワクを残していきます。この文章を書いてブログに投稿されること自体が既にワクワクです。