ちょっと大量のHTMLファイルをチェックする作業があって、grep/Perl One Linerで頑張るのも厳しいよなぁと思い、HTMLファイルをJavaでパースしてどうにかしようと思い立ちました、今日。
で、JavaでHTMLパーサといえば、個人的にはパッと思い浮かぶのがNekoHTML。
CyberNeko HTML Parser
http://nekohtml.sourceforge.net/
が、いかんせんこれは古い。HTML5にも対応していませんし。
よって、他のパーサを探してみました。2つほど見つかったので、ご紹介します。
HTMLをパースするので、以下のような閉じタグがないHTMLもパースできなければなりません。
index.html
<!DOCTYPE html> <html> <head> <title>タイトル</title> </head> <body> <div id="wrapper"> <h1>見出し</h1> <br> <p class="text-content">Hello</p> <p class="text-content">World</p> </div> </body> </html>
…なんと、しょうもないHTML。
これを、通常のDOMパーサでパースすると
jaxp.groovy
import javax.xml.parsers.* def dbf = DocumentBuilderFactory.newInstance() def db = dbf.newDocumentBuilder() def doc = db.parse(new File('index.html')) println(doc)
見事にコケます。
$ groovy jaxp.groovy [Fatal Error] index.html:12:7: 要素タイプ"br"は、対応する終了タグ"</br>"で終了する必要があります。 Caught: org.xml.sax.SAXParseException; systemId: file:/xxxxx/index.html; lineNumber: 12; columnNumber: 7; 要素タイプ"br"は、対応する終了タグ"</br>"で終了する必要があります。 org.xml.sax.SAXParseException; systemId: file:/xxxxx/index.html; lineNumber: 12; columnNumber: 7; 要素タイプ"br"は、対応する終了タグ"</br>"で終了する必要があります。 at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:257) at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:347) at jaxp.run(jaxp.groovy:5)
というわけで、HTMLをパースするには、やっぱりHTMLパーサが欲しいのです。
ちなみに、Javaで言いつつ、コードは面倒なのでGroovyでいきます…。
jsoup
今回、最終的に使用したのはこちらのパーサです。HTML5に対応しています。
jsoup: Java HTML Parser
http://jsoup.org/
Maven依存関係(というか、ここではGrapeですが)を使用して、以下の様に依存関係を定義します。
@Grab('org.jsoup:jsoup:1.7.3') import org.jsoup.Jsoup
で、目的のファイルをパースすると、Documentが得られます。
def file = new File('index.html') def encoding = 'UTF-8' def doc = Jsoup.parse(file, encoding)
ここでのDocumentは、org.w3c.dom.Documentではなく、jsoup独自のクラスです。
ここで、Elementを継承したサブクラスに対して、いろいろと便利な操作が実行できます。なお、DocumentはElementのサブクラスです。
id指定やclass指定、jQueryなどのようなCSSセレクタの指定などもでき、なかなか便利です。
// idで取得 def elm1 = doc.getElementById('wrapper') // classで取得 def elems1 = doc.getElementsByClass('text-content') // CSSセレクタで取得 def elems2 = doc.select('p.text-content') // CSSセレクタで取得 def elems3 = doc.select('#wrapper')
Element#selectの結果はElementsというクラスが返ってくるのですが、このクラスはCollectionやListを実装しているので、反復処理もけっこう楽です。Groovyならgrep/eachなども普通に可能。
その他のメソッドは、詳しくはJavadocを参照してください。
Element
http://jsoup.org/apidocs/org/jsoup/nodes/Element.html
これは、なかなか良いパーサが見つかりました。けっこう役に立ちそうです。
その他、URLからパースしたり
def doc = Jsoup.connect('https://www.google.co.jp/').get()
HTMLフラグメントをパースすることもできます。
jsoup-fragment.groovy @Grab('org.jsoup:jsoup:1.7.3') import org.jsoup.Jsoup def html = '''\ <div id="wrapper"> <h1>見出し</h1> <br> <p class="text-content">Hello</p> <p class="text-content">World</p> </div>''' def doc = Jsoup.parseBodyFragment(html) def body = doc.body()
オフィシャルのCookbookを見ると、いろいろできそうですよ。
jsoup cookbook
http://jsoup.org/cookbook/
The Validator.nu HTML Parser
jsoupを見つけた時点で、作業はそちらを利用したのでその時は使いませんでしたが、こちらもご紹介。
The Validator.nu HTML Parser
http://about.validator.nu/htmlparser/
Firefox4のHTML5パーサは、こちらを使用していたとのこと。
W3C準拠っぽくて、SAXやDOMのAPIを備えます。こちらも、Maven Centralから取得できるので、こんな感じでDOMのパースコードが書けます。
validator-nu.groovy
@Grab('nu.validator.htmlparser:htmlparser:1.4') import nu.validator.htmlparser.dom.HtmlDocumentBuilder import org.xml.sax.InputSource def builder = new HtmlDocumentBuilder() def reader = new BufferedReader(new InputStreamReader(new FileInputStream('index.html'), 'UTF-8')) def doc = builder.parse(new InputSource(reader)) // あとは、org.w3c.dom.Documentとして操作する reader.close()
HtmlDocumentBuilder#parseで、org.w3c.dom.Documentが返るので、あとは通常のDOMプログラミングをすればOKです。
DOMやSAXを扱う必要がある場合は、こちらを使うという感じでしょうか。パス指定をして選択したい場合は、XPathですね。
便利そうなので、両方覚えておきましょう。