Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách parse HTML trên Android với Jsoup

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)

SelectorMô tảVí dụ
tagElement theo tagdiv, a, img
#idElement theo ID#main-content
.classElement theo class.post-title
[attr]Có attributea[href], img[src]
[attr=value]Attribute bằng giá trịinput[type=submit]
[attr^=value]Attribute bắt đầu bằnga[href^=https]
[attr$=value]Attribute kết thúc bằngimg[src$=.jpg]
[attr*=value]Attribute chứadiv[class*=container]
parent > childDirect childul > li
ancestor descendantDescendant.post .title
element:eq(n)Element tại index ntr:eq(0) (header row)
element:contains(text)Chứa textdiv: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 userAgent giả 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.txt và 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ểmJsoupWebView + evaluateJavascript
JS RenderingKhôngCó
Tốc độRất nhanhChậm (cần load page)
MemoryThấpCao (WebView engine)
Phức tạpThấpCao
Dùng choStatic HTML, SEO content, APISPA, dynamic content, AJAX