遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

Rでニューラルネットワーク(1変数の関数近似)

機械学習・パターン認識方面の勉強初めてから4ヶ月ほど立ちました。最近はnaoya_tさん主催のPRML読書会に参加させて頂いています。

来週末8/29の第6回読書会ではニューラルネットワークの章の発表を担当することになったので、Rを使ってサンプルプログラムを組んでみました。参考にしたのはPRML5.1〜5.3の範囲で、sin関数からサンプリングした点データをニューラルネットワークを使って誤差逆伝播法で学習し、元のsin関数を近似します。

学習前の初期状態が以下の図。赤字が元の関数(線)およびサンプルデータ(点)で青字がニューラルネットワークの出力です。

で、学習後の状態が以下です。

いい感じに再現できています。
以下ソースコード

library(animation)

#number of training data
N <- 50

#number of hidden unit
s <- 5

#learning rate parameter
l <- 0.05

#standard deviation of training data
sd <- 0.05

#count of loop
LOOP <- 7000

#frame interval of animation movie
INTERVAL <- 0.1

#total time of animation movie
TOTAL <- 25

#initialize weight parameter
w1 <- matrix(seq(0.1,length=s*2, by=0.1),s,2)
w2 <- matrix(seq(0.1,length=s+1, by=0.1),1,s+1)

#generate traning data
xt <- seq(0,1,length=N)
yt <- sin(2*pi*xt)+rnorm(N,sd=sd)

neuro_func_ <- function (x) {
  a <- w1 %*% c(1, x)
  z <- tanh(a)
  y <- w2 %*% c(1, z)
  return(y)
}
#vectorize neuro_func_
neuro_func <- function (x) {
  sapply(x, neuro_func_)
}

png(filename = "graphic1.png", width = 480, height = 480, pointsize = 18, bg = "white")
plot(function(x) {return(sin(2*pi*x))}, xlab="input", ylab="output", xlim=c(0, 1), ylim=c(-2.5,2.5), sub=paste("S=", s, ",N=", N, ",l=", l), col=2)
plot(neuro_func, add=T, col=4, xlim=c(0,1))
points(xt, yt, col=6)

#main function to train the neural network
neuro_training <- function () {
  trigger_count <- as.integer(LOOP / (TOTAL / INTERVAL))
  for (k in 1:LOOP) {
    if (k %% trigger_count == 1) {
      plot(function(x) {return(sin(2*pi*x))}, xlab="input", ylab="output", xlim=c(0, 1), ylim=c(-2.5,2.5), main=paste("LOOP=", k), sub=paste("S=", s, ",N=", N, ",l=", l), col=2)
      plot(neuro_func, add=T, col=4, xlim=c(0,1))
      points(xt, yt, col=6)
    }
    
    for (i in 1:N) {
      # forward propagation
      a <- w1 %*% c(1, xt[i])
      z <- tanh (a)
      y <- w2 %*% c(1, z)

      # back propagation
      d2 <- y - yt[i]
      w2 <<- w2 - l * d2 * c(1, z)
      d1 <- d2 %*% (1-tanh(t(a))^2) * t(w2[2:(s+1)])
      w1 <<- w1 - l * t(d1) %*% c(1, xt[i])
    }
  }
}

saveMovie(neuro_training(), interval=INTERVAL, moviename="neural_network_training_movie", movietype="mpg", outdir=getwd(), width=640, height=480)

cat("learning end.print w1 and w2\n")
print(w1)
print(w2)

png(filename = "graphic2.png", width = 480, height = 480, pointsize = 18, bg = "white")
plot(function(x) {return(sin(2*pi*x))}, xlab="input", ylab="output", xlim=c(0, 1), ylim=c(-2.5,2.5), sub=paste("S=", s, ",N=", N, ",l=", l), col=2)
plot(neuro_func, add=T, col=4, xlim=c(0,1))
points(xt, yt, col=6)

収束する様子をムービーにしてみました。
中央上部に表示されている数値はサンプル点学習のループ回数です。

考察・雑感

学習パラメータは0.05固定としていますが、いろいろといじってみて、この値を大きくしすぎると収束せずに発散したり、振動したりするので、これくらいの値が妥当と判断しました。学習するデータ集合の数を多くした場合は、一つ一つの学習におけるの更新量は小さくするべきなので、これらの学習パラメータも小さくすることが望ましいはずです。
また、いずれにしても精度のよい近似が得られるまではデータ集合を繰り返し学習する必要があります。上記の動画を見ても、視覚的に精度のよい近似に近づくまでは1000回程度の学習ループを必要としています。学習パラメータを、固定ではなく、学習データに基づくパラメータに依存させたりすることでこの近似のスピードをもう少し早めることが出来るのかも知れません。PRMLの後の方で出てくるのかな?

参考文献

R全般に関して、id:syou6162氏のはてダを多いに参考にさせて頂きました。出し惜しみなく情報を晒して頂いてありがとうございます。
あと、Rでアニメーションを作るのは以下のエントリを参考にしました。

やろうと思ったことがほぼ何でもできちゃうR、ステキです!!。

DMC(Dynamic Markov Coding)のによるデータ圧縮プログラムを書いてみた

最近Managing Gigabytes勉強会に参加しているのでせっかくなので、この本に載っているアルゴリズムを使ってプログラムを組んでみました。

今回実装したのは、「2.5 SYMBOLWISE MODELS」の後半で説明されている「Dynamic Markov Coding(DMC)」です。書籍の他に、元論文「G. Cormack and R. Horspool, "Data compression using dynamic Markov modelling,"」を参考にしました。

実装はC++で行い、ソースはgithubに置きました。(CentOS 5.2+gcc 4.1.2で動作確認済)
http://github.com/thorikawa/MG/tree/master/dmc

以下、アルゴリズムの概要と実装上の工夫などをまとめてみます。
意見・指摘などは絶賛大歓迎です。

DMC(Dynamic Markov Coding)とは

DMCは適応型の算術符号化の一種で、データの流れ全体をマルコフ連鎖で表します。符号化の過程で、マルコフ連鎖の構造と確率を変化させていき、各状態における次状態への遷移確率を元に符号化します。

上の図は元論文から抜粋したものですが、状態Aから出ている矢印とその添字は、状態Aで読み込む次のビットが0である確率が50%、1である確率が50%であることを示しています。状態Aで0のビットを読み込むと、50%という確率に基づいて符号化を行った後に、状態Aから遷移する確率を更新して、状態Bに遷移します。

圧縮率を高めるためには、各状態における遷移確率が偏るようにマルコフ連鎖の構造を変化させていく必要があります。そのため、DMCではクローニングという仕組みを導入しています。クローニングを示した図が以下です。(元論文より抜粋)

(a)がクローニングを行う前で、AとBという2つの状態から状態Cに遷移していますが、ある一定の条件を満たしたときに、状態CはCとC’に分離します。ある条件というのは以下のソースを参照してほしいですが、元論文では2つのパラメタ閾値1と閾値2を設定して、AからCへのの遷移カウントが閾値1を超え、かつAからCへの遷移カウントとA以外からCへの遷移カウントとの差が閾値2を超えた場合にクローニングが行われます。

  // クローニングを行う。
  void DoCloning (uchar bit) {
    uint trans_count = current_state_->GetTransCount(bit);
    State *next_state = current_state_->GetNextState(bit);
    uint next_count = next_state->GetTotalCount();
    
    if (trans_count > cloning_threshold1_ && (next_count - trans_count) > cloning_threshold2_) {
      State *new_state = new State(last_state_++);
      current_state_->SetNextState(bit, new_state);
      double ratio = (double) (trans_count + 1) / (next_count + 2);
      // ratioの正規化(CloningはCloning後の次状態のtrans_count <= 現状態のtrans_countを保証していないので、ratioが1を超える場合がある。
      if (ratio > 1) {
        ratio = 1;
      }
      for (int i=0; i<=1; i++) {
        uint next_count_bit = next_state->GetTransCount(i);
        uint new_next_count_bit = (uint)(ratio * next_count_bit);
        new_state->SetNextState(i, next_state->GetNextState(i));
        new_state->SetTransCount(i, new_next_count_bit);
        next_state->SetTransCount(i, next_count_bit - new_next_count_bit);
      }
      cloning_count_++;
    }
  }

クローニングが行われないと、A→C→Dと、B→C→Eの2ケースのビット遷移が極端に多い場合でも、Cに到達した時点でAからきたかBからきたかの情報が失われてしまい、C→D・C→Eの遷移確率が均一化されてしまうので、効率のよい圧縮はできません。クローニングを行うことにより、出現頻度の多いパターンがたどる経路を分離させることができ、各状態における遷移確率を偏らせ、効率のよい圧縮が出来る、と理解しています。

Guazzoによる算術符号化アルゴリズム

次に、各状態における遷移確率から符号化を行う部分です。
Managing GigabytesにはDMCに特化したこの部分のロジック詳細は記載されていませんが、今回の実装では元論文でも紹介されているGuazzoによる算術符号化アルゴリズムを利用します。

Guazzoのアルゴリズムでは、整数値を取る数直線上をデータの出現確率に応じて分割します。今回はデータの入出力をビット単位で扱うこととしまうので、0と1に対応する二区間に数直線を分割していけばよいです。
数直線の幅はNを任意の整数値として[0x0,(1<<(N+1))-1]で初期化し、データを1ビット読み込むたびに、新たな上限と下限を計算していきます。そして計算し直した上限と下限を比較して両者の上位ビットが一致する場合は、一致するビットを確定ビットとしてアウトプットします。このビットシフトによって、上限と下限の最上位ビットは常に異なっている状態になり、数直線の幅を確保することができます。

以下が符号化部分のコードです。

  void Encode (uchar bit) {
    ulong mp = CalculateMP();
    Encode(bit, mp);
  }
  void Encode (uchar bit, ulong mp) {
    if (bit == 1) {
      lower_bound_ = mp;
    } else {
      upper_bound_ = mp - 1;
    }
    
    // 上限と下限を比較して確定しているbitがあればencoded bitとしてバッファに格納する。
    // 確定しているbit分だけ、上限・下限をシフトする。
    while ((lower_bound_ & kMSBIT) == (upper_bound_ & kMSBIT)) {
      unsigned char outbit = lower_bound_ >> (kN-1);
      AddBuffer(outbit);
      lower_bound_ = (lower_bound_ << 1) & kMSMASK;
      upper_bound_ = ((upper_bound_ << 1) & kMSMASK) | 1;
    }
    
    // クローニングを行う。
    DoCloning(bit);
 
    // マルコフ連鎖を更新する。
    current_state_->SetTransCount(bit, current_state_->GetTransCount(bit)+1);
    current_state_ = current_state_->GetNextState(bit);
  }
  // 分離点(mp)を計算する。
  ulong CalculateMP () {
    double p0, p1;
    ulong mp;
    uint totalCount = current_state_->GetTotalCount();
    p0 = (double) (current_state_->GetTransCount(0) + 1) / (totalCount + 2);
    p1 = (double) (current_state_->GetTransCount(1) + 1) / (totalCount + 2);
    mp = (ulong) ((p1 * lower_bound_ + p0 * upper_bound_) / (p0 + p1));
    if (mp <= lower_bound_) {
      mp = lower_bound_ + 1;
    }
    // mpの最右bitを1に変更
    mp = mp | 1;
    // 最右bitを1に変更した結果、upper_bound_を超える可能性がある。その場合、mp=upper_bound_とする。
    if (mp > upper_bound_) {
      mp = upper_bound_;
    }
    return mp;
  }
Guazzoアルゴリズムの実践における課題

元論文ではGuazzoのアルゴリズムを実装するにあたり2つの問題があると述べています。

  • 1つ目の問題は、整数値の数直線を上限と下限を分数で分割していくため、過程が進むにつれてより大きな精度が必要になる、という問題。
  • 2つ目の問題は、符号化するデータは有限であるという制約のもと、過不足のないデータを復元しなくてはいけないという問題。

まず一つ目の問題について、Guazzoは、正確に境界値を求める必要性を弱めることで解決しているそうです。上記のソースコードではCalculateMPの中で、確率からmp(数直線上の分岐点)を求める際にdouble型から整数値に丸めたり、mpの末尾bitを1に固定化したり(理由は後述)しています。このようにmpが正確な値ではなくても、圧縮時・復元時ともに同一の基準(論文ではfidelity criterion(忠実度基準)と呼んでいます)に従って計算が行われていれば、圧縮・復元に際しては大した問題にはなりません。

二つ目の問題については、かなり手ごわい問題で、たとえば1ビットが圧縮後に2ビットになった場合、圧縮後の2ビットがインプットされても元の2ビットを復元できる保障がない点に注意を払う必要があります。例えば、区間が[0x0000...0000, 0x1111...1111]として、入力ビット"0"を圧縮するとします。はじめの0で分岐点が"0x0010....0001"となり、区間が[0x0000...0000, 0x0010....0000]に縮小された場合、符号化された出力ビットは"00"になります。同様の状態でデコーダは分岐点の"0x0010....0001"以上かどうかで復元後のビットが0なのか1なのかを判断しますが、入力ビット"00"を復元しようとした場合、今後のビットの出現次第で、"0x0000....0000"や"0x0011....1111"のように分岐点以上にも以下にもなる可能性があります。そのため、この時点では復元することはできないのです。

すなわちaビットをbビットに符号化したとして、bビットあれば元のaビットを過不足なく復元できるとは限らないわけです。
この問題を解消するために、符号化時に二つの工夫をしています。

それは「mpの最右ビットを必ず1にしていること」と「符号化の最後にmpの最右ビットを除くN-1ビットを出力していること」です。これだけで過不足なく元データを復元することが出来るようになります。
理由を説明します。まずmpの最右ビットが1であるという条件から、数直線上の区間は必ず(ビットシフトの演算をのぞき)縮小されていきます。そのため、最後の入力ビットを符号化したときのmpのN-1ビット目までと最後に出力されるmpのN-1ビット目までは必ず異なるものになります。最後に出力されるmpのN-1ビットをインプットとすれば、最後の入力ビットを符号化したときのmp以上か未満かを必ず決定できる、つまり最後の入力ビットを決定できるのです。

また、最後のmpのN-1ビット目で止めている、というのは余分な1ビットが符号化されてしまうのを防止する意味もあります。最後に出力されるmpのNビット目を出力してしまうと、mp以上が確定してしまい、余分な"1"が符号化されてしまうことになります。しかし、N-1ビット目までの情報だと、デコーダは最後に出力されるmp以上か未満かを判別することはできません。つまり余分な1ビットは符号化されないのです。

Extra 7bits

さらに考慮すべき点があります。それはこのロジックでは符号化がビット単位になされていますが、実際にはデータはバイト単位だということです。符号化した後のビット数が8に達するたびにバイト単位で書き出しを行っていますが、最後に8に達しなかったあまりのビットは出力されることはありません。この状態のままでは完全な復元が出来なくなってしまいます。

そこで元論文で提案しているのは、符号化の最後に余分な7ビット(Extra 7 bits)を符号化することです。

  // encodedされた最終bitが、バイトとして出力されるようにダミーの7bitをencodeする。
  // また、encode対象の最終bitが正常にencodeされるように、mpの末尾1bitを除いた全てのbit列をencoded bitとして出力する。
  void EncodeFinish () {
    ulong mp;
    for (int i=0; i<7; i++) {
      mp = CalculateMP();
      // ダミーのbitとして、encoded bitが最低1bit確定されるbitを選択し、encodeする。
      if ((lower_bound_ & kMSBIT) == (mp & kMSBIT)) {
        // lower_bound_の最上位bitとmpの最上位bitが一致する場合は、ダミーのbitとして0をencodeする。
        Encode(0, mp);
      } else {
        // upper_bound_の最上位bitとmpの最上位bitが一致する場合は、ダミーのbitとして1をencodeする。
        Encode(1, mp);
      }
    }
    mp = CalculateMP();
    // mpの最後の1bitを除き、出力する。
    while (mp != kMSBIT) {
      uchar outbit = (mp >> (kN-1));
      AddBuffer(outbit);
      mp = (mp << 1) & kMSMASK;
    }
  }
};

ここで余分な7ビットを符号化しています。7ビットはなんでもよいかといえばそうではなく、それまでに符号化したビット全体がバイト列として出力されるためには、本体の符号化後ビット数が8N+1ビットだった場合を考慮して、最低でも7ビット分余分に出力を行う必要があります。そのため、0と1のうち、符号化した後に最低でも1ビット符号化される方のビットを選択して出力していきます。

これでバイト単位に丸めることによって、ビットが切り捨てられてしまったとしても、データ本体を符号化したビットは生き残るので、完全な形で復元することができます。

ベンチマーク

最後に、この圧縮手法で、圧縮率のベンチマークをとってみます。
テスト対象としては、定番所でThe Canterbury Corpusを利用しています。
まずはファイル種類別の圧縮率の相関図です。

それぞれのデータ特性についてあまり詳しく分析できていないのですが、テキストファイルではおおよそ35〜50%くらいの圧縮性能が実現できています。id:naoya氏が最近日記に書かれているPerl で Range Coder (再挑戦)のRange Coderでも、alice29.txtの圧縮率は50%をクリアできていなかったので、マルコフ連鎖を利用するだけで、大幅な性能改善を図ることが出来た、と理解しています。

次にCloningの閾値(上記ソースのcloning_threshold1=cloning_threshold2の値)を変化させていったときの圧縮率の変化です。他の圧縮アルゴリズムの比較として右端にZIPでの圧縮結果をプロットしています。

元論文では閾値が2のときがおおむね最も圧縮率が高い、と記述されていましたが、実際には上記グラフの通り、閾値が8〜32くらいのときに圧縮率がピークになるようです。直観的にも、閾値が低すぎると、各状態における状態遷移確率が偏る前に、すぐにクローニングしてしまうので、、圧縮率も低下するものと理解できます。

また、ZIPと比較すると、ptt5(FAXファイル)やEXCELファイル以外ではまだまだ圧縮率で負けていることがわかります。

まとめ

DMCを使って圧縮を行うと、エントロピーを最小にするように動的にマルコフ連鎖を最適化しながら圧縮を行うため、全体の出現頻度表をベースに圧縮するよりも効率のよい圧縮化を行うことができます。他のアルゴリズムとの比較など、考察が足りない部分は多々ありますが、それはおいおいということで。あらかじめ次数を決めてかかるPPMなどよりも、圧縮率はよいものと思われます。

参考文献

[1] Ian H. Witten, Alistair Moffat, Timothy C. Bell, Managing Gigabytes: Compressing and Indexing Documents and Images, Morgan Kaufmann Publisher, 1999
[2] G. Cormack and R. Horspool, "Data compression using dynamic Markov modelling," Computer J., 30 No. 6, 541-550, 1987.
[3] 高速な算術圧縮を実現する「Range Coder」(記事), 2006, CodeZine, 翔泳社

FirefoxのHTTPプロトコルハンドラを置換してローカルプロキシっぽい動作をさせる

先日の僕のFirefoxアドオン(XPCOM)でHTTPプロクシを実装するの記事の発展系として、piroさんがローカルプロキシっぽいことをローカルプロキシを立てずにやろうとして挫折したことのまとめというすばらしくためになる記事を書かれています。

この記事の中でpiroさんは「特定のURIにアクセスしようとした時だけ、あらかじめ定義しておいたルールに従って別のリソースを返す」ことを実現するために、3つのやり方を提案しています。

  1. ローカルプロキシを実装して、その中でリダイレクトするやり方。
  2. http-on-modify-requestイベントのタイミングでリダイレクトするやり方。
  3. nsIContentPoilcyのshouldLoad()の中でリダイレクトするやり方。

で、結論として2,3で目的を達成するのは難しそう、とのことなのですが、僕がかねてから考えていた4つ目のアイデアがあって、ちょっと試してみたらうまくいきそうな感じがするので途中経過だけど晒してみようかと思います。

4つ目のアイデア-httpプロトコルハンドラを置換する

その4つ目のアイデアというのはFirefoxデフォルトのhttpプロトコルハンドラを置換(上書き)してしまうというものです。

XPCOM コンポーネントの置換 - Days on the Moonにもある通り、Firefoxデフォルトで登録されているXPCOMであっても、同一のコントラクトIDを持つXPCOMを拡張の中で定義することによって、置換することができます。httpのリクエスト処理は「@mozilla.org/network/protocol;1?name=http」を起点にしてほとんどの処理が行われていますが、これを独自のXPCOMに置換してしまえば好きなようにリクエストを加工することができるはずです。

「独自のXPCOMに置換」と言うと、元々のXPCOMの挙動をちょっとだけ変更したい場合でも、全処理をスクラッチで書き直さなければいけないと思いがちですが、Components.classesByIDを使えば、クラスID(UUID)をキーにして置換前のXPCOMを取得することができるので、変更しなくていいところは全て置換前のXPCOMに丸投げしてしまえばよいです。

それを踏まえて、書いてみたのが以下のサンプル。

const Cc   = Components.classes;
const Ci   = Components.interfaces;
const Cr   = Components.results;
const kPROTOCOL_NAME = "my HTTP Protocol Handler";
const kPROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=http";
const kPROTOCOL_CID = Components.ID("97f7a1c0-4bf6-11de-8a39-0800200c9a66");
//default "@mozilla.org/network/protocol;1?name=http" class id
const ORIGINAL_CLASS = Components.classesByID["{4f47e42e-4d23-4dd3-bfda-eb29255e9ea3}"];


function myHttpProtocolHandler(){
  this._super = ORIGINAL_CLASS.createInstance();
}

myHttpProtocolHandler.prototype = {
  QueryInterface: function(iid) {
    return this._super.QueryInterface(iid);
  },

  allowPort: function(port, scheme) {
    return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).allowPort(port, scheme);
  },

  newURI: function(spec, charset, baseURI) {
    return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).newURI(spec, charset, baseURI);
  },

  newChannel: function(aURI) {
    if (aURI.asciiSpec == "http://www.google.co.jp/") {
      var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);  
      var uri = ioService.newURI("http://www.yahoo.co.jp/", null, null);
      return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).newChannel(uri);
    } else {
      return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).newChannel(aURI);
    }
  },
}


var myHttpProtocolHandlerFactory = {
  createInstance: function (outer, iid) {
    var handler = new myHttpProtocolHandler();
    return handler;
  }
}

var myModule = {
  registerSelf: function (compMgr, fileSpec, location, type) {
    compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
    compMgr.registerFactoryLocation(
      kPROTOCOL_CID,
      kPROTOCOL_NAME,
      kPROTOCOL_CONTRACTID,
      fileSpec, 
      location, 
      type
    );
  },

  getClassObject: function (compMgr, cid, iid) {
    if (!cid.equals(kPROTOCOL_CID))
      throw Cr.NS_ERROR_NO_INTERFACE;

    if (!iid.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      
    return myHttpProtocolHandlerFactory;
  },

  canUnload: function (compMgr) {
    return true;
  }
}

function NSGetModule (compMgr, fileSpec) {
  return myModule;
}

このようにコントラクトIDは「@mozilla.org/network/protocol;1?name=http」と重複定義して、クラスIDだけを置換前と後で異ならせています。httpリクエスト発行時には置換後のプロトコルハンドラが呼び出されますが、内部で置換前のプロトコルハンドラを呼び出していますので、基本的な処理は変わりません。変わっているのは「function newChannel(aURI)」の部分だけです。

さて、このXPCOMを登録した状態で、http://www.google.co.jp/のリクエストを発行するとYahoo!Japanのページを返します。

スクリーンショットを見てもらえば分かるのですがURL欄はhttp://www.google.co.jp/のままで、内部の処理内でのみURLを書き換えています。どうやらうまく動いているようです。

残課題

とここまで書いておいて何なのですが、実はこのソースにはなぜ正常に動作するのか、分かっていない部分があります。
一つはQueryInterfaceの部分で、QueryInterfaceの部分を置換前のXPCOMに丸投げしてしまっているので、その後の個別の処理も全て置換前XPCOMで行われてもよいはずです。だけど実際にはnewURI,newChannelといったメソッドは置換後のXPCOMのものが呼び出されます。なんでこういう動作をするのかを理解するのはXPCOMの仕組みとFirefoxの内部動作の仕組みをもうちょっと見てみないといけないかなと思ってます。

またもう一つは、このサンプルでも正常に動作しないサイトがいくつかあること。AJAXを多用しているサイトでも基本的には問題なく動くのですが、たとえばGMailにアクセスするとなぜか簡易版HTMLが表示されてしまいます。もしかすると上記の問題と関連しているのかも知れません。

というわけでまだまだ課題がありそうだけど、httpプロトコルハンドラの置換で、かなり柔軟なリクエストフックができるかも知れない、という話でした。これを踏み台にしてさらに発展させてくれる人がいることを願っています(汗)

FirefoxでProxy

inspired by http://moz-addon.g.hatena.ne.jp/ZIGOROu/20090518/1242640418

とりあえず、プロクシ設定して(色々不安定ながら)動くところまで。。
http://coderepos.org/share/browser/platform/firefox/Mogwai/trunk/src/components/MobileGateway.js?rev=33576

以下TODO。

  • Clientからのリクエストデータ読み込みをreadLineで行って1行まるごと解析しているが、Windows標準のtelnetのように1文字ずつ送信された場合などに対応できないので読み込んだデータからCRLFを検索するようにする。そもそも今はCRLF以外の改行コードでも、1行とみなされるが、HTTPの仕様上CRLFのみに対応すべき。
  • HTTPリクエストが相対パスでなされた場合の対応。
  • POSTリクエストの場合、Content-length分だけメッセージボディを読み込むようにする。
  • GETとPOSTくらいしか考慮していないので、他のメソッドにも対応する。(というか、メソッド行だけ読み込んで、あとはサーバー側に丸々コピーすればよいのか?)
  • ソケットの接続が切れた場合の対応とかが考慮されてない。
  • keep-aliveとか。
  • SSLとか。
  • マルチスレッド。
  • 不正なリクエストへの対応。エラーハンドリング。
  • ストリーム部分がいろんなInputStreamに切り替えていて見苦しいので何とかしたい。
  • GMailとかが正常に動かない。
  • dumpからの卒業。

Twitterを始めてみた

今更にも程があるのですが、Twitter始めてみました。

http://twitter.com/thorikawa

IPA未踏の情報がTwitterで発信され始めて、あーこれからはTwitterで情報発信していく時代なんだなーと実感して始めた次第。140字の中で表現する醍醐味をこれから味わっていく予定。

タブごとに端末選択可能なFireMobileSimulatorベータ版公開と人柱募集

「タブごとの選択機能」をβ版にて提供しておりましたが、2011.5.6にリリースした本家FireMobileSimulatorのバージョン1.2.0から、この機能がマージされております。お手数ですが、本家サイトより最新版のFireMobileSimulatorをダウンロードして下さい。

    • -

IRCには書き込んだのですが、FireMobileSimulatorで要望の多かった「タブごとに端末選択可能」を実装した新バージョンを作成したので公開します。

タブごとのHTTPリクエスト判別に結構特殊なことをやっているのと、インターフェース周りで使いづらい点があるかも知れないので、今回はベータ版として公開しています。人柱になってもいいよ、という方だけインストールしてください。
実際使ってみると、↓こんな感じで画面下部のステータスバーに選択している端末が表示されます。

不具合や要望などあったら、このエントリのコメント欄か、IRCチャネルfms-devel@freenodeに書き込んでいただけると嬉しいです。

タブごとのHTTPリクエストの識別には、

に書いたような処理をやってます。

あと、タブ分割の拡張機能Split Browserとの併用もできるようにしようと頑張ってみたのですが、今回はちょっと無理でした。タブ分割すると、タブごとに保存した内容が失われてしまうようなので、この辺はSplit Browser側で分割したタブごとにsessionStoreみたいな仕組みが使えると嬉しいな、と言ってみたりして。(自分が見つけられてないだけか?)

Plaggerで物件探し

夏頃に引っ越ししようと思って、いろんな不動産サイトを探しているのですが、フィードに対応してなかったり、通知機能がしょぼかったりするので、Plaggerで無理矢理対応させてしまおうかと。
とりあえず、目白商事という不動産サイトの検索結果をRSSフィードにするプラグインを書いてみました。ここから検索した結果ページのURLをYAMLで設定します。

以下スクリプトと設定ファイル

Plagger/Plugin/CustomFeed/MejiroShoji.pm
package Plagger::Plugin::CustomFeed::MejiroShoji;
use strict;
use warnings;
use base qw( Plagger::Plugin );

use URI;
use Web::Scraper;

sub register {
	my($self, $context) = @_;
	$context->register_hook(
		$self,
		'subscription.load' => \&load,
	);
}

sub load {
	my($self, $context) = @_;

	my $feed = Plagger::Feed->new();
	$feed->aggregator(sub { $self->aggregate(@_) });
	$context->subscription->add($feed);
}

sub aggregate {
	my($self, $context, $args) = @_;
	
	my $uri = URI->new($self->conf->{uri});
	$context->log(info => $uri);
		
	my $entry_scraper = scraper { 
		process '/tr', 'body' => 'TEXT',
		process '/tr/td[2]', 'id' => [ 'HTML' , sub { @_ = split /<br \/>/, $_; shift; }],
		process '/tr/td[2]/a', 'title' => 'TEXT' ,
		process '/tr/td[2]/a', 'link' => '@href' ,
		process '/tr/td[3]', 'date' => [ 'HTML' , sub { @_ = split /<br \/>/, $_; pop; }]
	};
	my $scraper = scraper {
		process '//div[@id="mainContents"]//table[@class!="search_form_tbl"]/tr[count(td) >= 1]', 'data[]' => $entry_scraper;
	};
	my $result = $scraper->scrape($uri);
	
	my $feed = Plagger::Feed->new();
	$feed->type('MejiroShoji');
	$feed->title('目白商事の物件');
	$feed->link($uri);
	
	foreach my $line (@{$result->{data}}) {
		my $entry = Plagger::Entry->new();
		my $dt = eval { Plagger::Date->parse_dwim($line->{date}) };
		$entry->date($dt) if $dt;
		$entry->body($line->{body});
		$entry->author($line->{title});
		$entry->title($line->{title});
		$entry->link($line->{link});
		
		$feed->add_entry($entry);
	}
	$context->update->add($feed);
}

1;

__END__
config.yaml
plugins:
  - module: CustomFeed::MejiroShoji
    config:
      uri: http://www.real-mejiro.co.jp/search/index.php?line_near_line[]=1&place_city=&place_town=&rent_min=0&rent_max=1&monopoly_area=&article_type=%E6%8C%87%E5%AE%9A%E3%81%AA%E3%81%97&line_time=0&mode=40&submit2.x=169&submit2.y=3&submit2=%E8%A9%B3%E7%B4%B0%E6%A4%9C%E7%B4%A2
  - module: Publish::Feed
    config:
      format: RSS
      dir: /var/www/html/moving-feed
      filename: MejiroShoji.rss

この調子でフォ●ントとか他のサイトも全部フィードとかメール通知とかに対応させていって、そのうち「WEB2.0時代の家探し」と題して書籍化する予定です(嘘)。
Googleストリートビューで、家の周りの様子とかもすぐ分かるしね。便利な世の中です。。