返回博客
指南
Mihai MaximLast updated on May 12, 20263 min read

用 Jsoup 在 Java 中解析 HTML

用 Jsoup 在 Java 中解析 HTML
简而言之:Jsoup 是 Java 中解析 HTML 的默认库。本指南将带您完整了解其生命周期(Maven 配置、加载 Document、CSS 选择器、DOM 遍历、数据提取、修改和序列化),并提供一个可运行的爬虫项目,同时涵盖错误处理、分页,以及那些促使您转向无头浏览器或爬虫 API 的局限性。

若需在 JVM 服务中提取或重写 HTML,您有几种选择,但对于大多数实际工作而言,Java 中的 HTML 解析仍以 Jsoup 为起点和终点。网页抓取是指从网站 HTML 源代码中自动提取数据,而 Jsoup 正是将该源代码转换为可导航 DOM 的开源库,您可通过 CSS 选择器对其进行查询并就地修改。

本 Jsoup 教程专为希望获得实操指南而非营销概述的中级 Java 开发者(后端工程师、数据工程师、SEO 和 QA 人员,以及任何负责内容迁移的人员)而设计。我们将涵盖 Maven 配置、加载 Document ,配置 HTTP 请求,处理错误,遍历和选择元素,提取文本和属性,修改节点,以及将结果序列化回干净 String, File或 URL 加载文件、配置 HTTP 请求、处理错误、遍历和选择元素、提取文本和属性、修改节点,以及将结果序列化回干净的 HTML。文章结尾提供了一个完整的可运行抓取项目,并附有分页和速率限制的说明。

我们也坦诚地指出其局限性:Jsoup无法运行JavaScript、轮换IP地址或绕过反机器人防御机制。结尾部分将说明其能力边界,并推荐后续可选方案。

为何 Jsoup 是 Java 中 HTML 解析的首选

当所需数据位于公共网页上且网站未提供 API 时,您需要编写一个爬虫。多年来,在 Java 中的 HTML 解析领域,Jsoup 一直是实用的默认选择:开源、版本稳定、文档完善,并拥有流畅的 API,可从 jQuery 或原生 DOM JavaScript 无缝迁移。关键在于,它覆盖了工作流的两个环节:读取 HTML 和写入 HTML。

Jsoup 功能一览

Jsoup 实现了 WHATWG HTML5 规范,因此它能像现代浏览器一样解析几乎任何标记,无论是结构完美的,还是真正损坏的。 您将获得 DOM 树、jQuery 风格的选择器,以及用于读取和写入的方法。但它无法执行 JavaScript。任何在初始响应后由客户端框架注入的内容(如 React 存储、延迟加载的行、需加载后才能显示的内容)对 Jsoup 而言都是不可见的。这一局限性将导致后续“限制”部分的讨论。

在 Maven 项目中配置 Jsoup

使用以下命令生成一个 Maven 模板: mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart,然后将 Jsoup 依赖项添加到 pom.xml。撰写本文时,当前版本为 1.17.x 系列,但在生产环境中锁定版本前,请务必在 Maven Central 上确认最新稳定版本:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.17.x</version>
</dependency>

若需使用 mvn exec:java运行示例,请在 <plugins> 块中注册 Exec Maven Plugin。请使用其插件页面列出的当前版本;旧教程中提到的 3.0.0 可能已过时。不使用 Maven?Jsoup 提供了一个单一的 JAR 文件,您可以将其放入类路径中,而 Gradle 用户可以声明 implementation 'org.jsoup:jsoup:1.17.x'.

将 HTML 加载到 Jsoup 文档中

使用此库在 Java 中解析 HTML 的入口点是 Jsoup 类。你可以通过 StringFileInputStream,或(最常见的方式)直接从 URL 获取 HTML。一个简洁的 Jsoup 连接示例如下:

// From a string, great for unit tests
Document fromString = Jsoup.parse("<html><body><p>Hello</p></body></html>");

// From a local file
Document fromFile = Jsoup.parse(new File("page.html"), "UTF-8");

// From a live URL: issues the HTTP request, then parses the response
Document doc = Jsoup.connect("https://example.com").get();

与自行编写 HttpURLConnection,流畅的 Jsoup.connect(...) API 省去了大量冗余代码:它负责管理套接字、读取正文、解码字符集,并通过一次调用返回已解析的 Document 。该 Document 即为内存中的 DOM,您可基于此进行后续所有操作,从 CSS 选择器到 DOM 修改皆可。

Jsoup.connect(url) 返回一个 Connection 对象,你可以在发出请求前对其进行配置。对于友好的接口,默认设置即可,但大多数真实目标至少需要一个真实的 User-Agent 和一个合理的超时时间:

Document doc = Jsoup.connect("https://example.com/listing")
    .userAgent("Mozilla/5.0 (compatible; MyJavaScraper/1.0; +https://yourdomain.tld/bot)")
    .referrer("https://example.com")
    .header("Accept-Language", "en-US,en;q=0.9")
    .cookie("session", "abc123")
    .timeout(10_000)
    .data("q", "java")
    .method(Connection.Method.GET)
    .get();

选择一个能如实标识你机器人的 User-Agent。当 User-Agent 看起来像默认的 Java HTTP 客户端时,许多服务器会返回精简版响应,甚至直接封禁。

处理 HTTP 错误、状态码和超时

此处有两个关键的异常。 HttpStatusException 当服务器返回 4xx 或 5xx 状态码时会抛出该异常,并同时提供出错的 URL 和状态码。 IOException 则涵盖其他所有情况:DNS 失败、连接重置、套接字超时。请同时捕获这两种情况:

try {
    Document doc = Jsoup.connect(url).timeout(10_000).get();
} catch (HttpStatusException e) {
    log.warn("Bad status {} for {}", e.getStatusCode(), e.getUrl());
} catch (IOException e) {
    // retry with exponential backoff, then escalate
}

若您确实需要获取 404 页面的正文(用于软 404 检测),请将 .ignoreHttpErrors(true).get()。对于生产环境的爬虫,请将网络调用封装在采用指数退避策略的重试循环中;大规模运行时,短暂的 5xx 错误和重置错误是正常的。

使用 Jsoup CSS 选择器选择元素

一旦获取了 Document,查询它只需一行代码。 Document.select(String cssQuery) 支持与 querySelectorAll 中使用的语法,并返回一个 Elements 集合,该集合绝不会 null,即使没有任何匹配项也是如此。仅此一点,就消除了使用简单的 DOM 代码时会遇到的整类 NullPointerExceptions。

Jsoup 的 CSS 选择器语法远不止标签和类。以下简要介绍值得与任何 CSS 选择器速查表一同收藏:

选择器

匹配

div.post-card

<div> 具有 class post-card

article > h2

直接子元素 h2article

a[href^=https]

链接 hrefhttps

img[src*=authors]

图片,其 src 包含子字符串 authors

li:nth-child(2)

第二个 li 在其父级中

section:has(h2)

包含至少一个 h2

p:contains(error)

段落,且这些段落包含字面文本“error”

可自由组合这些条件。一种稳妥的做法是将子选择器的作用域限定在先前选定的 Element ,而非从文档根节点重新运行查询。

getElementById、getElementsByClass 和 select 的比较

对于希望使用显式获取器的读者,Jsoup 复现了 JavaScript DOM API。 getElementById(id) 返回单个 Element (或 null) 元素,与 document.getElementByIdgetElementsByClass(name) 返回所有匹配项,就像 document.getElementsByClassName. select(cssQuery) 等同于 querySelectorAll ,且是三者中最灵活的。

当意图明确时(如稳定的 ID 或单一语义类),请使用显式获取器,而 select() 需要组合或属性过滤时,请使用显式获取器。一个实际应用中的警告:避免将框架生成的实用类用作锚点选择器。例如 Tailwind 中的 p-[10px]text-slate-700 这类 Tailwind 类只是构建输出的细节,可能在下一次部署时消失。请依赖稳定的 ID、ARIA 角色或语义标签,这样你的爬虫程序将具有更强的持久性。

遍历 DOM 树:父节点、同级节点、子节点、第一个/最后一个/第 n 个

选择器让你打开大门;遍历则让你找到兄弟节点和祖先节点。Jsoup Document API 提供了 parent(), parents(), children()siblingElements(),以及通过 first(), last()以及 get(int n)。每个 Element 还拥有各自的 select() 方法,可将查询范围限定在该子树内,这是编写健壮选择器的最简洁方式:

Element card = doc.selectFirst("article.post-card");
String title  = card.select("h2 > a").text();
String author = card.parent().select(".byline").text();
Elements tags = card.children().select("span.tag");

从文档根节点开始,向上遍历至一个稳定的祖先节点,再向下遍历,这种方式比从文档根节点起链式调用易碎的类选择器要可靠得多,特别是在使用 CSS-in-JS 或实用类框架的页面上。

从元素中提取文本、HTML 和属性

一旦选中了一个 Element,有四种方法几乎可以覆盖所有从 HTML 中提取数据的场景。 text() 返回可见且已压缩空格的文本(类似于 innerText). html() 返回内部 HTML 作为字符串。 outerHtml() 包含元素自身的标签。 ownText() 仅返回元素的直接文本节点,跳过子元素。

对于属性, attr("href") 读取一个值, absUrl("href") 根据文档的基准 URI 解析相对 URL,这在抓取链接列表时非常有用。迭代操作非常简单,因为 ElementsIterable:

for (Element link : doc.select("a[href]")) {
    System.out.println(link.text() + " -> " + link.absUrl("href"));
}

您还可以进行流式处理,使用 forEach,或通过索引使用 get(n)。无论哪种方式最符合您的代码风格都行。

使用 Jsoup 修改和输出 HTML

大多数教程仅止步于内容提取,但使用 Jsoup 在 Java 中解析 HTML 实际上是双向的。相同的 attr(), text()html() 方法同时兼具设置器功能。你可以使用 new Element(Tag.valueOf("..."))创建新节点,并通过 appendChild()appendElement(),并使用 remove()。Jsoup 修改 HTML 的操作界面如下所示:

Document doc = Jsoup.parse(rawHtml);

// Edit existing nodes
doc.select("a.tracker").forEach(a -> a.attr("rel", "nofollow"));
doc.selectFirst("h1").text("Updated Title");

// Add a new node
Element note = new Element(Tag.valueOf("p"), "")
    .text("Edited by my scraper at " + Instant.now());
doc.body().appendChild(note);

// Remove ad slots
doc.select("div.ad-slot").remove();

// Serialize back to a clean HTML string
String cleaned = doc.html();

正是这种往返操作(解析、修改、序列化)使得 Jsoup 不仅适用于一次性抓取,更在内容迁移、HTML 净化和源数据规范化方面大显身手。

实践项目:端到端抓取博客列表

为了将所有内容整合起来,构建一个小型抓取器,从公共博客列表中的每张帖子卡片中提取标题、链接、封面图片和作者头像。首先在开发者工具中打开页面;手动勘察总比凭空猜测更可靠。为每张卡片确定一个稳定的容器选择器,然后针对它编写逐字段的选择器。

Document doc = Jsoup.connect("https://example.com/blog")
    .userAgent("MyJavaScraper/1.0")
    .timeout(10_000)
    .get();

for (Element card : doc.select("article.post-card")) {
    String title   = card.select("h2 > a").text();
    String url     = card.select("h2 > a").absUrl("href");
    String header  = card.selectFirst("img.header-image").absUrl("src");
    String avatar  = card.select("img[src*=authors]").attr("abs:src");

    System.out.printf("%s | %s | %s | %s%n", title, url, header, avatar);
}

每个字段都有其基于意图的专属选择器。 img[src*=authors] 通过属性子字符串进行过滤,当标记结构发生变化时,这种方法比链式结构选择器更稳健。这种基于 Jsoup 的结构化 Java 网页抓取,总是比脆弱的基于索引的解析更胜一筹。

遍历分页页面

大多数列表都遵循可预测的 URL 方案,例如 /blog, /blog/page/2/, /blog/page/3/。将第 1 页视为特例,并循环直至遇到空结果集或来自 HttpStatusException。每次请求间隔一两秒,稍作随机化,并遵守目标网站的 robots.txt(RFC 9309)。没有速率限制的分页操作是最快被封禁的方式,也是人们最终不得不阅读“为何被封禁”相关文章的最常见原因。

Jsoup 的局限性及何时需要寻找替代方案

Jsoup 的硬性限制在于 JavaScript。它仅解析服务器初始返回的内容,因此任何在客户端渲染的内容(如 React、Vue 或 Angular 的单页应用,延迟加载的无限滚动,以及隐藏在 hydration 机制后的内容)都无法被识别。此外,它不支持无头渲染、代理轮换或反机器人绕过机制。

当页面为动态内容时,请将 Jsoup 与无头浏览器配合使用:Selenium 和 Playwright 可驱动真实的 Chromium 浏览器;HtmlUnit 是一种更轻量级的 JVM 原生选项;Jaunt 提供了一个类似的 Java API,并内置了 HTTP 功能。 当页面为静态但存在防护机制(如 Cloudflare、频繁的 IP 封禁、指纹识别)时,请通过可管理的数据抓取 API 转发请求,该 API 负责处理代理和 CAPTCHA 验证,随后将响应的 HTML 直接回传给 Jsoup。这能保持解析代码的简洁性,并减少系统中的动态组件。

总结:构建高韧性的 Java HTML 解析器

使用 Jsoup 在 Java 中进行 HTML 解析的完整工作流包含四个步骤:加载选择提取修改,然后输出。 若需深入阅读,Jsoup 实战指南和 Javadocs 是权威参考资料。在开始编写新的爬虫之前,请快速检查以下决策清单:页面是静态的还是通过 JavaScript 渲染的?目标页面是否可能阻断访问?我需要修改 HTML 还是仅需读取?这三个问题的答案将决定仅使用 Jsoup 是否足够。

关键要点

  • 任何 HTML 标记由服务器渲染的 Java 任务,都应使用 Jsoup 进行解析。它能像现代浏览器一样处理格式错误的 HTML。
  • Jsoup.connect(url).get() 将数据获取与解析合并为一次调用。务必设置真实的 User-Agent 以及非默认超时值,并同时捕获 HttpStatusExceptionIOException.
  • select() 返回一个 Elements 列表,该列表可能为空,但绝不会 null。建议优先使用稳定的ID、ARIA角色和语义选择器,而非框架生成的实用类。
  • Jsoup 是双向的: attr, text,以及 html 作为设置器,此外 appendChild 以及 remove,可让您编辑并重新序列化 HTML,而不仅仅是读取它。
  • Jsoup 不执行 JavaScript。对于单页应用 (SPA),请将其与 Selenium、Playwright 或 HtmlUnit 配合使用;对于被封锁的目标,请通过受管理的抓取 API 转发请求。

常见问题

Jsoup 能抓取 JavaScript 渲染的页面或单页应用吗?

不可以。Jsoup 仅解析服务器返回的原始 HTML,因此页面加载后由客户端框架生成的任何内容对其而言都是不可见的。若要抓取单页应用(SPAs)或在客户端加载的页面,请使用 Selenium、Playwright 或 HtmlUnit 驱动真实浏览器或无头浏览器,捕获完全渲染后的 HTML,然后将该字符串传递给 Jsoup.parse(...) 进行基于选择器的提取。

在 HTML 解析方面,Jsoup 与 HtmlUnit、Jaunt 或 Selenium 有何不同?

Jsoup 是一个纯粹的 HTML 解析器。它不执行 JavaScript,不运行 JS 引擎,也不模拟浏览器。HtmlUnit 和 Selenium 都通过 JS 引擎渲染页面(HtmlUnit 在 JVM 内,Selenium 通过真实的浏览器驱动程序)。Jaunt 更接近 Jsoup,它既是解析器,也是简单的 HTTP 客户端。当页面是静态的时使用 Jsoup;当需要渲染或交互时使用其他工具。

使用 Jsoup 解析页面时,如何避免被封禁或受速率限制?

在 User-Agent 中如实标识您的机器人,将每台主机的请求限制为每秒几条,随机化延迟,并在适当情况下复用 Cookie。请阅读并遵守 robots.txt。对于高流量任务或具有防御性的目标,请通过住宅代理或轮换代理池转发请求,因为 Jsoup 本身不具备 IP 轮换、指纹伪装或 CAPTCHA 处理功能。

Jsoup 能解析 XML、RSS 源或格式错误的 HTML 吗?

以上三者均可。对于 Jsoup.parse(input, baseUri, Parser.xmlParser()) 解析 RSS 源、网站地图及其他 XML 文档。对于格式错误的 HTML,默认解析器具有容错性,会像现代浏览器一样规范化标记,因此未闭合的标签和多余字符通常仍能生成可用的 Document.

Jsoup 的最新稳定版本是什么?如何保持更新?

请直接查阅 Maven Central,因为版本号更新频繁,教程中提及的任何版本号可能已过时。订阅 Jsoup GitHub 仓库的发布说明,或运行 Maven 依赖更新插件(如 versions:display-dependency-updates ,以自动发现可用的升级版本。如果您的仓库托管在相应平台,Renovate 和 Dependabot 均可使用。

结论

如果你读完这篇指南只记住一件事,那就记住这个四步流程:将 HTML 加载到 Document,筛选目标内容,提取或修改,最后序列化输出。这一流程是您将编写的每个基于 Jsoup 的爬虫、内容迁移工具及 HTML 净化器的核心。若再添加真实的 User-Agent、合理的超时设置、结构化的异常处理以及带退避机制的重试策略,您便拥有了一个能经受生产环境流量考验的解析器。

但有一点必须坦诚:Jsoup 无法运行 JavaScript,也无法绕过反机器人防御机制。如果页面在客户端渲染,你需要一个无头浏览器。如果目标网站封锁了你的 IP 或识别出了你的抓取工具,你需要更智能的请求层。

第二种情况正是托管式抓取 API 大显身手之处。WebScrapingAPI 的 Scraper API 即使面对敌对目标也能返回原始 HTML,并在其端处理代理轮换、验证码和浏览器指纹识别,因此您可以保持 Jsoup 解析代码不变,只需替换抓取步骤即可。这是我们目前发现的,将生产环境级别的韧性无缝集成到精简 Java 解析器上的最简洁方案。

关于作者
Mihai Maxim, 全栈开发工程师 @ WebScrapingAPI
Mihai Maxim全栈开发工程师

米海·马克西姆(Mihai Maxim)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。