什麼是 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 檔案加入你的專案中,但更好的作法是使用 Maven 或 Gradle 的自動化工具。
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 解析函式庫來處理,就能取得內容了。
我要留言
留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。