提到爬取,Java 不是大多数人首先想到的语言,但它是最可靠的选择之一。如果你的数据管道、服务层或批处理任务已经在 JVM 上运行,用 Java 做数据提取可以让所有内容保持在同一个代码库、同一套构建和同一套部署流程中。生态系统已经相当成熟:一个快速的 HTML 解析器、无头浏览器绑定,以及强大的并发模型,都是一流的支持。

本指南是使用 Java 进行网络爬取的实用教程。我们介绍你真正需要的工具箱(静态页面用 Jsoup,JavaScript 渲染页面用 HtmlUnit 和 Selenium),从头到尾演示一个完整的 Jsoup 示例,然后处理那个在规模化时最容易打垮爬虫的部分:反爬虫防御和 IP 封锁。目标是建立一个可以在此基础上继续构建的清晰思维模型,而不是一份详尽的目录。

Java 爬取工具箱

几乎每个 Java 爬虫都是由以下三种工具之一构建的。选哪个,主要取决于目标页面如何提供内容。

  • Jsoup 是主力工具。它通过 HTTP 获取 URL 并将返回的 HTML 解析为支持 CSS 选择器查询的文档,用起来像 jQuery 一样顺手。它速度快,没有外部运行时依赖,是任何数据存在于初始 HTML 中的页面的正确默认选择。
  • HtmlUnit 是 Java 的无 GUI 浏览器。它执行 JavaScript 并构建 DOM,因此可以访问 Jsoup 看不到的内容。它比真实浏览器更轻量,但其 JavaScript 引擎落后于现代网站,因此复杂的单页应用程序可能会让它束手无策。
  • Selenium WebDriver 从 Java 驱动真实浏览器(Chrome、Firefox)。它可以渲染用户看到的任何内容,是处理繁重客户端渲染最强大的选项,代价是速度最慢且资源消耗最大。

决策比你有时看到的那份长长的"爬虫类型"清单要简单得多。数据在原始 HTML 中?用 Jsoup。只有在脚本运行后才出现?先试 HtmlUnit,当 HtmlUnit 跟不上时再退而求其次用 Selenium。其他都是细节。

如何判断你需要哪个

打开目标 URL,查看源代码(不是检查器,是原始源代码),搜索你想提取的某个值。如果它在那里,说明页面是服务端渲染的,单独使用 Jsoup 即可。如果源代码几乎是空壳,而数据只出现在实时 DOM 中,说明页面是客户端渲染的,你需要浏览器引擎或渲染服务。

搭建 Jsoup 项目

Jsoup 作为单个 Maven 依赖发布。将其添加到你的 pom.xml,就可以开始获取和解析了。

xml
<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.17.2</version>
</dependency>

如果你使用 Gradle,将相同的坐标以 implementation 'org.jsoup:jsoup:1.17.2' 的形式加入 build.gradle。无论哪种方式,你现在都拥有了集获取与解析于一体的单个库。

一个完整的 Jsoup 示例,从头到尾

静态页面的模式始终相同:连接到 URL,获取解析后的 Document,用 CSS 选择器选取你想要的元素,然后提取字段。下面是一个完整、可运行的示例,它爬取书籍条目列表并为每本书打印一条结构化记录。将 URL 和选择器替换为你自己的目标即可。

java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class BookScraper {

    public static void main(String[] args) throws Exception {
        String url = "https://books.toscrape.com/";

        Document doc = Jsoup.connect(url)
            .userAgent("Mozilla/5.0 (compatible; MyScraper/1.0)")
            .timeout(10000)
            .get();

        Elements books = doc.select("article.product_pod");

        for (Element book : books) {
            String title = book.selectFirst("h3 a").attr("title");
            String price = book.selectFirst("p.price_color").text();
            String stock = book.selectFirst("p.instock").text().trim();

            System.out.printf("%s | %s | %s%n", title, price, stock);
        }
    }
}

这段代码中有几处细节值得一提。userAgent 调用设置了一个合理的标识符,而不是使用很多服务器会直接拒绝的 Jsoup 默认值。timeout 防止缓慢的主机使整个运行永久挂起。select 返回所有匹配的元素供你迭代;selectFirst 返回一个,这正是你在行内提取字段时想要的。从 title 属性读取书名,而不是从链接文本读取,可以避免可见文本有时携带的截断"..."。

输出是每本书一行整洁的记录:书名、价格和库存状态。从这里到将每条记录写入 JSON、CSV 或直接写入数据库,以及跟随分页链接遍历整个目录,只需几步之遥。如果你的目标是完整的多页爬取而不是单次获取,关于如何用 Java 构建网络爬虫的专项教程会将这个模式进一步延伸到队列和链接发现。

选择器是脆弱的部分

上面的 Java 代码几乎不需要改变。选择器则不然。网站会在没有任何警告的情况下重命名类和重构标记,因此上个月还能正常工作的爬虫今天可能返回空结果。将选择器集中放在一个地方,在预期元素缺失时大声报错,并在提取变得安静时重新检查实时页面。这是例行维护,不是说明方法有问题。

处理 JavaScript 渲染的页面

Jsoup 只能看到服务器发送的 HTML。当页面在浏览器中构建内容时,初始 HTML 是一个空壳,Jsoup 会返回空结果。你有两条出路。

第一条是 Java 浏览器引擎。HtmlUnit 无头运行页面的 JavaScript,并给你一个可以像 Jsoup 一样查询的已填充 DOM。它速度快,不需要安装外部浏览器,但其脚本引擎不如真实浏览器新,因此现代框架可能渲染不正确或抛出错误。Selenium WebDriver 通过自动化真实的 Chrome 或 Firefox 来规避这个问题:它精确渲染用户看到的内容,等待元素出现,甚至可以处理点击和滚动等交互。代价是开销。每个工作进程需要一个完整浏览器,消耗大量内存和 CPU,运行和维护一个浏览器集群本身就是真实的基础设施工作。

第二条路是完全跳过"在本地运行浏览器"这个问题,让渲染服务返回完整的 HTML。这样你的 Java 代码可以像上面的 Jsoup 示例一样简单,同时仍然获得完全渲染的页面,这正是下一节要介绍的内容。

真正的拦路虎:规模化时的反爬虫防御

渲染是问题中容易的那一半。困难的那一半在你发出超过少量几个请求的那一刻就会出现。商业网站监视爬虫形状的流量,并以速率限制、CAPTCHA 和彻底的 IP 封禁来反击。来自数据中心 IP 的每分钟数十个完全相同的请求很快就会被标记,一旦 IP 被封锁,无论你的解析代码有多干净,来自该 IP 的每一个请求都会失败。

解决方法是众所周知的:轮换多个 IP 地址使任何单一地址不会触发限制,优先使用被视为真实用户的住宅 IP,控制请求节奏,变换请求头,并渲染页面使流量看起来像来自浏览器。麻烦在于,自己组合这一切(一个健康的代理池加上无头浏览器集群加上重试逻辑)占据了大部分工程工作量,而这一切与你真正想要的数据毫无关系。完整的策略手册请参阅如何在不被封锁的情况下爬取网站

通过 Crawling API 路由请求

这正是将困难部分推到服务器端让你的 Java 保持简洁的地方。Crawling API 是一个单一 HTTP 端点:你向它发送目标 URL 和你的令牌,它在轮换住宅 IP 后面的真实浏览器中渲染页面,然后返回完整的 HTML。你的代码保持为一个普通的 HTTP 调用加上一次 Jsoup 解析。没有无头浏览器集群,没有代理池,没有在你这边处理 CAPTCHA。

因为它只是一个 HTTP 请求,你可以用 Java 内置的 HttpClient 调用它。当目标是客户端渲染时传递 &javascript=true;静态页面则省略,以节省渲染开销。

java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

public class CrawlingApiScraper {

    private static final String TOKEN = "YOUR_CRAWLBASE_JS_TOKEN";

    public static void main(String[] args) throws Exception {
        String target = "https://www.example.com/products";
        String encoded = URLEncoder.encode(target, StandardCharsets.UTF_8);

        String endpoint = "https://api.crawlbase.com/?token=" + TOKEN
            + "&javascript=true&url=" + encoded;

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(endpoint))
            .GET()
            .build();

        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            System.out.println("Request failed: " + response.statusCode());
            return;
        }

        Document doc = Jsoup.parse(response.body());
        Elements items = doc.select("div.product");
        System.out.println("Parsed " + items.size() + " products");
    }
}

注意解析部分与普通 Jsoup 示例完全相同。唯一的变化是 HTML 的来源:不再是 Jsoup.connect(url).get() 直接访问网站,而是通过 API 获取并将响应体传入 Jsoup.parse()。你关于选择器的所有既有知识一分不变,而渲染、IP 轮换和反封锁则发生在那个 HTTP 调用的另一侧。

Crawlbase Crawling API

规模化爬取的失败点在于封锁,而不在于解析。Crawling API 在轮换住宅 IP 后面的真实浏览器中渲染每个页面,并通过单次 HTTP 调用返回完整的 HTML,因此你的 Java 代码保持为一次获取加一次 Jsoup 解析。为客户端渲染页面添加 javascript=true,从免费套餐开始。

何时使用浏览器而非服务

通过 API 路由并非唯一有效的选择。如果你的目标需要真实的交互,登录、点击多步流程、填写表单、拖动滑块,那么使用 Selenium 驱动真实浏览器会给你单次 HTTP 调用无法提供的控制能力。坦诚地说,运营成本是真实存在的:你要自己负责浏览器集群、内存占用,以及自动化实时 UI 所带来的不稳定性。关于用浏览器进行提取的背景以及它们在哪些场景下值得使用,网络爬取无头浏览器选择是一篇很好的参考文章。

常见的折中方案是:对大量简单页面获取使用托管 API(在这些场景中,封锁和渲染是全部问题所在),并为那一小部分真正需要交互的目标保留一个 Selenium 工作进程。这样既能让基础设施保持轻量,又不会放弃需要完整浏览器的场景。

生产 Java 爬虫的良好习惯

无论你选用哪种工具,以下几种做法能区分一个只运行一次的脚本和一个可以可靠运行数月的爬虫。

  • 设置真实的 User-Agent 和超时。默认标识符会被拒绝,而无界限的请求可能使整个批次停滞。
  • 在字段缺失时大声报错。预期元素处出现 null 是选择器漂移最早的信号。捕获它并记录 URL。
  • 控制节奏并使用退避重试。间隔发送请求,遇到瞬时错误时等待更长时间再重试,而不是持续轰炸。
  • 将获取与解析分离。清晰的边界意味着你可以从直接的 Jsoup 获取切换到 Crawling API,而不需要修改提取代码。
  • 边运行边持久化。在每条记录解析后立即写入,而不是将长时间运行的结果缓冲在内存中,这样即使接近末尾时崩溃也不会丢失一切。
回顾

核心要点

  • Jsoup 是默认选择。对于数据存在于原始 HTML 中的任何页面,连接、用 CSS 选择器选取并提取。它速度快,依赖轻量。
  • JavaScript 页面需要浏览器引擎或渲染器。HtmlUnit 和 Selenium 在 Java 中渲染客户端内容;渲染服务无需你自己运行即可返回完整 HTML。
  • 封锁而非解析,才是规模化时打垮爬虫的原因。速率限制、CAPTCHA 和 IP 封禁是真正的障碍,轮换住宅 IP 加上合理节奏是答案。
  • Crawling API 让 Java 保持简洁。单次 HTTP 调用在服务端处理渲染和 IP 轮换,因此你的代码保持为一次获取加一次 Jsoup 解析。
  • 选择器是脆弱的那一层。预期它们会漂移,漂移时大声报错,将重新检查实时页面视为例行维护。

常见问题

可以用 Java 进行网络爬取吗?

可以。Java 拥有成熟的爬取生态系统:Jsoup 用 CSS 选择器获取和解析静态 HTML,HtmlUnit 和 Selenium 处理 JavaScript 渲染的页面,内置的 HttpClient 让你调用渲染或代理服务。如果你的技术栈已经在 JVM 上运行,用 Java 爬取可以将提取与管道其余部分保持在同一个代码库中。

哪个 Java 库最适合网络爬取?

取决于页面类型。Jsoup 是静态服务端渲染 HTML 的最佳默认选择,因为它速度快且简单。对于在浏览器中构建内容的页面,HtmlUnit 可以无头运行 JavaScript,Selenium WebDriver 则为最繁重的单页应用程序驱动真实浏览器。大多数项目使用 Jsoup 进行解析,仅在页面有需要时才添加浏览器引擎或渲染服务。

Jsoup 什么时候会不够用?

Jsoup 只能看到服务器返回的 HTML,因此对于使用 JavaScript 在客户端渲染数据的页面,它会返回空结果。快速测试方法是查看原始页面源代码,搜索你想要的某个值:如果它不在源代码中但出现在实时 DOM 中,你需要 HtmlUnit 或 Selenium 这样的浏览器引擎,或者返回完整 HTML 供 Jsoup 解析的渲染服务。

如何在用 Java 爬取时避免被封锁?

轮换多个 IP 地址使任何单一地址不会触发速率限制,优先使用被视为真实用户的住宅 IP,控制请求节奏,变换请求头,并渲染页面使流量看起来像浏览器。自己构建这一切是大部分工作量所在,这就是为什么很多团队选择通过 Crawling API 路由请求,它在服务端处理轮换、渲染和反封锁。

Java 和 Python 哪个更适合网络爬取?

Python 拥有更大的爬取社区和略为平缓的学习曲线,但 Java 完全有能力胜任,当你的数据平台、服务或批处理任务已经在 JVM 上运行时,Java 往往是更好的选择。在一种语言和一套构建中同时处理提取和下游处理,通常比库数量上的边际差异更有价值。

每个页面都需要在 Crawling API 上使用 JavaScript 选项吗?

不需要。只有当页面在客户端渲染内容时才传递 javascript=true。静态的服务端渲染页面无需它即可返回数据,对这些页面跳过渲染更快也更省钱。最简单的规则:当普通获取返回空壳时开启它,否则关闭。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量