🎯 Trình duyệt đám mây tùy chỉnh, chống phát hiện được hỗ trợ bởi Chromium tự phát triển, thiết kế dành cho trình thu thập dữ liệu webtác nhân AI. 👉Dùng thử ngay
Quay lại blog

Java Web Scraping Với jsoup: Lấy, Phân Tích và Hiển Thị

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

04-Aug-2026

TL;DR:

  • jsoup là một trình phân tích HTML Java với một động cơ bộ chọn CSS, và nó cung cấp riêng một client HTTP, vì vậy một trình thu thập hoạt động chỉ cần một phụ thuộc và khoảng hai mươi dòng.
  • jsoup chỉ giữ lại 2 MiB đầu tiên của một phản hồi theo mặc định. Trên một trang dài 2,235,648 byte, nó giữ chính xác 2,097,152 byte, loại bỏ 68 trong số 255 mục tham chiếu, mất một phần hoàn toàn và không loại bỏ gì cả.
  • attr("href") trả về href chính xác như được viết trong HTML. attr("abs:href") giải quyết nó theo URI cơ sở của tài liệu.
  • jsoup không thực thi JavaScript. Trên một trang được render bởi client, mã bộ chọn tương tự tìm thấy 0 mục trực tiếp và 10 mục khi HTML đến đã được render trước.
  • Định tuyến việc thu thập thông qua Scrapeless Universal Scraping API chỉ thay đổi phương tiện — phương pháp phân tích không bị ảnh hưởng.
  • Kế hoạch miễn phí của Scrapeless đủ để thực hiện mọi yêu cầu trong hướng dẫn này.

Java là nơi nhiều dữ liệu được thu thập cuối cùng. Nếu dịch vụ tiêu thụ dữ liệu là một ứng dụng Spring hoặc Quarkus, việc giữ cho quá trình extraction trong cùng một JVM loại bỏ ranh giới ngôn ngữ, một bước tuần tự hóa và một mục tiêu triển khai thứ hai.

Thư viện giúp điều đó thực tiễn là jsoup. Nó phân tích HTML từ thế giới thực giống như một trình duyệt — đóng các thẻ chưa đóng, sửa nesting và chuẩn hóa các thuộc tính, tuân theo các quy tắc xử lý lỗi trong specification phân tích HTML — và sau đó công khai kết quả thông qua API bộ chọn CSS.

Hướng dẫn này xây dựng một trình thu thập hoạt động chống lại hai trang web trực tiếp, sau đó đo lường hai hành vi quyết định xem trình thu thập có đúng hay không: cái gì jsoup âm thầm loại bỏ trên một trang lớn, và cái gì nó trả về trên một trang được render trong trình duyệt.

Những gì jsoup cung cấp cho bạn

jsoup trước tiên là một bộ phân tích và thứ hai là một client HTTP. Jsoup.connect(url) trả về một builder yêu cầu lưu loát; .get() thực hiện yêu cầu và trả về một Document mà bạn truy vấn bằng các bộ chọn.

java Copy
String url = "https://books.toscrape.com/";
Document doc = Jsoup.connect(url).get();
String title = doc.selectFirst("h1").text();

Document đó là một cây đã phân tích thay vì một chuỗi, vì vậy một trang bị lỗi vẫn tạo ra một cấu trúc truy vấn được. Nó cũng mang URI cơ sở mà nó được lấy từ đó, điều này cho phép giải quyết URL tuyệt đối có thể sau này.

Những gì jsoup không làm là chạy các script. Nó không có engine JavaScript và không có vòng lặp sự kiện DOM. Bất cứ điều gì mà server gửi là những gì bạn nhận được để phân tích.

Thiết lập Dự án

Một phụ thuộc bao phủ công việc phân tích. Gson ở đây chỉ để đọc bao bì JSON trong phần cuối; nếu bạn bỏ qua phần đó bạn có thể xóa nó.

xml Copy
<dependencies>
  <dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.21.1</version>
  </dependency>
  <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.14.0</version>
  </dependency>
</dependencies>

Ghim plugin biên dịch một cách rõ ràng. Maven 3.8.7 vẫn liên kết maven-compiler-plugin 3.1 theo mặc định, điều này bỏ qua maven.compiler.release và dừng lại với thông báo Source option 5 is no longer supported:

xml Copy
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.15.0</version>
    </plugin>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>exec-maven-plugin</artifactId>
      <version>3.5.0</version>
      <configuration>
        <mainClass>com.example.Scraper</mainClass>
      </configuration>
    </plugin>
  </plugins>
</build>

Với maven.compiler.release đặt thành 17, mã dưới đây sẽ biên dịch trên bất kỳ JDK hiện tại nào. Chạy xác minh đã sử dụng OpenJDK 21.0.11.

Lấy một Trang và Phân Tích Nó

Đặt một user agent và một thời gian chờ trên mọi yêu cầu. Đại lý mặc định của jsoup xác định bản thân là jsoup, và nhiều trang web thay đổi phản hồi của họ chỉ dựa trên điều đó.

java Copy
static final String USER_AGENT =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    + "(KHTML, như Gecko) Chrome/140.0.0.0 Safari/537.36";

static Document fetch(String url) throws IOException {
    return Jsoup.connect(url)
        .userAgent(USER_AGENT)
        .timeout(30_000)
        .get();
}

timeout là mili giây và áp dụng cho cả kết nối và đọc. Một get() vượt quá nó gây ra SocketTimeoutException; một trạng thái không phải 2xx gây ra HttpStatusException, mang theo mã trạng thái.

Chọn Dữ liệu

Các bộ chọn là CSS tiêu chuẩn. select trả về mọi kết quả phù hợp dưới dạng một bộ sưu tập Elements; selectFirst trả về một Element hoặc null.

java Copy
record Book(String title, String price, String url) {}

static List<Book> books(Document doc) {
    List<Book> found = new ArrayList<>();
    for (Element card : doc.select("article.product_pod")) {
        Element link = card.selectFirst("h3 > a");
        found.add(new Book(
            link.attr("title"),
            card.selectFirst("p.price_color").text(),
Copy
link.attr("abs:href")));
    }
    return found;
}

So với danh mục trực tiếp, điều này trả về 20 cuốn sách, cuốn đầu tiên là A Light in the Attic với giá £51.77.

Tiền tố abs: trên dòng cuối cùng đang thực hiện công việc thực sự. Đoạn mã nguồn đọc:

text Copy
catalogue/a-light-in-the-attic_1000/index.html

attr("href") cho bạn chuỗi đó theo đúng nghĩa. Tiền tố tên thuộc tính bằng abs: giải quyết nó theo URI cơ sở của tài liệu bằng cách sử dụng thuật toán trong Tiêu chuẩn URL của WHATWG, tạo ra:

text Copy
https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html

Một công cụ thu thập thông tin mà lưu giá trị thô hoạt động tốt cho đến khi có thứ gì đó khác cố gắng truy xuất nó.

Giới Hạn Khiến Bạn Mất Dữ Liệu

jsoup giới hạn độ dài phản hồi mà nó sẽ đọc. Mặc định là 2 MiB, được tài liệu trên giao diện yêu cầu jsoup. Đến điểm đó, nó dừng đọc và phân tích những gì mà nó có. Nó không nâng bất kỳ ngoại lệ nào, không ghi lại cảnh báo, và không để lại cờ nào trên Document để nói rằng nội dung đã bị cắt ngắn.

Sử dụng execute() thay vì get() khi bạn muốn xem phản hồi trước khi phân tích nó:

java Copy
Connection.Response capped = Jsoup.connect(big)
    .userAgent(USER_AGENT).timeout(60_000).execute();
Connection.Response whole = Jsoup.connect(big)
    .userAgent(USER_AGENT).timeout(60_000).maxBodySize(0).execute();

Chạy trên một trang so sánh Wikipedia có kích thước 2,235,648 byte, hai phản hồi khác nhau đúng bằng giới hạn:

text Copy
trạng thái http, giới hạn mặc định: 200
byte nhận được, giới hạn mặc định: 2097152
byte nhận được, maxBodySize(0): 2235648
dòng bảng, giới hạn mặc định: 544
dòng bảng, maxBodySize(0): 544
mục tham khảo, giới hạn mặc định: 187
mục tham khảo, maxBodySize(0): 255
phần cuối cùng, giới hạn mặc định: Tham khảo
phần cuối cùng, maxBodySize(0): Liên kết bên ngoài

Cả hai lần chạy đều trả về HTTP 200. Cả hai đều tạo ra một Document. Các bảng so sánh mà trang tồn tại cho đã được đưa vào giống hệt nhau, 544 dòng theo cả hai cách, bởi vì chúng nằm gần đầu tài liệu.

Mọi thứ bên dưới phần cắt đều đơn giản không có. Danh sách tham khảo mất 68 trong số 255 mục của nó, và phần cuối cùng Liên kết bên ngoài hoàn toàn không tồn tại trong cây bị cắt ngắn. Một công cụ thu thập thông tin đọc các bảng sẽ không bao giờ nhận ra; một công cụ thu thập thông tin thu thập trích dẫn sẽ âm thầm báo cáo thiếu một phần tư và vẫn nhìn khỏe mạnh.

maxBodySize(0) loại bỏ giới hạn. Đặt nó một cách có chủ đích thay vì reflex — một lần đọc không giới hạn trên một phản hồi không mong đợi là một vấn đề riêng — nhưng hãy đặt nó có ý thức, và so sánh với Content-Length mà máy chủ báo cáo, mà tiêu chuẩn nghĩa HTTP định nghĩa là số lượng octet của nội dung.

Khi Trang Được Hiển Thị Trong Trình Duyệt

https://quotes.toscrape.com/js/ đánh dấu ranh giới. Nó phục vụ các báo giá của mình dưới dạng một mảng JavaScript và xây dựng DOM ở phía khách hàng, và jsoup lấy nó thành công:

text Copy
byte html được lấy trực tiếp: 5479
các báo giá được tìm thấy bởi jsoup:       0

5,479 byte, HTTP 200, không có kết quả. Thẻ markup mà jsoup nhận được thực sự không chứa yếu tố div.quote — chúng được tạo ra sau khi đoạn script chạy, và jsoup không có engine chạy script.

Hầu hết các hướng dẫn trả lời điều này bằng cách chuyển sang một công cụ tự động hóa trình duyệt, có nghĩa là phải viết lại mã trích xuất nữa. Điều đó là trade-off giống nhau mà Cheerio và Puppeteer đứng ở hai bên trong Node, và nó tốn cùng một thứ trong Java. Giải pháp hẹp hơn là chỉ thay đổi cách mà HTML đến. Công việc đó thuộc về API Thu Thập Dữ Liệu Toàn Cầu Scrapeless. Nó hiển thị trang và trả về HTML thu được dưới dạng chuỗi, mà bạn chuyển cho cùng một trình phân tích.

HttpClient tích hợp sẵn của JDK HttpClient là đủ — không cần phụ thuộc vào HTTP:

java Copy
static String render(String url) throws IOException, InterruptedException {
    JsonObject input = new JsonObject();
    input.addProperty("url", url);
    input.addProperty("proxy_country", "US");
    input.addProperty("js_render", true);

    JsonObject payload = new JsonObject();
    payload.addProperty("actor", "unlocker.webunlocker");
    payload.add("input", input);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.scrapeless.com/api/v2/unlocker/request"))
        .header("Content-Type", "application/json")
        .header("x-api-token", System.getenv("SCRAPELESS_API_KEY"))
        .timeout(Duration.ofSeconds(180))
java Copy
.POST(HttpRequest.BodyPublishers.ofString(payload.toString(), StandardCharsets.UTF_8))
        .build();

    HttpResponse<String> response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
        throw new IOException("unlocker trả về HTTP " + response.statusCode());
    }
    return JsonParser.parseString(response.body())
        .getAsJsonObject().get("data").getAsString();
}

Envelope phản hồi là {"code": ..., "data": "<html được render>"}. Phân tích chuỗi đó với Jsoup.parse(html, url) — việc truyền URL sẽ thiết lập URI cơ sở, vì vậy abs:href tiếp tục hoạt động — và chạy phương thức chọn lọc giống hệt:

java Copy
static List<String> quotes(Document doc) {
    List<String> found = new ArrayList<>();
    for (Element quote : doc.select("div.quote")) {
        found.add(quote.selectFirst("span.text").text()
            + " -- " + quote.selectFirst("small.author").text());
    }
    return found;
}
text Copy
bytes html được render:            8940
quotes được tìm thấy bởi parser giống nhau: 10

Phương thức giống nhau, selector giống nhau, cùng API Document. Điều duy nhất đã thay đổi là nơi mà 8,940 bytes đến từ. Giữ khóa API của bạn trong môi trường, như SCRAPELESS_API_KEY ở trên, thay vì trong mã nguồn. Hướng dẫn hướng dẫn bắt đầu với Universal Scraping API bao gồm các tham số yêu cầu còn lại.

Bắt đầu chỉ mất một phút — tạo một tài khoản Scrapeless miễn phí và gói miễn phí bao gồm mọi thứ trong hướng dẫn này.

Chạy Nó

Với lớp đã được lắp ghép, một lệnh biên dịch và chạy nó:

bash Copy
export SCRAPELESS_API_KEY="khóa-api-của-bạn"
mvn -q -B compile exec:java

Đầu ra đầy đủ từ việc xác minh chạy:

text Copy
jsoup 1.21.1 | java 21.0.11
--- trang tĩnh, phân tích trực tiếp ---
số sách trên trang 1: 20
tiêu đề đầu tiên: A Light in the Attic
giá đầu tiên: £51.77
url đầu tiên:   https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
href như đã viết trong HTML: catalogue/a-light-in-the-attic_1000/index.html
--- giới hạn kích thước cơ thể mặc định ---
trạng thái http, giới hạn mặc định: 200
bytes nhận được, giới hạn mặc định: 2097152
bytes nhận được, maxBodySize(0): 2235648
hàng trong bảng, giới hạn mặc định: 544
hàng trong bảng, maxBodySize(0): 544
mục tham chiếu, giới hạn mặc định: 187
mục tham chiếu, maxBodySize(0): 255
phân đoạn cuối, giới hạn mặc định: Tham khảo
phân đoạn cuối, maxBodySize(0): Liên kết bên ngoài
--- trang được render bằng javascript ---
bytes html được lấy trực tiếp: 5479
quotes tìm thấy bởi jsoup:       0
--- cùng trang qua Universal Scraping API ---
bytes html được render:            8940
quotes tìm thấy bởi parser giống nhau: 10

Khắc Phục Lỗi

Tùy chọn Nguồn 5 không còn được hỗ trợ. Maven đã ràng buộc maven-compiler-plugin mặc định của nó chứ không phải của bạn. Ghim phiên bản plugin trong <build><plugins> như đã chỉ ra ở trên.

selectFirst trả về null và dòng tiếp theo ném NullPointerException. Selector không khớp với bất cứ điều gì. Kiểm tra phần tử với HTML mà jsoup thật sự nhận được — doc.html() — không so với những gì trình duyệt kiểm tra hiển thị, đó là DOM sau khi chạy kịch bản.

Số lượng thấp hơn so với trang hiển thị. So sánh response.bodyAsBytes().length với Content-Length của máy chủ. Nếu chúng khớp ở 2,097,152, thì giới hạn kích thước cơ thể là nguyên nhân.

HttpStatusException: 403. Máy chủ đã từ chối yêu cầu thay vì phân tích. Đặt một tác nhân người dùng thực tế trước; nếu trang cũng yêu cầu render, điểm chuyển đổi ở trên áp dụng.

Mojibake trong văn bản đã trích xuất. jsoup đọc charset từ header Content-Type, sau đó từ thẻ meta. Khi một máy chủ không khai báo cả hai, hãy truyền mã hóa một cách rõ ràng cho Jsoup.parse(InputStream, String, String).

Kết Luận

Đối với HTML tĩnh, jsoup và JDK là toàn bộ công cụ: một phụ thuộc, một yêu cầu trôi chảy, một API selector CSS, và một cây đã phân tích chịu được các markup xấu. Hai hành vi quyết định kết quả có đáng tin cậy hay không đều vô hình theo mặc định — giới hạn kích thước cơ thể 2 MiB trả về một tài liệu một phần trông khỏe mạnh, và bộ kết quả trống trên trang được render phía khách.

Cả hai đều dễ kiểm tra. So sánh bytes nhận được với Content-Length, và so sánh số lượng của một selector với những gì trang hiển thị. Khi kiểm tra thứ hai thất bại vì markup không đến, cách khắc phục là thay đổi phương tiện truyền tải và để yên parser.
Sẵn sàng thử nghiệm chưa? Bắt đầu với gói miễn phí của Scrapeless và xem giá cả hiện tại cho khối lượng cao hơn.

Câu hỏi thường gặp (FAQ)

H: jsoup có đủ không, hay tôi cần Selenium?

Đối với HTML được xử lý trên máy chủ, jsoup một mình đủ và nhanh hơn rất nhiều, vì nó không bao giờ khởi động trình duyệt. Bạn chỉ cần bước xử lý khi dữ liệu được tạo ra bởi JavaScript — và như phép đo ở trên cho thấy, bước đó có thể là một cuộc gọi HTTP trả về HTML đã được xử lý thay vì một trình duyệt đầy đủ trong quy trình của bạn.

H: Làm thế nào để tôi biết liệu một trang có cần JavaScript trước khi viết bộ chọn?

In doc.html() từ một liên kết jsoup đơn giản và tìm kiếm một giá trị mà bạn có thể thấy trên trang. Nếu giá trị đó vắng mặt trong chuỗi đó nhưng hiển thị trong trình duyệt, nó đang được xử lý phía khách hàng. So sánh số lượng bộ chọn với số lượng hiển thị sẽ phát hiện ra điều tương tự.

H: Lý do thực tiễn nào để thay đổi maxBodySize?

Mặc định giữ 2 MiB, điều này nhỏ hơn nhiều trang danh sách, lưu trữ và so sánh. Nếu mục tiêu của bạn vượt quá giới hạn này, mọi thứ sau điểm cắt sẽ không có trong cây phân tích mà không có lỗi nào được báo cáo. Đặt maxBodySize(0) khi bạn cần toàn bộ tài liệu, và so sánh số byte nhận được với Content-Length để biết khi nào điều đó có ý nghĩa.

H: jsoup có xử lý HTML không hợp lệ không?

Có. Nó áp dụng cùng một quy tắc khôi phục lỗi như trình duyệt, do đó các thẻ không đóng và các phần tử lồng nhau sai vẫn tạo ra một cây có thể truy vấn. Đó là lý do chính để ưu tiên nó hơn một trình phân tích XML nghiêm ngặt cho các trang thực tế.

H: Ch scraping bằng Java có hợp pháp không?

Ngôn ngữ không liên quan đến câu hỏi pháp lý. Điều quan trọng là dữ liệu bạn thu thập, điều khoản của trang web, các chỉ thị robot mà bạn tôn trọng, và khu vực pháp lý mà bạn hoạt động. Hạn chế thu thập trên các trang công cộng, giữ lưu lượng yêu cầu ở mức khiêm tốn, và tìm kiếm tư vấn pháp lý cho bất kỳ điều gì liên quan đến dữ liệu cá nhân.

Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến ​​cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.

Bài viết phổ biến nhất

Danh mục