goo blog サービス終了のお知らせ 

Ptron: ブロックリストを用いて外部ファイルにフィルタを記述する

2010-07-15 19:28:03 | Proxomitron
※ この記事には一部「無駄」な情報が含まれています(巻末参照…笑)。


さて前回は「可読性の悪さをどう取り除くか」という課題を残して話を終えた。ご覧の通り HTTP Header filter editor は入力欄がすべて単一行テキストボックスで構成されている。



フィルタが長くなってくると見えないどころか処理の流れすら掴めない。最大の欠点はデバッグが非常にやりづらいことである。可読性の悪さからメモ帳などに書いたコードをテストのたびにテキストボックスへコピペしている開発者も多いことだろう。この作業は煩わしい以外の何者でもないが、かくいう私も最初はそうであった(苦笑)。

この抜け道としてお薦めしたいのがブロックリストを使う方法である。「そんなのとっくに知っているよ」という人もいるだろうが、これ以外にもう一つトリックを用意しているので、煙たがらずに読み進めていただければと思う。

ブロックリストを使うと外部ファイルに記述したフィルタをヒアドキュメント的に読み込むことができる。今回の例でいうと、ゼロパディング部分のフィルタをテキストファイルとして保存し、ブロックリストに登録する。拡張子が .prx になっているのは筆者が愛用するテキストエディタの都合であり、それ以外に特別な意味は持たない。またブロックリスト名が func/date となっているのは前々回の log/iTunes と同様、Block List Configuration におけるソートを考慮して付けたものである。



あとは filter editor 側で $LST コマンドを使い、このフィルタを読み込ませるだけ。



これで filter editor にベタで入力したときと同じ処理を行わせることができる。またブロックリストはフィルタの実行時にリロードされるため、メモ帳などで編集作業を行っている場合、変更が即時反映されるというメリットもある。

ただこの「リロード」が曲者で、フィルタが実行されるたびに「ファイルの読み込み」というオーバーヘッドが発生することは念頭に置いておくべきであろう。これを避けるには URL フィルタをしっかりと掛けておき、必要なとき以外はファイルの読み込みが発生しないようにしておく。また頻繁に実行されるフィルタはデバッグが終了したらテキストボックスに直接書き込むといった対策も有効だ。このあたりの判断はユーザに任せたい。

そろそろ「もう一つのトリック」について触れておこう。これは複雑なフィルタを開発する人に是非お薦めしたい裏技である。ブロックリストを用いる方法であってもフィルタが長くなると可読性が悪くなることは避けられない。改行してフィルタを読みやすくしようとすると、改行コードがリストの区切りとみなされ、各行が独立したフィルタとして実行されてしまう。つまり単一行という制限からは逃れられないわけだ。

そこで色々と調べてみたところ、改行と認識するのは LF のみであって CR は空白文字列として無視されることがわかった。つまりこれを逆手に取るのである。テキストエディタを選ぶが、改行コードを任意に設定できるものであれば CR にしておけばよいということである。これにより見た目は改行されていても Proxomitron は単一行として認識してくれるので、可読性とフィルタ処理を両立させることができる。つまり、
$SET(M=$DTM(M))($TST(M=[#1-9])$SET(M=0$GET(M))|)$SET(D=$DTM(D))($TST(D=[#1-9])$SET(D=0$GET(D))|)$SET(E=$GET(D)/$GET(M)/$DTM(Y))$SET(d=$DTM(Y)-$GET(M)-$GET(D))

というフィルタは改行コードを CR にさえすれば
$SET(M=$DTM(M))($TST(M=[#1-9])$SET(M=0$GET(M))|)
$SET(D=$DTM(D))($TST(D=[#1-9])$SET(D=0$GET(D))|)
$SET(E=$GET(D)/$GET(M)/$DTM(Y))
$SET(d=$DTM(Y)-$GET(M)-$GET(D))

と書いても良いということだ。

CR で改行された複数行のフィルタをブロックリストとして作成し $LST コマンドで読み込めば、かなり複雑なフィルタの開発も行えるようになるだろう。ちなみに以下のコードは普通のプログラムっぽく見えるが、紛れもなく Proxomitron のヘッダフィルタである。
(
    $FILTER(True)
    ($URL((*(?*))2&s=([0-9]+)1) (
        $SET(q=)
        $SET(s=1)
        $SET(u=2)
    )|(
        $SET(q=q)
        $SET(u=u)
    ))

    ($RESP(?*) (
        ((^$URL(*(?|&)fn=)) (
            ($IHDR(Content-Length:)|)
            ($TST(=[#0-800]) (
                ($OHDR(X-Apple-Store-Front:([^-]+)2)|)
                $SET(s=2)
                $SET(list=143462,143441,143444,143455,143450,143442,143449,143457,143456)
                ($TST(list=*$TST(s),([0-9]+)3*) (
                    $JUMP($GET(u)&s=3)
                )|)
            )|)
        )|)
    )|(
        ((^$TST(s=?*)) (
            $SET(s=143462)
            ($KEYCHK(^C^S) (
                ($ADDLSTBOX(Artwork-List,iTunes Artwork Fixer,$TST($SET(url=))) (
                    $ADDLST(Artwork-List,$TST(q=$WESC($GET(q))(^?))n)
                )|)
            )|)
            ($LST(Artwork-List)|$SET(url=))
            ($TST(url=?*) (
                $LOG(Rmatched: q)
                $LOG(Rmerged: $GET(url))
            )|)
            ($TST(url=http://([^?]+)?1) (
                $SET(buf=1)
                ($TST(=itunes.apple.com/WebObjects/MZStore.woa/wa/viewAlbum) (
                    $SET(url=$GET(buf))
                )|(
                    $SET(q=?fn=$ESC($GET(url)))
                    $SET(url=)
                ))
            )|)
            ($SET(2=) $TST(url=(*?|)([^=]+)=([^&]+)1(&2|)) (
                ($TST(=id) $SET(key=p) | $SET(key=))
                $SET(val=1)
                $SET(prm=$GET(key)=$GET(val))
                ($TST(2=?*) $SET(url=2) | $SET(url=))
                ($TST(key=s) (
                    $SET(s=$GET(val))
                )|(
                    ($TST(q=$TST(key)=[^&]+1) (
                        $SET(q=$GET(prm)1)
                    )|(
                        $SET(q=$GET(q)&$GET(prm))
                    ))
                ))
            )|($TST(url=(?*)) (
                $SET(q=?fn=$ESC())
                $SET(url=)
            )|))
            ($SET(2=) $TST(url=(*?|)([^=]+)=([^&]+)1(&2|)) (
                ($TST(=id) $SET(key=p) | $SET(key=))
                $SET(val=1)
                $SET(prm=$GET(key)=$GET(val))
                ($TST(2=?*) $SET(url=2) | $SET(url=))
                ($TST(key=s) (
                    $SET(s=$GET(val))
                )|(
                    ($TST(q=$TST(key)=[^&]+1) (
                        $SET(q=$GET(prm)1)
                    )|(
                        $SET(q=$GET(q)&$GET(prm))
                    ))
                ))
            )|)
            ($SET(2=) $TST(url=(*?|)([^=]+)=([^&]+)1(&2|)) (
                ($TST(=id) $SET(key=p) | $SET(key=))
                $SET(val=1)
                $SET(prm=$GET(key)=$GET(val))
                ($TST(2=?*) $SET(url=2) | $SET(url=))
                ($TST(key=s) (
                    $SET(s=$GET(val))
                )|(
                    ($TST(q=$TST(key)=[^&]+1) (
                        $SET(q=$GET(prm)1)
                    )|(
                        $SET(q=$GET(q)&$GET(prm))
                    ))
                ))
            )|)
            $JUMP(http://hp$GET(q)&s=$GET(s))
        )|)
    ))
)&?*



※ 後日「複数行フィルタは行頭をインデントさせることで実現できる」という情報をいただきました(練炭さんありがとう!)。ヘルプファイルの Creating blocklists の項で詳しく解説されていたんですね。読んでみるとかなり特殊な使い方を想定した実装だったことがわかります。やっぱりこのツールの作者さん、凄いよねぇ。ちなみに面倒なので記事はそのままにしておきます(笑)。


Ptron: $DTM(d) をゼロパディングする

2010-07-14 19:36:27 | Proxomitron
さて前回の宿題であった $DTM(d) がゼロパディングされない仕様について私なりの解決策を提示しようと思う。実は「月」と「日」を取得する $DTM(M) と $DTM(D) も同様にゼロパディングされない。まずはこれらをゼロパディングすることから始めよう。方法は色々と考えられるが、私は以下のようなフィルタを作ってみた。
$SET(M=$DTM(M))($TST(M=[#1-9])$SET(M=0$GET(M))|)

$SET(D=$DTM(D))($TST(D=[#1-9])$SET(D=0$GET(D))|)

両者は基本的に同一の構造である。「月」を例に解説すると
$SET(M=$DTM(M))

は $DTM(M) で取得した「月」を変数 M に格納する処理。
($TST(M=[#1-9])$SET(M=0$GET(M))|)

は条件分岐であり、次のように捉えるとよいだろう。
($TST(条件式)真の時の処理|偽の時の処理)

つまり M=[#1-9] が真であれば $SET(M=0$GET(M)) を実行し、偽であれば空文字(true)を返す。偽の時の処理でわざわざ true を返すのは、以降の処理を継続するためである。これを省き、
$TST(M=[#1-9])$SET(M=0$GET(M))

とだけ書くと、フィルタは M=[#1-9] が偽となった時点で false を返し、処理は停止する。くれぐれも true を返すのを忘れてはいけない。

さて $TST(M=[#1-9]) は変数 M すなわち $DTM(M) が返す「月」の値が一桁の場合という条件式である。これが真であれば $GET(M) で変数 M を読み出し、先頭に 0 を付けて再び変数 M に格納するという処理を行う。これは「日」についても全く同様であり、「月」と「日」はそれぞれ $GET(M) と $GET(D) を用いてゼロパディングされた値を取得できる。

ここまで来るとあとは簡単だ。以下のようにして新たな変数を用意、$DTM(E) $DTM(d) に相当する年月日を作成してやればよい。
$SET(E=$GET(D)/$GET(M)/$DTM(Y))

$SET(d=$DTM(Y)-$GET(M)-$GET(D))

これらは $GET(E) と $GET(d) で取得できる。さて今回のフィルタを踏まえて前回のフィルタを書き換えてみよう。
[HTTP headers]
In = FALSE
Out = TRUE
Key = "URL: iTunes Access Log (out)"
Match = "$SET(M=$DTM(M))($TST(M=[#1-9])$SET(M=0$GET(M))|)$SET(D=$DTM(D))($TST(D=[#1-9])$SET(D=0$GET(D))|)$SET(E=$GET(D)/$GET(M)/$DTM(Y))$SET(d=$DTM(Y)-$GET(M)-$GET(D))$OHDR(User-Agent:iTunes)"
Replace = "$ADDLST(log/iTunes, $GET(d) $DTM(T) ¥u)"

「月」と「日」が正しくゼロパディングされるようになったはずだ。唯一の欠点はフィルタが複雑になったため可読性が落ちたことであろう。実はこれにも解決策があるのだが、詳しくはまた次回。

Ptron: iTunes の通信ログを記録する

2010-07-13 19:18:51 | Proxomitron
iTunes はサーバとの通信に HTTP を用いている。itms:// というスキームの iTunes Store プロトコルが使われることもあるが、この実体は http:// へのリダイレクトである。HTTP プロキシはログオンユーザのインターネットオプションを参照するため、Internet Explorer と同様に Proxomitron で通信をトラップすることが可能だ。iTunes がどのようなリクエストを出しているかを調べるには以下のようなフィルタを書けばよい。
[HTTP headers]
In = FALSE
Out = TRUE
Key = "URL: iTunes Access Log (out)"
Match = "$OHDR(User-Agent:iTunes)"
Replace = "$ADDLST(log/iTunes, $DTM(d) $DTM(T) ¥u)"

リクエストヘッダの User-Agent に "iTunes" という文字列が含まれる通信を処理対象とする。ログファイルにはリクエストされた URL の他にアクセス日時が記録される。
2010-7-4 09:12:08 http://ax.itunes.apple.com/jp/album/tamashii-revolution-single/id377825255
2010-7-4 09:12:11 http://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/customerReviews?displayable-kind=2&id=377825255
2010-7-4 09:12:11 http://my.itunes.apple.com/WebObjects/MZStoreElements.woa/wa/cmaDetailsFragment?id=377825255
2010-7-12 20:58:59 http://ax.itunes.apple.com/jp/album/live-from-tokyo-ep/id258753142
2010-7-12 20:59:02 http://a1.phobos.apple.com/us/r30/Features/af/01/77/dj.byytoisi.jpg
2010-7-12 20:59:02 http://a1.phobos.apple.com/us/r30/Features/80/68/6f/dj.uxkxdlvc.jpg

ちなみにログファイルのリスト名が "log/iTunes" となっているのは、筆者がソートを考慮して付けたものであり、スラッシュが特別な意味を持つわけではないのでお間違えなきよう。



ところでログを見てお気付きの方もいると思うが $DTM(d) はゼロパディングされない。桁を統一して見やすくするには少し工夫する必要がある。この辺りは次回以降に解説する予定。

Ptron: 存在しないページを Google Cache で自動検索

2010-07-12 19:00:34 | Proxomitron
シンプルなわりに、意外と役に立っている自作フィルタ。Web サーバがステータスコード 403 あるいは 404 を返してきた場合、非透過的に Google キャッシュへリダイレクトします。そもそもページが表示されないのなら、キャッシュでも見れたほうがマシでしょって寸法ね(笑)。
[HTTP headers]
In = TRUE
Out = FALSE
Key = "URL: Redirect to Google Cache (in)"
Match = "$RESP([#403:404])"
Replace = "$JUMP(http://www.google.com/search?q=cache:$ESC(¥u))"


Proxomitron はもう10年くらい使い続けている付き合いの長いツール。スマートなフィルタを考えたり、トリッキーな手法を試したり、そんな感じで楽しんできた(力技的な Web スクレイピングはあまり好きではない)。また HTTP プロトコルの勉強においても良き教材となってくれた。とくにログウィンドウは HTTP 通信の解析になくてはならない存在である。

そんなわけで今更だけど自分が作成してきたフィルタを少しずつ公開していこうかなと思っている。ただご覧のとおりの気まぐれ屋なため、飽きるか面倒になったらすぐ停滞するでしょう。まあ不定期掲載ということでお茶を濁しときます(笑)。