CLOVER🍀

That was when it all began.

Javaで使える、HTML5パーサ

ちょっと大量のHTMLファイルをチェックする作業があって、grepPerl 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/

Firefox4HTML5パーサは、こちらを使用していたとのこと。

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ですね。

便利そうなので、両方覚えておきましょう。