Haskell+ParsecでHTMLのパーシング

Haskellの紹介で、「パーサーコンビネータを使うとパーシングが簡単にできる」と良く聞くので、僕もParsecを使ってちょっと試してみた。

情報源

Parsecの使い方は本家に置いてあるpdfが古いが短くて割と分かりやすい。というか他にあまり資料が見つからない。
http://legacy.cs.uu.nl/daan/download/parsec/parsec.pdf
あとはライブラリの最新ドキュメントを眺めればどんなコンビネータを使えるのかが分かる。
http://hackage.haskell.org/package/parsec-3.1.2

HTMLのパージングに関しては、W3Cのページが参考になる。ただ今回は仕様に忠実に実装したわけではない。
http://www.w3.org/TR/html5/parsing.html#parsing

プログラム

試行過程をこの記事に書こうかと思ったがそうとう面倒なのでとりあえずはソースのみ置くことにする。Gistにソースを置いた。
https://gist.github.com/2904427

HTMLをパースしてツリー構造を作る。パーサ本体は150行くらいになった。さらに、ツリー構造を表示/HTMLを生成するコードや、QuickCheckのためのarbitrary関数の定義などを含めて計300行。QuickCheckでは、

  1. 任意のツリーを生成
  2. それをHTMLに書き出し→再びパースして生成したツリー

この2つのツリーが等価であるべしというprop_html関数を定義した。

使ってみる

main関数は標準入力のHTMLをパースしてツリーを出力する。http://www.ne.jp/asahi/hishidama/home/tech/scala/index.htmlから持ってきたHTMLをパースしてみる。

$ runhaskell htmlparse.hs < test.html 
html: []
|
+- head: []
|  |
|  +- "
"
|  |
|  +- meta: [http-equiv="Content-Type",content="text/html;charset=Shift_JIS"]
|  |
|  +- "
"
.....(略).....
   |  |  |  |  |  +- "さんの"
   |  |  |  |  |  |
   |  |  |  |  |  `- a: [class="ext",target="class-diagrams.appspot.com",href="http://class-diagrams.appspot.com/"]
   |  |  |  |  |     |
   |  |  |  |  |     `- "scalaとかjavaとかのclass図を表示するサイト"
   |  |  |  |  |
   |  |  |  |  +- "
			"
   |  |  |  |  |
   |  |  |  |  +- li: []
   |  |  |  |  |  |
   |  |  |  |  |  +- "kmizuさんの"
   |  |  |  |  |  |
   |  |  |  |  |  `- a: [class="ext",target="sites.google.com",href="https://sites.google.com/site/scalajp/"]
   |  |  |  |  |     |
   |  |  |  |  |     `- "プログラミング言語Scala 日本語情報サイト"
   |  |  |  |  |
   |  |  |  |  `- "
		"
.....(略).....

わりと良い感じに動いている! 結構時間かかったしここまで作るのにあまり簡単でもなかったけど。特に、try関数の使い方がわりと難しかった。choice あるいは <|>を使う状況で、選択される各々の中にtryを書き忘れると間違ったパーサが選ばれてしまいエラーになったりした。どこまでtryを書けば良いのか、という判断をする際に少し混乱した。

現時点で分かっている制限

  • 入力ファイルの文字コードがUTF-8しか受け付けないっぽい。エンコードの件は未調査。
  • タグは網羅していない。ただ冒頭のdata宣言にタグを足せば良いだけ。
  • invalidなHTMLや、g:plusoneなどというGoogle+独自のタグなどはエラーになる。