杰瑞科技汇

java httpclient爬虫

为什么选择 Java HttpClient?

相比于传统的 HttpURLConnection 和非常流行的第三方库 Apache HttpClientJsoup,Java 11+ 内置的 HttpClient 有以下优势:

java httpclient爬虫-图1
(图片来源网络,侵删)
  1. 现代标准:是 Java 标准库的一部分,无需添加额外依赖。
  2. 异步非阻塞:基于 java.net.http.HttpClient,支持异步请求 (CompletableFuture),性能更高,能更好地处理大量并发请求。
  3. API 设计优秀:API 流畅、直观,易于使用。
  4. 功能全面:支持 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 请求包含三个核心部分:

  1. 创建 HttpClient 实例:可以配置连接池、超时时间等。
  2. 构建 HttpRequest:指定请求的 URL、方法、头信息等。
  3. 发送请求并获取响应:同步发送或异步发送。

示例 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,非常适合高并发场景。

java httpclient爬虫-图2
(图片来源网络,侵删)
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();
        }
    }
}

构建一个简单的爬虫

我们把上面的知识组合起来,构建一个可以爬取指定网页所有链接的简单爬虫。

爬虫逻辑

  1. 起始 URL:从一个或多个起始 URL 开始。
  2. 下载页面:使用 HttpClient 获取网页的 HTML 内容。
  3. 解析链接:解析 HTML,提取出所有的 <a> 标签中的 href 属性。
  4. 处理链接:将提取的链接规范化(如转为绝对路径),并过滤掉非目标域名的链接(避免爬到其他网站)。
  5. 存储与去重:将处理过的链接存入一个待爬取的队列,并将已爬取的 URL 存入一个集合中,避免重复爬取。
  6. 循环:从队列中取出下一个 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:

java httpclient爬虫-图3
(图片来源网络,侵删)
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);
    }
}

高级爬虫技巧与最佳实践

上面的简单爬虫只是一个起点,一个健壮的爬虫还需要考虑更多:

异步并发爬取

同步爬取效率很低,一个请求完成后再发下一个,我们应该使用 CompletableFutureExecutorService 来实现异步并发。

// 在 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 爬取到的将是空页面或骨架页。

解决方案:使用无头浏览器,如 SeleniumPlaywright,它们可以驱动一个真实的浏览器(在后台运行),执行 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 爬虫,爬虫开发是一个实践性很强的领域,多动手尝试,你会遇到各种问题并学会如何解决它们。

分享:
扫描分享到社交APP
上一篇
下一篇