🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

Java网络爬虫与jsoup:获取、解析和渲染

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

04-Aug-2026

TL;DR:

  • jsoup 是一个带有 CSS 选择器引擎的 Java HTML 解析器,它自带一个 HTTP 客户端,因此一个有效的抓取器只需依赖一个库和大约二十行代码。
  • jsoup 默认情况下只保留响应的前 2 MiB。在一个 2,235,648 字节的页面上,它恰好保留了 2,097,152 字节,丢弃了 255 个引用条目中的 68 个,失去了一个完整的部分,没有抛出任何内容。
  • attr("href") 返回与 HTML 中写的完全相同的 href。attr("abs:href") 则将其解析为文档的基本 URI。
  • jsoup 不会执行 JavaScript。在一个客户端渲染的页面上,相同的选择器代码直接找到 0 个项,而在 HTML 预渲染到达时找到 10 个项。
  • 通过 Scrapeless Universal Scraping API 进行获取路由仅改变运输——解析方法未被触及。
  • Scrapeless 免费计划足以运行本指南中的每个请求。

Java 是许多抓取数据的最终归宿。如果消耗数据的服务是 Spring 或 Quarkus 应用程序,将提取保持在同一 JVM 中可以消除语言边界、序列化步骤和第二个部署目标。

实现这一切的库是 jsoup。它以浏览器的方式解析真实世界的 HTML——闭合未闭合的标签、修复嵌套以及规范化属性,遵循 HTML 解析规范 中的错误处理规则——然后通过 CSS 选择器 API 暴露结果。

本指南构建了一个针对两个实时站点的有效抓取器,并测量了决定抓取器是否正确的两个行为:jsoup 在大型页面上默默丢弃了什么,以及在浏览器中呈现的页面返回了什么。

jsoup 提供的功能

jsoup 首先是一个解析器,其次是一个 HTTP 客户端。Jsoup.connect(url) 返回一个流式请求构建器;.get() 执行请求并返回一个你可以用选择器查询的 Document

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

Document 是一个解析后的树,而不是字符串,因此格式错误的页面仍然会产生可查询的结构。它还携带了其获取的基本 URI,这使得后续绝对 URL 解析成为可能。

jsoup 不做的是运行脚本。它没有 JavaScript 引擎,也没有 DOM 事件循环。服务器发送的任何内容都是你要解析的内容。

设置项目

一个依赖项覆盖解析。Gson 仅用于读取最后一节中的 JSON 封装;如果你跳过该节,可以去掉它。

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>

显式锁定编译器插件。Maven 3.8.7 默认情况下仍绑定 maven-compiler-plugin 3.1,这会忽略 maven.compiler.release 并报错 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>

maven.compiler.release 设置为 17,该代码在任何当前 JDK 上编译。验证运行使用的是 OpenJDK 21.0.11。

获取页面并解析

在每个请求上设置用户代理和超时。jsoup 的默认代理将自己标识为 jsoup,而许多网站的响应则仅在此基础上发生变化。

java Copy
static final String USER_AGENT =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    + "(KHTML, like 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 为毫秒,并适用于连接和读取。超时的 get() 将引发 SocketTimeoutException;非 2xx 状态将引发 HttpStatusException,并携带代码。

选择数据

选择器是标准 CSS。select 返回每个匹配项作为 Elements 集合;selectFirst 返回一个 Element 或 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(),
对实时目录,这返回了20本书,第一本是《阁楼里的光》,售价为51.77英镑。

最后一行的 "abs:" 前缀确实在发挥作用。源中的锚点为:

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

attr("href") 会逐字返回该字符串。在属性名称前加上 abs: 会根据文档的基本 URI 使用 WHATWG URL 标准 中的算法解析它,产生:

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

一个存储原始值的抓取器在某天其他东西尝试取用它时会出现问题。

会让你失去数据的限制

jsoup 对它将读取的响应体做了限制。默认限制为 2 MiB,这在 jsoup 请求接口 上有文档说明。超出该点后,它会停止读取并解析它所拥有的内容。它不会引发异常、记录警告,也不会在 Document 上留下任何标志来说明主体被截断。

当你想在解析之前查看响应时,使用 execute() 而不是 get()

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();

在对一个 2,235,648 字节的 Wikipedia 比较页面进行操作时,这两个响应的差异正好是限制值:

text Copy
http 状态,默认限制:200
接收的字节数,默认限制:2097152
接收的字节数,maxBodySize(0):2235648
表格行,默认限制:544
表格行,maxBodySize(0):544
参考条目,默认限制:187
参考条目,maxBodySize(0):255
最后部分,默认限制:References
最后部分,maxBodySize(0):External links

两次运行都返回了 HTTP 200。两者都生成了 Document。该页面存在的比较表格内容一模一样,都是 544 行,因为它们位于文档的顶部附近。

所有在截断以下的内容则是简单缺失。参考列表失去了 255 个条目中的 68 个,而最终的 External links 部分在截断的树中根本不存在。一个读取表格的抓取器不会注意到这一点;而一个收集引用的抓取器则会安静地少报四分之一的数据,仍然看起来正常。

maxBodySize(0) 移除了限制。请有意识地设置,而不是条件反射——在意外响应上进行无界读取本身就是一个问题——但要有意识地设置,并与服务器报告的 Content-Length 进行比较,后者被 HTTP 语义规范 定义为主体的八位字节数。

当页面在浏览器中呈现时

https://quotes.toscrape.com/js/ 标志着边界。它将其引用作为 JavaScript 数组提供,并在客户端构建 DOM,而 jsoup 成功获取了它:

text Copy
直接获取的 html 字节:5479
jsoup 找到的引用:0

5,479 字节,HTTP 200,零结果。jsoup 接收到的标记确实不包含任何 div.quote 元素——它们是在脚本运行后创建的,而 jsoup 没有脚本引擎。

大多数指南通过切换到浏览器自动化工具来回答这个问题,这意味着提取代码也要重写。这种权衡在 Node 中 Cheerio 和 Puppeteer 分别处于两侧,在 Java 中也需要付出同样的代价。更狭窄的解决方案是仅仅改变 HTML 到达的方式。这个工作属于 Scrapeless Universal Scraping API。它呈现页面并将生成的 HTML 作为字符串返回,你可以将其传递给相同的解析器。

JDK 内置的 HttpClient 足够用了——无需 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))

.POST(HttpRequest.BodyPublishers.ofString(payload.toString(), StandardCharsets.UTF_8))
.build();

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

}

Copy
响应包为 `{"code": ..., "data": "<渲染的 html>"}`。使用 `Jsoup.parse(html, url)` 解析该字符串 — 传递 URL 设置基本 URI,因此 `abs:href` 继续有效 — 然后运行相同的选择器方法:

```java
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
渲染的 html 字节数:            8940
相同解析器找到的引用: 10

相同的方法,相同的选择器,相同的 Document API。唯一改变的是这 8,940 字节的来源。将你的 API 密钥保存在环境中,如上面的 SCRAPELESS_API_KEY,而不是源码中。Universal Scraping API 快速入门指南 涵盖了其余请求参数。

入门只需要一分钟 — 创建一个免费的 Scrapeless 账户,免费计划涵盖本指南中的所有内容。

运行它

组装好类后,一个命令即可编译并运行它:

bash Copy
export SCRAPELESS_API_KEY="你的-api-key"
mvn -q -B compile exec:java

验证运行的全部输出:

text Copy
jsoup 1.21.1 | java 21.0.11
--- 静态页面,直接解析 ---
页面 1 上的书籍: 20
第一个标题: A Light in the Attic
第一个价格: £51.77
第一个 URL:   https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
HTML 中书写的 href: catalogue/a-light-in-the-attic_1000/index.html
--- 默认体大小限制 ---
http 状态,默认限制: 200
接收字节数,默认限制: 2097152
接收字节数,最大体大小(0): 2235648
表格行,默认限制: 544
表格行,最大体大小(0): 544
参考条目,默认限制: 187
参考条目,最大体大小(0): 255
最后一节,默认限制: References
最后一节,最大体大小(0): External links
--- JavaScript 渲染页面 ---
直接获取的 HTML 字节数: 5479
通过 jsoup 找到的引用:       0
--- 通过 Universal Scraping API 获取的相同页面 ---
渲染的 HTML 字节数:            8940
相同解析器找到的引用: 10

故障排除

Source option 5 is no longer supported. Maven 绑定了其默认的 maven-compiler-plugin 而不是你的。按上述方式在<build><plugins>中固定插件版本。

selectFirst 返回 null,下一行抛出 NullPointerException. 选择器没有匹配到任何东西。检查与 jsoup 实际接收到的 HTML — doc.html() — 而不是与浏览器检查器显示的内容比较,这个是后脚本 DOM。

计数低于页面显示。response.bodyAsBytes().length 与服务器的 Content-Length 进行比较。如果它们在 2,097,152 时匹配,则体大小限制是原因。

HttpStatusException: 403. 服务器拒绝了请求而不是解析。首先设置一个合理的用户代理;如果页面也需要渲染,则上述转变适用。

提取文本中的 Mojibake。 jsoup 从 Content-Type 头读取字符集,然后是 meta 标签。当服务器都没有声明时,明确将编码传递给 Jsoup.parse(InputStream, String, String)

结论

对于静态 HTML,jsoup 和 JDK 是整个工具链:一个依赖关系,一个流式请求,一个 CSS 选择器 API,以及一个能承受糟糕标记的解析树。决定结果是否可信的两种行为默认都是不可见的 — 返回一个看似健康的部分文档的 2 MiB 体积限制,以及在客户端渲染页面时的空结果集。

这两者都很便宜检查。将接收的字节与 Content-Length 进行比较,将选择器的计数与页面显示的进行比较。当第二个检查失败,因为标记为空时,解决方法是更改传输而保持解析器不变。
准备好尝试了吗?从Scrapeless免费计划开始,查看当前定价以获取更高的使用量。

常见问题

问:jsoup单独使用够吗,还是需要Selenium?

对于服务器渲染的HTML,单独使用jsoup就足够了,并且速度明显更快,因为它不会启动浏览器。只有当数据是由JavaScript生成时,你才需要渲染步骤——正如上面的测量所示,这一步可以是返回渲染HTML的HTTP请求,而不是你过程中的完整浏览器。

问:我怎么知道一个页面是否需要JavaScript才能编写选择器?

从纯jsoup获取中打印doc.html()并在其中搜索一个你可以在页面上看到的值。如果该值在字符串中缺失但在浏览器中可见,则它是客户端渲染的。比较选择器计数和可见计数可以捕捉到相同的情况。

问:改变maxBodySize的实际原因是什么?

默认情况下为2 MiB,这比许多列表、档案和比较页面要小。如果你的目标超过了这个限制,超过部分将从解析树中消失而不会引发错误。当你需要整个文档时,设置maxBodySize(0),并将接收到的字节与Content-Length进行比较,以知道何时需要。

问:jsoup能处理格式错误的HTML吗?

可以。它应用浏览器所遵循的相同错误恢复规则,因此未闭合的标签和嵌套错误的元素仍然会生成可查询的树。这是将其优先于严格的XML解析器处理现实世界页面的主要原因。

问:使用Java进行抓取合法吗?

语言与法律问题无关。重要的是你收集的数据、网站的条款、你遵循的爬虫指令以及你所处的管辖区。将收集限制在公开页面,保持请求量适中,对于任何涉及个人数据的情况寻求法律建议。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录