HTMLをパースするライブラリ
今回あるサイトのHTTPのレスポンスのHTMLをパースする必要がありました。HTMLは基本はXMLなので色々なパーサがありますが、jsoupを使ったら意外とよかったのでそれを紹介したいと思います。
そもそも・・・なんでHTMLをパースする必要があるのか?
今、開発しているアプリがAndroid端末内でCSVファイルを作ってそれをWebサイトにアップロードする仕組みなんですが、そのWebサイトがWEB APIを提供していなくて、(おそらく、10年以上前に作った、そのまま)普通のブラウザからHTMLでのアップロードのインタフェースしか提供していません。
- ログイン。
- CSVを添付して送信。この時正常か、エラーになる
- 送信結果を過去の履歴も含めて取得
- ログアウト
という、一連の流れになっています。こんなの、APIあれば一発なのに・・・と思うのが普通でしょうが。各リクエスト/レスポンスのやり取りで、ブラウザから来る前提なので、結果のレスポンスは全てHTMLです。そのHTMLの中の特定のタグの値を見て正常か、失敗かを判定する必要があります。
jsoupを使って見る
依存ライブラリの追加
まず、ライブラリの依存性を追加します
dependencies {
・・・
implementation 'org.jsoup:jsoup:1.15.3'
}
パース機能の呼び出し方
パースはString型を引数にorg.jsoup.Jsoup#parseを呼ぶと、org.jsoup.nodes.Document型が返ってきます。簡単ですね。
val doc = Jsoup.parse(response)
このorg.jsoup.nodes.Document型がHTMLの全ての情報がパースされて格納されています。ここなら目的の箇所を探し当てて、値を取得します。この方法ですが、いくつかあります。
- Document#getElementById(String id)
HTMLのid="・・・"で探す - Document#getElementsByTag(String tag)
Tagで探す。"table"とか、"input"とか - Document#getElementsByClass(String className)
HTMLのclass="・・・"で探す - Document#getElementsByAttribute(String key)
属性で探す。"href"とか。
この結果にorg.jsoup.select.Elements型が返ってきます。上のいずれの方法で探しても、HTMLの中に複数個所該当する場合があります。
org.jsoup.select.Elementsの中にorg.jsoup.nodes.Elementを複数繰り返しで持っています。
elements.forEach { element ->
println(element.text())
}
HTMLなので当然階層構造になっているので、org.jsoup.nodes.Elementには子要素が存在します。
val elements = table.children()
子要素も複数のElementの繰り返しの可能性があるので、org.jsoup.select.Elementsで返ってきます。これらをforEach等で繰り返しながら、目的のElementを探し当てます。
目的のElementを探し当てたら、そのElementのデータを取得します。これも取得するデータによってメソッドが分かれています。
- Element#attr(String key) 属性
- Element#attributes() 全ての属性
- Element#id() id="・・・"
- Element#className() class="・・・" 複数の場合はスペース区切り
- Element#classNames() class="・・・" Setで返ってくる
- Element#text() テキストの部分
- Element#html() HTMLの形で返ってくる
- Element#outerHtml() 外側のHTMLが返ってくる
- Element#data() データ、例えばScriptタグの内側
- Element#tag() タグが返ってくる。Tag型
- Element#tagName() タグ名が返ってくる。String型
例えば、探し当てた目的のElementが
<input type="hidden" id="xxxx_key" name="xxxxx_key" value="20221216195944675">
だったら、value="・・・" の部分を取得したければ、
var val = element.text()
と言った感じになります。
joupを使って、直接送信
jsoupは上の例のようにHTMLパーサとして使えますが、HTTPで送信する機能も持っています。ただし、非同期ではなく同期なので、Androidから送信する場合は非同期の部分はcoroutineなり、RxJavaなりで自分で実装する必要があります。
val document = Jsoup.connect("http://www.google.co.jp").get()
println(document.html())
リクエストパラメータを付けることもできます。
val doc = Jsoup.connect("http://example.com")
.data("query", "Java")
.userAgent("Mozilla")
.cookie("auth", "token")
.timeout(3000)
.post()