为什么选择 Java HttpClient?
相比于传统的 HttpURLConnection 和非常流行的第三方库 Apache HttpClient 和 Jsoup,Java 11+ 内置的 HttpClient 有以下优势:

- 现代标准:是 Java 标准库的一部分,无需添加额外依赖。
- 异步非阻塞:基于
java.net.http.HttpClient,支持异步请求 (CompletableFuture),性能更高,能更好地处理大量并发请求。 - API 设计优秀:API 流畅、直观,易于使用。
- 功能全面:支持 HTTP/1.1, HTTP/2, WebSocket,以及 WebSocket 客户端。
环境准备
你需要确保你的项目使用的是 Java 11 或更高版本。
如果你使用 Maven,你的 pom.xml 文件中不需要添加任何 HttpClient 的依赖,因为它已经是 Java 标准库的一部分。
<!-- 你的 pom.xml 文件 -->
<project ...>
...
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
...
</project>
HttpClient 基础用法
一个简单的 GET 请求包含三个核心部分:
- 创建
HttpClient实例:可以配置连接池、超时时间等。 - 构建
HttpRequest:指定请求的 URL、方法、头信息等。 - 发送请求并获取响应:同步发送或异步发送。
示例 1:同步 GET 请求
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class SimpleHttpClient {
public static void main(String[] args) {
// 1. 创建 HttpClient 实例
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // 使用 HTTP/2
.connectTimeout(Duration.ofSeconds(10)) // 连接超时时间
.build();
// 2. 构建 HttpRequest
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get")) // 一个用于测试的网站
.header("User-Agent", "My Java Crawler") // 设置请求头
.GET() // 显式指定 GET 方法
.build();
try {
// 3. 发送请求并同步获取响应
// response.body() 是响应体,这里我们期望是字符串
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 4. 处理响应
System.out.println("Status Code: " + response.statusCode());
System.out.println("Response Body:");
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
}
示例 2:异步 GET 请求
异步请求不会阻塞当前线程,而是返回一个 CompletableFuture,非常适合高并发场景。

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class AsyncHttpClient {
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.build();
// 异步发送请求
CompletableFuture<HttpResponse<String>> futureResponse = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
// 使用 thenAccept 处理响应结果
futureResponse.thenAccept(response -> {
System.out.println("Status Code: " + response.statusCode());
System.out.println("Response Body (Async):");
System.out.println(response.body());
});
// 主线程不能立即结束,否则异步任务可能还没执行完
// 这里我们简单等待一下,实际项目中通常有更复杂的异步处理逻辑
System.out.println("Request sent asynchronously...");
try {
// 等待异步任务完成
futureResponse.join(); // 阻塞直到 future 完成
} catch (Exception e) {
e.printStackTrace();
}
}
}
构建一个简单的爬虫
我们把上面的知识组合起来,构建一个可以爬取指定网页所有链接的简单爬虫。
爬虫逻辑
- 起始 URL:从一个或多个起始 URL 开始。
- 下载页面:使用
HttpClient获取网页的 HTML 内容。 - 解析链接:解析 HTML,提取出所有的
<a>标签中的href属性。 - 处理链接:将提取的链接规范化(如转为绝对路径),并过滤掉非目标域名的链接(避免爬到其他网站)。
- 存储与去重:将处理过的链接存入一个待爬取的队列,并将已爬取的 URL 存入一个集合中,避免重复爬取。
- 循环:从队列中取出下一个 URL,重复步骤 2-5,直到队列为空或达到爬取深度限制。
实现代码
为了解析 HTML,我们需要一个库。Jsoup 是一个非常流行且强大的 Java HTML 解析库,你需要先添加它的依赖。
Maven 依赖 (pom.xml):
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version> <!-- 使用最新版本 -->
</dependency>
SimpleCrawler.java:

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
public class SimpleCrawler {
// 使用线程安全的集合
private final Queue<String> urlsToCrawl = new ConcurrentLinkedQueue<>();
private final Set<String> crawledUrls = Collections.synchronizedSet(new HashSet<>());
private final HttpClient httpClient;
private final String targetDomain; // 目标域名,防止爬到其他网站
public SimpleCrawler(String startUrl) {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL) // 自动跟随重定向
.build();
// 规范化起始URL并添加到队列
this.urlsToCrawl.add(normalizeUrl(startUrl));
this.targetDomain = extractDomain(startUrl);
}
public void start(int maxPages) {
int pageCount = 0;
while (!urlsToCrawl.isEmpty() && pageCount < maxPages) {
String currentUrl = urlsToCrawl.poll();
// 如果已经爬过,则跳过
if (crawledUrls.contains(currentUrl)) {
continue;
}
System.out.println("Crawling: " + currentUrl);
try {
// 1. 下载页面
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(currentUrl))
.header("User-Agent", "My-Java-Crawler/1.0")
.build();
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() == 200) {
pageCount++;
crawledUrls.add(currentUrl);
// 2. 解析页面中的链接
Document doc = Jsoup.parse(response.body());
Elements links = doc.select("a[href]");
// 3. 处理链接
for (Element link : links) {
String newUrl = link.absUrl("href"); // 获取绝对URL
String normalizedNewUrl = normalizeUrl(newUrl);
// 检查链接是否有效且属于目标域名,并且未被爬取过
if (isValidUrl(normalizedNewUrl) && !crawledUrls.contains(normalizedNewUrl)) {
urlsToCrawl.add(normalizedNewUrl);
}
}
} else {
System.err.println("Failed to fetch: " + currentUrl + ", Status: " + response.statusCode());
}
} catch (Exception e) {
System.err.println("Error crawling " + currentUrl + ": " + e.getMessage());
}
}
System.out.println("\nCrawling finished. Total pages crawled: " + crawledUrls.size());
System.out.println("Crawled URLs:");
crawledUrls.forEach(System.out::println);
}
private String normalizeUrl(String url) {
if (url == null || url.isEmpty()) {
return "";
}
// 移除末尾的斜杠,方便比较
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
}
private String extractDomain(String url) {
try {
URI uri = new URI(url);
return uri.getHost();
} catch (Exception e) {
return null;
}
}
private boolean isValidUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
// 简单检查,确保是 http 或 https,并且域名匹配
return url.startsWith("http://") || url.startsWith("https://") &&
url.contains(targetDomain);
}
public static void main(String[] args) {
// 爬取维基百科 "Java" 页面,最多爬取 20 个页面
String startUrl = "https://en.wikipedia.org/wiki/Java_(programming_language)";
SimpleCrawler crawler = new SimpleCrawler(startUrl);
crawler.start(20);
}
}
高级爬虫技巧与最佳实践
上面的简单爬虫只是一个起点,一个健壮的爬虫还需要考虑更多:
异步并发爬取
同步爬取效率很低,一个请求完成后再发下一个,我们应该使用 CompletableFuture 和 ExecutorService 来实现异步并发。
// 在 SimpleCrawler 类中添加
private final ExecutorService executorService = Executors.newFixedThreadPool(10); // 10个线程
// 修改 start 方法
public void startAsync(int maxPages) {
// ... 初始化逻辑 ...
while (!urlsToCrawl.isEmpty() && pageCount < maxPages) {
String currentUrl = urlsToCrawl.poll();
if (crawledUrls.contains(currentUrl)) continue;
// 使用 submit 提交任务到线程池
executorService.submit(() -> {
// ... (下载、解析、处理链接的逻辑) ...
// 注意:crawledUrls 和 urlsToCrawl 的操作需要是线程安全的
});
}
executorService.shutdown(); // 关闭线程池
try {
executorService.awaitTermination(1, TimeUnit.HOURS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
尊重 robots.txt
robots.txt 是网站所有者用来告知爬虫哪些页面可以爬取,哪些不可以的协议,在爬取任何网站之前,都应该先检查其 robots.txt。
public boolean isAllowedToCrawl(String url) {
try {
String robotsTxtUrl = url.substring(0, url.indexOf('/', 8)) + "/robots.txt";
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(robotsTxtUrl)).GET().build();
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() == 200) {
// 这里应该使用一个 robots.txt 解析库,如 crawler-commons
// 简化版:检查 User-Agent 是否被允许
String body = response.body();
// ... 简单的字符串匹配逻辑 ...
// return !body.contains("Disallow: /");
System.out.println("robots.txt content for " + robotsTxtUrl + ":\n" + body);
return true; // 简化处理,实际应解析
}
} catch (Exception e) {
System.err.println("Could not fetch robots.txt for " + url);
}
return true; // 如果无法获取,默认允许
}
推荐:使用成熟的库如 crawler-commons 来解析
robots.txt。
设置请求头和 User-Agent
一些网站会检查 User-Agent,使用常见的浏览器 User-Agent 可以降低被屏蔽的概率。
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.build();
处理动态加载内容(JavaScript 渲染)
HttpClient 只能获取原始的 HTML,无法执行 JavaScript,如果网站内容是通过 JS 动态加载的(单页应用 SPA),HttpClient 爬取到的将是空页面或骨架页。
解决方案:使用无头浏览器,如 Selenium 或 Playwright,它们可以驱动一个真实的浏览器(在后台运行),执行 JS 并渲染出最终页面,然后再提取内容。
代理 IP
为了防止因请求过于频繁而被 IP 封禁,可以设置代理池,每次请求随机使用一个代理。
// 创建 HttpClient 时配置代理
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)))
.build();
你需要一个可靠的代理 IP 源。
数据存储
爬取到的数据(如文章标题、内容、链接等)需要存储起来。
- 控制台输出:仅用于调试。
- 文件存储:如 CSV, JSON, TXT,简单直接。
- 数据库:
- 关系型数据库:如 MySQL, PostgreSQL,适合结构化数据。
- 非关系型数据库:如 MongoDB,适合半结构化或非结构化数据。
| 特性 | 同步爬虫 | 异步爬虫 | 备注 |
|---|---|---|---|
| 核心库 | HttpClient |
HttpClient + CompletableFuture |
异步性能更高 |
| 并发控制 | 单线程,慢 | 线程池 (ExecutorService) |
异步爬虫的标配 |
| HTML解析 | Jsoup |
Jsoup |
Jsoup 是 Java 爬虫的标配 |
| 反爬虫 | 需手动处理(User-Agent, 代理等) | 需手动处理,但异步更容易实现IP轮换 | 反爬是持续对抗的过程 |
| 动态页面 | 无法处理 | 无法处理 | 需要集成 Selenium/Playwright |
从 HttpClient 开始,你可以逐步构建一个功能强大、健壮的 Java 爬虫,爬虫开发是一个实践性很强的领域,多动手尝试,你会遇到各种问题并学会如何解决它们。
