2011年05月12日

Validator.nu HTML Parserを使ってみる

Validator.nu HTML Parserは、Java製のHTMLパーサ。Firefox4のHTML5パーサはこれを元にしているらしい。Liftが依存しているライブラリの中を覗いていたらこの子がいたので、試しに使ってみた。

利用しているバージョンは1.2.1。Scala2.8で記述。



Validator.nu HTML Parserは、JavaのSAXやDOM、XOMなどの機能と連携して解析することができるらしい。

たとえばJavaのSAXのDefaultHandlerを使って、HTMLのリンクの部分だけ抜き出す処理を行うと、下記のようなソースになる。Scalaで書いてるけど使ってる機能はJavaなので、心の目で見ればJavaに見えるはずです。

尚、記述している内容は、HTMLをパースして、そこからhrefの値を抜き出すようなことをやってます。
import java.io.StringReader
import org.xml.sax.InputSource
import nu.validator.htmlparser.sax.HtmlParser

object Foo {

def main(args: Array[String]) {
// Validator.nuのSAX用のパーサを用意する
val parser = new HtmlParser
// 自作のHandlerをsetContentHandlerする
parser.setContentHandler(new TestHandler)

// このHTMLを解析にかける
val reader = new StringReader("""
<p><a href="/foo.html">foo</a><br>
<a href="/bar.html">bar</a>
<p>HogeHoge""")

// 解析実行
parser.parse(new InputSource(reader))
}
}

// 自前でDefaultHandlerを継承したクラスを用意する
import org.xml.sax.helpers.DefaultHandler
import org.xml.sax.Attributes
import collection.JavaConversions._
class TestHandler extends DefaultHandler {
// StartElement時に、属性にhrefが入ってる要素の値を抜き出す
override def startElement(uri: String, localName: String, qName: String,
attrs: Attributes): Unit = {
for (idx <- 0 to attrs.getLength - 1 if attrs.getQName(idx) == "href")
println(attrs.getValue(idx))
}
}
上記のように書くと、以下のような結果が返ります。
/foo.html
/bar.html
次にDOMの場合。
import java.io.StringReader
import org.xml.sax.InputSource
import nu.validator.htmlparser.dom.HtmlDocumentBuilder

object Foo {
def main(args: Array[String]) {
// DOMの場合はHtmlDocumentBuilderを利用
val builder = new HtmlDocumentBuilder()

// このHTMLを解析する
val reader = new StringReader("""
<p><a href="/foo.html">foo</a><br>
<a href="/bar.html"><i>bar</a>
<p>HogeHoge""")

// parse実行。これでDocumentクラスが返る
val doc = builder.parse(new InputSource(reader))

// 試しに一番上のNodeの名前を取ってみる
println(doc.getChildNodes().item(0).getNodeName)
}
}
上記の処理を実行すると一番上の要素の名前が返るはずですが、結果は「html」と表示されます。HTML ParserがHTMLらしく整形する過程でhtml要素などを突っ込んでいるためです。

整形前と整形後でどんな風に要素が変化しているかは、次のScala版を見てもらうと分かりやすいかと。

Scalaで利用する場合はどうするか。Liftのnet.liftweb.util.HtmlParserではNoBindingFactoryAdapterを利用してScalaのNodeSeqを取得しているようです。NoBindingFactoryAdapterはDefaultHandlerを継承しているので、JavaのSAX版の時と似たような処理ができる。
import scala.xml.parsing.NoBindingFactoryAdapter
import java.io.StringReader
import org.xml.sax.InputSource
import nu.validator.htmlparser.sax.HtmlParser
import nu.validator.htmlparser.common.XmlViolationPolicy

object Foo extends Application {

// JavaのSAX版でも使ったHtmlParseを用意
val hp = new HtmlParser

// NoBindingFactoryAdapterをsetContentHandlerでセット
val saxer = new NoBindingFactoryAdapter
hp.setContentHandler(saxer)

// このHTMLをパースします
val reader = new StringReader("""
<p><a href="/foo.html">foo</a><br>
<a href="/bar.html"><i>bar</a>
<p>HogeHoge""")

// パース実行
hp.parse(new InputSource(reader))

// 全体を表示
println(saxer.rootElem)

}
パース処理を実行する前は、こんな感じのHTMLでした。
// Before
<p><a href="/foo.html">foo</a><br>
<a href="/bar.html"><i>bar</a>
<p>HogeHoge
しかし処理結果にはhtmlやhead、bodyなどの要素が追加されてこんな風になっています。
// After
<html><head></head><body><p><a href="/foo.html">foo</a><br></br>
<a href="/bar.html"><i>bar</i></a><i>
</i></p><p><i>HogeHoge</i></p></body></html>
ちなみにBeforeでは意図的に「i」要素を閉じ忘れていますが、Afterでは閉じ忘れた以降のすべての要素にiがかかる感じに修正されています。

このように要素の不備はうまく直してくれますが、タグを開っぱなし(<i ←こんな感じ</i>)にするとパース時にエラーが起きます。ソースを見たところ、TreeBuilderにsetNamePolicyがALLOWだったら、タグを閉じ忘れるなど酷いことをしていてもwarnで済ませてやろう的な記述がありました。

つまり、こう書けば崩れたタグの閉じ忘れがあるような酷いHTMLもなんとか解釈してくれるようです。
import scala.xml.parsing.NoBindingFactoryAdapter
import java.io.StringReader
import org.xml.sax.InputSource
import nu.validator.htmlparser.sax.HtmlParser
import nu.validator.htmlparser.common.XmlViolationPolicy

object Foo extends Application {

// HtmlParseを用意し、setNamePolicyにALLOWを設定
val hp = new HtmlParser
hp.setNamePolicy(XmlViolationPolicy.ALLOW)

// NoBindingFactoryAdapterをsetContentHandlerでセット
val saxer = new NoBindingFactoryAdapter
hp.setContentHandler(saxer)

// このHTMLをパースします
val reader = new StringReader("""
<a href="/bar.html"><ibar</a>
""")

// パース実行
hp.parse(new InputSource(reader))

// 全体を表示
println(saxer.rootElem)

}
// 結果
<html><head></head><body><a href="/bar.html"><ibar< a="">
</ibar<></a></body></html>

なんとも嫌な感じの結果ですが、嫌な感じのHTMLをパースしたのだから仕方ない。