Giới thiệu
Jsoup là thư viện Java phổ biến nhất để parse, manipulate, extract data từ HTML. Hoạt động tốt trên Android, API dễ dùng giống jQuery (CSS selectors). Bài viết hướng dẫn setup, crawl dữ liệu, và best practices.
Các bước thực hiện
Bước 1: Tạo dự án mới
Android Studio → File → New Project → Empty View Activity.
Bước 2: Thêm dependency (build.gradle.kts)
dependencies {
// Jsoup - phiên bản ổn định mới nhất
implementation("org.jsoup:jsoup:1.17.2")
// Coroutines cho background work
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
Bước 3: Cấp quyền Internet (AndroidManifest.xml)
<uses-permission android:name="android.permission.INTERNET" />
Bước 4: Thiết kế giao diện (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<EditText
android:id="@+id/etUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nhập URL (vd: https://example.com)"
android:inputType="textUri"
android:padding="12dp"
android:background="@drawable/edittext_rounded" />
<Button
android:id="@+id/btnParse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Parse HTML" />
<TextView
android:id="@+id/tvResult"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:padding="12dp"
android:background="@android:color/darker_gray"
android:textSize="13sp"
android:scrollbars="vertical"
tools:text="Kết quả parse..." />
</LinearLayout>
Bước 5: MainActivity.kt (Jsoup + Coroutines)
package com.example.htmlparser
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.htmlparser.databinding.ActivityMainBinding
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnParse.setOnClickListener { parseHtml() }
}
private fun parseHtml() {
val url = binding.etUrl.text.toString().trim()
if (url.isEmpty()) {
Toast.makeText(this, "Vui lòng nhập URL", Toast.LENGTH_SHORT).show()
return
}
binding.tvResult.text = "Đang tải..."
binding.btnParse.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) {
try {
// Kết nối và parse HTML
val doc: Document = Jsoup.connect(url)
.userAgent("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36")
.timeout(10000) // 10 giây timeout
.followRedirects(true)
.get()
// Extract dữ liệu
val result = StringBuilder()
// 1. Title
val title = doc.title()
result.appendLine("=== TITLE ===").appendLine(title).appendLine()
// 2. Meta description
val metaDesc = doc.select("meta[name=description]").first()?.attr("content")
?: doc.select("meta[property=og:description]").first()?.attr("content")
?: "Không có meta description"
result.appendLine("=== META DESCRIPTION ===").appendLine(metaDesc).appendLine()
// 3. Tất cả links
val links: Elements = doc.select("a[href]")
result.appendLine("=== LINKS (${links.size}) ===")
links.take(20).forEachIndexed { index, link ->
val href = link.attr("abs:href") // Absolute URL
val text = link.text().takeIf { it.isNotBlank() } ?: "[no text]"
result.appendLine("$index. $text\n → $href")
}
if (links.size > 20) result.appendLine("... và ${links.size - 20} link khác")
// 4. Images
val images: Elements = doc.select("img[src]")
result.appendLine().appendLine("=== IMAGES (${images.size}) ===")
images.take(10).forEachIndexed { index, img ->
val src = img.attr("abs:src")
val alt = img.attr("alt").takeIf { it.isNotBlank() } ?: "[no alt]"
result.appendLine("$index. $alt\n → $src")
}
// 5. Headings (h1-h3)
val headings: Elements = doc.select("h1, h2, h3")
result.appendLine().appendLine("=== HEADINGS ===")
headings.forEach { heading ->
result.appendLine("${heading.tagName().uppercase()}: ${heading.text()}")
}
// Update UI trên main thread
withContext(Dispatchers.Main) {
binding.tvResult.text = result.toString()
binding.btnParse.isEnabled = true
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
binding.tvResult.text = "Lỗi: ${e.message}"
binding.btnParse.isEnabled = true
Toast.makeText(this@MainActivity, "Lỗi parse: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
}
Jsoup Selectors phổ biến (CSS Selector Syntax)
| Selector | Mô tả | Ví dụ |
|---|---|---|
tag | Element theo tag | div, a, img |
#id | Element theo ID | #main-content |
.class | Element theo class | .post-title |
[attr] | Có attribute | a[href], img[src] |
[attr=value] | Attribute bằng giá trị | input[type=submit] |
[attr^=value] | Attribute bắt đầu bằng | a[href^=https] |
[attr$=value] | Attribute kết thúc bằng | img[src$=.jpg] |
[attr*=value] | Attribute chứa | div[class*=container] |
parent > child | Direct child | ul > li |
ancestor descendant | Descendant | .post .title |
element:eq(n) | Element tại index n | tr:eq(0) (header row) |
element:contains(text) | Chứa text | div:contains(Android) |
Thao tác DOM phổ biến
// Lấy text
val text = element.text()
// Lấy HTML bên trong
val html = element.html()
// Lấy attribute
val href = element.attr("href")
val absHref = element.attr("abs:href") // Absolute URL
// Set attribute
element.attr("class", "new-class")
// Thêm/remove class
element.addClass("highlight")
element.removeClass("old")
element.toggleClass("active")
// Tạo element mới
val newDiv = Document.createElement("div").text("New content")
element.appendChild(newDiv)
// Xóa element
element.remove()
// Clean HTML (chỉ giữ tags cho phép)
val cleanHtml = Jsoup.clean(dirtyHtml, Safelist.basic()) // Chỉ giữ b, i, u, a...
Xử lý HTML không chuẩn (Malformed HTML)
Jsoup tự động fix HTML broken (thiếu closing tag, nested sai...). Nếu cần strict: Parser.xmlParser().
Best Practices & Lưu ý
- Luôn chạy trên background thread (IO Dispatcher) - Jsoup connect/network blocking.
- Set
userAgentgiả lập browser - nhiều site block request không có UA. - Set
timeout(10-30s) tránh treo app. - Dùng
attr("abs:href")để lấy absolute URL tự động resolve relative path. - Giới hạn kết quả (take, limit) tránh OOM khi parse trang lớn.
- Respect
robots.txtvàrate limiting- không crawl quá nhanh. - Cache kết quả (Room/DataStore) tránh parse lại cùng URL.
- Jsoup không chạy JavaScript - nội dung render bằng JS (React, Vue) sẽ không thấy. Cần headless browser (Playwright, Puppeteer) hoặc WebView evaluate JS.
So sánh Jsoup vs WebView evaluate JS
| Đặc điểm | Jsoup | WebView + evaluateJavascript |
|---|---|---|
| JS Rendering | Không | Có |
| Tốc độ | Rất nhanh | Chậm (cần load page) |
| Memory | Thấp | Cao (WebView engine) |
| Phức tạp | Thấp | Cao |
| Dùng cho | Static HTML, SEO content, API | SPA, dynamic content, AJAX |