部分継続を用いたエンコーディング変換
最近 XPath の Scheme 版 (SXPath) を自前で開発しているんですが、その関連で HTML パーサーも作ってみようと思い立ちました。
が、パーサー以前の問題でいきなり躓いてしまいました。文字コードの判定の仕方が分からなかったんです。
PLT Scheme では、入出力のエンコーディングを明示的に指定して変換することは出来るんですが、自動判定の機能が無いみたいなんです。
野良 HTML を相手にしたいので、HTTP ヘッダや META タグでのエンコーディング指定は当てに出来ない、というのが大前提です。
なので、自動判定の仕組みを自分で作る必要が出てきました。
考えた結果、入力側のエンコーディング方式として想定されるもののリストを予め用意しておき、総当り的に変換を試みる方法が浮かびました。
とりあえず、文字コード変換には scheme/port というライブラリの reencode-input-port という関数を使うことにします。これは、入力ポートを、指定エンコーディングから UTF-8 に変換した新しい入力ポートを返す関数です。
呼び出し例:
(reencode-input-port in "CP932" #f)
実際に使ってみて分かったんですが、変換に失敗する場合でも、この関数を評価した時点では何のエラーも起こりません。返された入力ポートを読み込むうちにエラーが出るんです。
したがって、変換とは別の場所 (読み込み・パースを行う場所) でエラー・ハンドリングを行い、再び前の変換の部分に戻る仕組みを考える必要があります。
このようになりました:
(require scheme/port scheme/control) (define encs '("CP932" "EUC-JP" "ISO-2022-JP")) (define (rewind in) (or (zero? (file-position in)) (file-position in 0)) in) (define (to-utf8 in) (reset (for-each (lambda (enc) (shift k (values (reencode-input-port (rewind in) enc #f) (lambda () (k #f))))) encs) (values (rewind in) #f))) (define (in-utf8 f in) (define (try in2 next) (with-handlers ((exn:fail? (lambda (x) (close-input-port in2) (call-with-values next try)))) (f in2))) (call-with-values (lambda () (to-utf8 in)) try))
部分継続を取り出すのに reset と shift のペアを用いるパターンは zipper の時にもお話しました。ここでは shift で切り取った k を lambda に包んで、失敗の際に次の候補を試すための継続として返しています。
テスト:
(call-with-input-file tmp-file (lambda (in) (in-utf8 (lambda (in) (display "Title: ") (display (cadr (regexp-match #rx"<TITLE>(.+?)</TITLE>" in))) (newline)) in)))
結果:
Title: Title: 今夜も♥うさちゃんピース
最初の試行 (Shift-JIS) では一つ目の display の後でエラーが生じ、そして何事も無かったように次のエンコーディングが試されて成功した、ということが読み取れます。
in-utf8 の引数の関数内では、変換エラーが起こる可能性を全く気にせずにプログラミングできる、というわけです。
実際には、in-utf8 には関数値を返すだけの、副作用を生じない関数を渡すべきでしょう。変換エラーが生じた場合、途中までの計算は全て破棄され、正しい結果だけを得ることができて便利だと思います。
ただ、この実装には若干の問題があります。変換時のエラーとそれ以外のエラーとを区別していないのです。reencode-input-port が固有の例外型を投げてくれないので、一般のエラーと同じ扱いで (exn:fail?) 補足しているためです。exn-message 関数でエラーメッセージをチェックするなどの工夫が必要でしょう。