HtmlAgilityPackよりSgmlReaderがいいと思う

最近、仕事で他のメンバーが書いたHTMLスクレイパーみたいなコードの大幅な手直しをしているのだけど、ちょっとこれは書いておこうと思ったネタを公開しようと思う。それは.NETでHTMLを解析する、より真っ当な方法のことだ。

一言で言うなら、HtmlAgilityPackを使うより、SgmlReaderを使ったほうが良い。理由も簡潔に言うなら、HTMLはSGMLに準拠して設計された仕様だから、SGMLの流儀に従ってロジカルにマークアップを解析できるパーサーを使った方が適切に処理できるし、実際HtmlAgilityPackの解析はSgmlReaderより雑だ。

ちょっと待った。何が「雑」なんだろう? 雑というのはちょっといい加減な物言いだ。HTMLを解析するというのは、そんなに雑だったり厳密だったりするものだろうか? 厳密すぎるHTMLパーサーというのはかえって実用性が低かったりするんじゃないの?

実用性は、そうかもしれない。でもそれは正しいHTMLをまともに処理できるようになってから言うべきことだ*1。HtmlAgilityPackは、正しいHTMLをまともに処理できない。とりあえずこれがSgmlReaderを選好する一番の理由だ。

SgmlReaderは、じゃあHtmlAgilityPackに比べて何が良いのか? どうまともにHTMLを処理できるのか? キーポイントはHTMLがSGMLである、という点にある。

SGMLをちゃんと解析するのは、XMLを解析するほど簡単ではない*2SGMLでは、一定の条件のもとで、閉じタグを省略することができる。閉じタグだけじゃなくて開きタグも省略できることがある(!)。各タグが省略できるかどうかは、DTDで決まる。XMLDTDとは異なり、SGMLでは要素型宣言(<!ELEMENT ... >)で、これらの省略可否を指定できる(しかもなんと両方省略することもできる)。この特徴をなくして、代わりにDTDを完全にオプションの地位に追いやったのが、XMLの功績のひとつだ。DTDの情報が無いと、どこで開きタグや閉じタグが省略されたのかわからないから、SGMLではDTDは必須だ。

さて、この必須のDTDあるいはそれに相当する情報が無いと、HTMLを「正しく」解析することはできない。何が起こるかというと、たとえば次のようなHTMLがあるとする:

<table> <tr> <td>ABC <td>DEF <td>GHI </tr></table>

このHTMLのツリー構造は、まあこんな感じだろう、と(HTMLを解する)人間ならわかる:

table
  tr
    td
      ABC
    td
      DEF
    td
      GHI

でもこの2番目のtdが何でtrの下に着くのか説明できるだろうか? HTMLのDTDがあれば、tdは閉じタグが省略可能で、tdの下にtdが来ないことも、 <!ELEMENT td ... > の内容モデルの定義からわかる。だけど、DTDの情報が無ければ、これは分かりようがない。わからなければ、とりあえずtdの下にtdをくっつける等の処理を進めることになる。

このDTDに相当する情報が、HtmlAgilityPackにはない。だから、実際にHtmlAgilityPackにHTMLを解析させてみると、tdの下にtdが来るようなツリー構造が生成されてしまうことになる。

HtmlAgilityPackのソースをざっと見た限り、DTDを解析するためのコードは見当たらないし、それに相当する文書型を実現するコードも見当たらない。文書型定義に照らして妥当なツリーを構築しないことになる。そして、これは単純な数行〜数十行バグフィックスでどうにかなるレベルの問題じゃなくて、根本的な設計の問題だ。「単純なHTMLが解析できるお手軽なパーサー」を作りたかったということなら、それはそれでアリだと思う。だけど、それでは万人向けにはならないというか、少なからぬ人が妙なところで躓くことになると思う。

HtmlAgilityPackに、かっちりしたパーサーを実装するくらいなら、SgmlReaderを使ったほうが早いと思う。幸いなことに、かつては不明ライセンスで使えなかったSgmlReaderも、Microsoftから公式にMS-PLで公開され、MindTouchによってApacheライセンスでメンテされている

ちなみにHTMLパーサーの代替についてはこのブログポストがいろいろ書いているのだけど、SgmlReaderに関するコメントが事実に反するので(同エントリの著者が何でXHTMLに準拠していないといけないと思ったのかは謎)、とりあえずSgmlReaderが現実解として使えると思う、という自分の意見に変わりはない。(あと実は閉じタグ処理周りではSgmlReaderもたぶんバグを抱えているんじゃないかと思うことがあるのだけど、ちゃんと検証していないのでまだわからない。)

小ネタ数行で終わらせるつもりが、なんか無駄に長くなってしまった気がするので、今日はこの辺で。

*1:そもそもHtmlAgilityPackが厳密でないからと言って、柔軟だということには特にならないわけだけど

*2:ちなみにXMLの解析だってそんなに超簡単ってわけにはいかない。XML conformance test suiteを全部通すのはすごく大変だ。CDATA sectionの終端 "]]>" をちゃんと認識して、]]]>を飛ばさなかったり、well-formed XMLでもDTDに文法エラーが無いかチェックしたりとか