使用 jsoup 解析網頁 HTML

HTML 5

如果你需要解析網頁內容,可以使用 jsoup 這個 Java 函式庫來幫助你解析網頁 HTML。

什麼是 jsoup

早些年我寫過兩篇文章,一篇是關於如何使用 GET/PSOT 方法來取得網頁資料,另一篇則是如何解析網頁,這兩篇所提到的作法都偏低階,使用上不是那麼有效率,jsoup 可以讓你更快、更容易的做到下載及解析網頁的任務。

jsoup 是一個 Java library ,它提供了方便易用的 API 來解析 HTML,使用的方法和 CSS 的選擇器或 jquery 有點像。

jsoup 可以做到:

  • 從網路連結 (URL)、檔案及字串中解析 HTML
  • 使用 DOM 樹遍歷或 CSS 的選擇器來尋找資料
  • 操縱 HTML 元素、屬性和文字
  • 避免 XSS 攻擊
  • 輸出簡潔的 HTML

如何使用 jsoup

1. 安裝

目前為 1.11.3 版,你可以直接下載 jsoup-1.11.3.jar 檔案加入你的專案中,但更好的作法是使用 MavenGradle 的自動化工具。

Maven

<dependency>
  <!-- jsoup HTML parser library @ https://jsoup.org/ -->
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.11.3</version>
</dependency>

Gradle

// jsoup HTML parser library @ https://jsoup.org/
compile 'org.jsoup:jsoup:1.11.3'

2. 解析完整 HTML 字串

先從最簡單的字串開始,假設你有一段 HTML 字串,如何解析它呢?請看以下範例:

註:本篇程式碼使用 Kotlin。

import org.jsoup.Jsoup

fun parseString() {
    val html = """
        <!DOCTYPE html>
        <html lang="zh-tw">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>JSOUP Demo</title>
        </head>
        <body>
            <p>This is jsoup demo by blog.tonycube.com</p>
        </body>
        </html>
    """.trimIndent()

    val doc = Jsoup.parse(html)
    val tagP = doc.select("p").firstOrNull()
    println(tagP?.text() ?: "") // 結果: This is jsoup demo by blog.tonycube.com
}

Jsoup.parse(String html) 方法讓你傳入一個 HTML 字串,它會解析該字串然後回傳 Document 物件,你就可以使用該物件來取得你想要的節點元素。

select(String cssQuery) 方法讓你以 CSS 選擇器的方式指定要選取的 HTML 元素,這裡我們要找 <p></p> ,該方法會回傳 Elements 物件,這個物件是 ArrayList<Element> 的子類別,也就是說它是集合物件,可能有零個到多個結果,但是我們只要第一個,於是用 firstOrNull() 來取得第一個元素否則就是 Null

如果你使用 Java ,你得判斷 tagP 是否為 Null ,才能做接下來的事,Kotlin 則使用 ? 來解決這個問題,只要它不是 Null,就使用 text() 方法取出該元素的文字。

你只要專注在 select() 這個方法,使用對的選擇器去找出你要的元素即可,剩下的就只是取資料而已。

3. 解析部份 HTML 字串

有時候你取得的字串並不是完整的 HTML 內容,而是部份元素組成,例如使用者手動輸入的內容

<div><b>Hello~~</b>I am Tony.

這時候你可以這麼用

fun parseBodyFragment() {
    val html = """<div><b>Hello~~</b>I am Tony."""
    val doc = Jsoup.parseBodyFragment(html)
    val body = doc.body()
    println(body)
}

結果:

<body>
 <div>
  <b>Hello~~</b>I am Tony.
 </div>
</body>

Jsoup.parseBodyFragment(String bodyHtml) 方法會將你傳入的字串插入它新增的 <body></body> 元素中。而且它可以避免錯誤的元素結構,你有沒有發現例子中少了結尾的 </div>,但輸出的結果卻是自動補上了。如果你單純使用 parse() 方法,它只會原封不動的輸出。

Document.body() 方法只會取出 <body></body> 元素中的內容。

重要!如果你要接受使用者傳入的 HTML 字串,請使用 clean(String bodyHtml, Whitelist whitelist) 來清除不必要的內容,避免被跨網站指令碼攻擊 (Cross-site scripting, XSS)

4. 解析由網址取得的 HTML

現在你可以從網路上下載某個網頁的 HTML,藉由解析該網頁來取得你想要的資料。

// 這個範例會取得我的部落格首頁的文章標題清單
fun parseHtmlFromUrl() {
    val doc = Jsoup.connect("https://blog.tonycube.com/").get()
    val tagTitleList = doc.select("h1.blog-post-title")
    if (tagTitleList.isEmpty()) {
        println("找不到文章標題")
    } else {
        tagTitleList.map { it.text() }.forEach(::println)
    }
}

使用 connect(String url)方法建立一個 Connection,然後使用 get()方法去取得網頁的 HTML。如果發生錯誤會丟出 IOException ,你可以自行決定是否補捉並處理。

Connection 是一個介面,可以讓你串接多個實作該介面的方法,例如當你想使用 post()方法時,可以這麼用:

val doc = Jsoup.connect("https://abc.demo.test")
 .data("query", "Kotlin")
 .userAgent("Mozilla")
 .timeout(10000)
 .post()

data() 方法可以讓你建立 post 方法所需的資料,其他可用的方法可以在 HttpConnection 查到。

5. 解析由檔案取得的 HTML

除了從網路上下載網頁,你也可以解析本地端的 HTML 檔案。

fun parseHtmlFromFile() {
    val file = File("./test.html")
    val doc = Jsoup.parse(file, "UTF-8")
    // ...略
}

parse(File in, String charsetName) 方法允許你傳入一個檔案,然後它會回傳解析後的文件。

6. 如何提取資料

6.1 使用 DOM

假設你有一個 HTML 檔案如下:

<!DOCTYPE html>
<html lang="zh-tw">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Test</title>
</head>
<body>
    <div id="content">
        <h1>Menu</h1>
        <ul>
            <li><a href="a.html">AAA</a></li>
            <li><a href="b.html">BBB</a></li>
            <li><a href="c.html">CCC</a></li>
            <li><a href="d.html">DDD</a></li>
        </ul>
        <a href="e.html">EEE</a>
    </div>
</body>
</html>

取得連結的方法如下:

fun parseHtmlFromFileByDOM() {
    val file = File("./test.html")
    val doc = Jsoup.parse(file, "UTF-8")
    val tagContent = doc.getElementById("content")
    val links = doc.getElementsByTag("a")
    links.forEach {
        val href = it.attr("href")
        val text = it.text()
        println("$text - $href")
    }
}

結果:

AAA - a.html
BBB - b.html
CCC - c.html
DDD - d.html
EEE - e.html

你可以在 Element 類別的說明文件中查找 getElementXXX 相關的方法,jsoup 可以讓你非常靈活的在各個節點中移動。當你找到想要的節點後,接下來就是取得資料了,如果需要節點的屬性,可以用 attr(String attributeKey),如果要取得內容文字則使用 text()html() 方法。

6.2 使用 CSS Selector

這個方法比較簡單好用,但是首先你得先瞭解 CSS 選擇器如何使用,示範如下:

fun parseHtmlFromFileBySelector() {
    val file = File("./test.html")
    val doc = Jsoup.parse(file, "UTF-8")

    val tagContent = doc.select("#content").first()
    val links = tagContent.select("li > a")
    links.forEach {
        val href = it.attr("href")
        val text = it.text()
        println("$text - $href")
    }
}

結果:

AAA - a.html
BBB - b.html
CCC - c.html
DDD - d.html

select("li > a") 表示只取 <li></li> 之下的 <a></a> 節點,所以 EEE 被忽略。# 表示取 id 屬性,. 表示取 class 屬性,其他選擇器的用法請自行瞭解,如果不熟,只要先知道幾個常用的功能即可。

7. 如何下載 JSON 內容

雖然 jsoup 不能解析 JSON 內容,但是你可以利用它來下載 JSON 格式的內容。

fun fetchJson() {
    val json = Jsoup.connect("https://jsonplaceholder.typicode.com/todos/1")
        .ignoreContentType(true)
        .execute()
        .body()
    println(json)
}

ignoreContentType(true) 用來取消內容格式的檢查,因為 JSON 的內容不是 HTML,所以如果沒有將這個方法設為 true,就會出現資料型態的例外錯誤;因為使用 get() 會回傳含有 HTML 的內容,這樣不符合 JSON 格式,所以改用 execute()方法,並且直接使用 body()方法來取得字串內容。

最後將 JSON 格式的字串,使用其他 JSON 解析函式庫來處理,就能取得內容了。

參考資料



本文網址:https://blog.tonycube.com/2018/12/jsoup-html.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

我要留言

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。