jsoup 1.23.1:更快、更省内存,并补齐复杂 HTML 解析细节

2026-07-30 26 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

处理网页抓取、内容清洗或富文本导入时,真正棘手的通常不是结构规整的示例 HTML,而是标签缺失、嵌套混乱,并夹杂 SVG、MathML、CDATA 与 noscript 的真实页面。jsoup 1.23.1 针对这些场景继续改进解析器,在提升速度、降低内存占用的同时,进一步增强了对 HTML 标准的兼容性与处理安全性。

性能改进为什么值得关注

jsoup 的常见工作流包括三个阶段:读取输入、构建 DOM、通过 CSS 选择器提取或修改节点。对于单个小页面,解析器的性能差异可能不明显;但在爬虫、批量迁移和内容审核任务中,DOM 构建会被重复执行成千上万次,内存分配和解析耗时会直接影响吞吐量。

更快且更节省内存的解析器尤其适合以下场景:

  • 批量解析大量网页或归档 HTML;
  • 在有限堆内存的容器中运行内容处理服务;
  • 解析体积较大的富文本、产品详情或文档页面;
  • 同时执行多个解析任务,需要控制垃圾回收压力。

升级后不要只比较单次请求耗时。更有价值的指标是固定堆大小下的持续吞吐量、峰值内存、GC 暂停时间,以及异常页面是否会导致任务堆积。

noscript、CDATA、SVG 与 MathML 不只是边角情况

现代网页经常混合多种内容模型。noscript 的解释可能受解析上下文影响;CDATA 在 HTML 与 XML 中的规则不同;SVG 和 MathML 又包含各自的命名空间、标签名称和文本处理方式。如果解析器没有正确切换规则,最终 DOM 可能与浏览器生成的结构不同。

这种差异会产生很实际的问题:

  • CSS 选择器找不到浏览器中明明存在的节点;
  • SVG 内部元素被错误归一化或重新排列;
  • 数学公式中的文本和嵌套结构丢失;
  • 内容清洗后,原本可用的嵌入式图形无法渲染;
  • 同一段输入在 HTML 模式和 XML 模式下得到意外结果。

1.23.1 对这些区域的改进,意味着 jsoup 生成的 DOM 更接近 HTML 标准定义的解析结果。不过,标准兼容性提升也可能让少量依赖旧解析行为的选择器或快照测试发生变化,因此升级时仍需执行回归测试。

可以这样实践:解析并检查混合内容

下面是一个可直接运行的 Maven 示例。运行前需要安装 JDK 17 或更高版本以及 Maven,并将两个文件放在对应目录中。

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>example</groupId>
    <artifactId>jsoup-1231-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.23.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.5.0</version>
                <configuration>
                    <mainClass>example.JsoupDemo</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

创建 src/main/java/example/JsoupDemo.java

package example;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

public class JsoupDemo {
    public static void main(String[] args) {
        String html = """
            <!doctype html>
            <html>
              <body>
                <article data-id="42">
                  <h1>Parser compatibility check</h1>
                  <noscript><p class="fallback">JavaScript is disabled</p></noscript>
                  <svg viewBox="0 0 100 20">
                    <text x="5" y="15">SVG label</text>
                  </svg>
                  <math>
                    <mrow><mi>x</mi><mo>=</mo><mn>42</mn></mrow>
                  </math>
                </article>
              </body>
            </html>
            """;

        Document document = Jsoup.parse(html);
        Element article = document.selectFirst("article[data-id=42]");

        if (article == null) {
            throw new IllegalStateException("Expected article was not parsed");
        }

        System.out.println("Title: " + article.selectFirst("h1").text());
        System.out.println("Fallback: " + article.select("noscript").text());
        System.out.println("SVG text: " + article.select("svg text").text());
        System.out.println("Math text: " + article.select("math").text());
        System.out.println("Normalized HTML:\n" + article.outerHtml());
    }
}

运行:

mvn compile exec:java

这个例子同时覆盖普通 HTML、noscript、SVG 和 MathML。实际升级时,可以把线上出现过的复杂页面裁剪成类似的最小样本,保存预期选择器结果,而不是只比较完整 HTML 字符串。完整序列化结果可能因标准兼容性修正而发生合理变化,业务真正依赖的节点、属性和文本通常更适合作为断言目标。

为解析入口加上边界

解析器更快或更安全,并不意味着应用可以无条件接受任意输入。对于来自用户、第三方网站或消息队列的 HTML,仍应在业务层设置限制:

  • 限制响应体或上传文件大小,避免超大输入耗尽堆内存;
  • 为网络下载设置连接和读取超时;
  • 不要把解析后的不可信 HTML 直接输出到页面;
  • 输出用户内容前使用允许列表进行清洗;
  • 抓取远程地址时防范 SSRF、重定向滥用和异常内容类型;
  • 对解析失败、超时和输入过大分别记录指标。

例如,需要把用户提交的富文本重新展示到网页时,可以使用 jsoup 的允许列表清洗 API:

import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;

String untrusted = "<p>Hello <strong>team</strong></p>"
        + "<script>alert('unsafe')</script>";

String cleaned = Jsoup.clean(untrusted, Safelist.basic());
System.out.println(cleaned);

允许列表必须根据产品需求调整。Safelist.basic() 适合作为演示起点,但不代表它天然符合所有富文本、链接属性或嵌入媒体策略。

升级建议

从旧版本升级到 1.23.1 时,可以按下面的顺序控制风险:

  1. 收集包含残缺标签、noscript、CDATA、SVG 和 MathML 的真实输入样本;
  2. 对业务依赖的 CSS 选择器、节点属性和提取文本建立回归测试;
  3. 在相同 JVM 参数和数据集下比较吞吐量、峰值堆内存与 GC;
  4. 检查序列化 HTML 的变化是否会影响缓存键、内容差异比较或下游系统;
  5. 对外部输入继续保留大小、超时、地址访问和输出清洗限制。

jsoup 1.23.1 的价值不只是跑分提升。对于长期处理真实网页的系统,更准确的 DOM、更稳定的混合内容解析和更低的资源成本,往往会一起减少那些难以复现的数据提取问题。升级的关键,是用自己的页面样本验证行为,而不是只确认项目能够编译。


相关推荐