首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java并发爬虫实战:快速批量获取各省份人口详情

Java并发爬虫实战:快速批量获取各省份人口详情

原创
作者头像
小白学大数据
发布2025-09-10 16:51:43
发布2025-09-10 16:51:43
1040
举报

一、技术选型与设计思路

我们的目标是高效、稳定、可扩展地获取数据。因此,在技术选型上,我们选择了以下强大的Java库:

  1. OkHttp: 一个高效的HTTP客户端,它支持HTTP/2协议,默认支持连接池,这对于并发请求至关重要。
  2. Jackson: 业界顶级的JSON处理库,用于将JSON字符串解析为Java对象(反序列化),操作简单且性能优异。
  3. ExecutorService (线程池): Java原生并发包下的核心工具。通过创建一个固定大小的线程池,我们可以有效地管理并复用线程,避免频繁创建和销毁线程的开销,从而控制并发请求的数量,既提高了速度,又防止对目标服务器造成过大压力。

整体工作流程设计如下:

  1. 构造所有需要爬取的省份数据API的URL列表。
  2. 创建一个固定大小的线程池。
  3. 将每一个URL的爬取和解析任务包装成一个CallableRunnable任务,提交给线程池。
  4. 线程池分配线程并发地执行这些任务,包括:发送HTTP请求、接收响应、解析JSON数据。
  5. 将每个任务解析得到的最终数据结果收集到一个线程安全的集合中。
  6. 关闭线程池,等待所有任务完成。
  7. 主线程处理或输出最终收集到的所有省份数据。

二、实现步骤与代码详解

下面我们通过一个完整的示例代码来演示这一过程。请注意,由于实际的可爬取、返回规范JSON的省份人口API难以公开获得,本例将采用一个模拟服务来演示。核心的并发爬虫逻辑是完全一致且可复用于真实场景的。

步骤1:引入Maven依赖

首先,在你的pom.xml文件中添加OkHttp和Jackson的依赖。

代码语言:txt
复制
<dependencies>
    <!-- OkHttp HTTP Client -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.12.0</version> <!-- 请使用最新版本 -->
    </dependency>

    <!-- Jackson Core -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.16.1</version> <!-- 请使用最新版本 -->
    </dependency>
</dependencies>

步骤2:定义省份数据实体类

根据预期的JSON数据结构,我们定义一个Java Bean类ProvincePopulation

代码语言:txt
复制
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
 * 省份人口详情实体类
 * 使用@JsonIgnoreProperties忽略未知属性,增强鲁棒性
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class ProvincePopulation {
    private String provinceName;
    private Long totalPopulation;
    private Long malePopulation;
    private Long femalePopulation;
    private Double growthRate;

    // 无参构造器、全参构造器、getter和setter方法必不可少
    public ProvincePopulation() {
    }

    public ProvincePopulation(String provinceName, Long totalPopulation, Long malePopulation, Long femalePopulation, Double growthRate) {
        this.provinceName = provinceName;
        this.totalPopulation = totalPopulation;
        this.malePopulation = malePopulation;
        this.femalePopulation = femalePopulation;
        this.growthRate = growthRate;
    }

    // ... 省略 getter 和 setter 方法

    @Override
    public String toString() {
        return String.format("省份:%s, 总人口:%d, 男性:%d, 女性:%d, 增长率:%.2f%%",
                provinceName, totalPopulation, malePopulation, femalePopulation, growthRate * 100);
    }
}

步骤3:编写并发爬虫主程序

这是整个项目的核心,我们实现了并发请求、解析和结果收集。

代码语言:txt
复制
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class ConcurrentPopulationCrawler {

    // 线程安全的列表,用于存储爬取结果
    private static final List<ProvincePopulation> RESULT_LIST = new CopyOnWriteArrayList<>();

    // 模拟的API基础URL。真实场景中需替换为真实URL。
    // 这里我们用%s作为省份名的占位符
    private static final String API_URL_TEMPLATE = "https://api.fake-population-data.example/province?name=%s";

    // 需要爬取的省份列表
    private static final String[] PROVINCES = {
            "北京市", "天津市", "上海市", "重庆市",
            "河北省", "山西省", "辽宁省", "吉林省", "黑龙江省",
            "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省",
            "河南省", "湖北省", "湖南省", "广东省", "海南省",
            "四川省", "贵州省", "云南省", "陕西省", "甘肃省",
            "青海省", "台湾省",
            "内蒙古自治区", "广西壮族自治区", "西藏自治区", "宁夏回族自治区", "新疆维吾尔自治区",
            "香港特别行政区", "澳门特别行政区"
    };

    // 代理配置信息
    private static final String proxyHost = "www.16yun.cn";
    private static final int proxyPort = 5445;
    private static final String proxyUser = "16QMSOML";
    private static final String proxyPass = "280651";

    public static void main(String[] args) throws InterruptedException {
        // 1. 创建线程池(假设固定大小为10,可根据网络情况和机器性能调整)
        int threadPoolSize = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);

        // 2. 创建带有代理配置的Http客户端
        OkHttpClient httpClient = createProxyHttpClient();
        ObjectMapper objectMapper = new ObjectMapper();

        // 3. 创建任务列表
        List<Callable<Void>> tasks = new ArrayList<>();
        for (String province : PROVINCES) {
            // 为每个省份创建一个爬虫任务
            Callable<Void> task = () -> {
                try {
                    // 构造完整的请求URL
                    String formattedUrl = String.format(API_URL_TEMPLATE, province);
                    Request request = new Request.Builder()
                            .url(formattedUrl)
                            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
                            .build();

                    // 执行HTTP请求
                    try (Response response = httpClient.newCall(request).execute()) {
                        if (!response.isSuccessful() || response.body() == null) {
                            System.err.println("请求失败: " + response.code() + " - " + province);
                            return null;
                        }

                        // 获取响应体JSON字符串
                        String jsonString = response.body().string();
                        // 使用Jackson将JSON字符串解析为Java对象
                        ProvincePopulation populationData = objectMapper.readValue(jsonString, ProvincePopulation.class);
                        
                        // 将解析好的对象添加到全局结果列表
                        RESULT_LIST.add(populationData);
                        System.out.println("成功爬取: " + province);
                    }
                } catch (Exception e) {
                    System.err.println("处理省份 [" + province + "] 时发生错误: " + e.getMessage());
                    e.printStackTrace();
                }
                return null;
            };
            tasks.add(task);
        }

        // 4. 批量提交任务到线程池,并等待所有任务完成
        System.out.println("开始通过代理并发爬取 " + PROVINCES.length + " 个省份的数据...");
        List<Future<Void>> futures = executorService.invokeAll(tasks);

        // 5. 优雅关闭线程池(等待已提交的任务执行完成)
        executorService.shutdown();
        // 等待所有任务最终完成,设置最大超时时间
        if (!executorService.awaitTermination(1, TimeUnit.MINUTES)) {
            System.err.println("仍有任务未在超时时间内完成!");
        }

        // 6. 输出最终结果
        System.out.println("\n======= 所有省份人口详情爬取完成 =======");
        System.out.println("成功爬取 " + RESULT_LIST.size() + " 条数据:");
        for (ProvincePopulation data : RESULT_LIST) {
            System.out.println(data);
        }
    }

    /**
     * 创建带有代理配置的OkHttpClient
     * 支持需要认证的HTTP代理
     */
    private static OkHttpClient createProxyHttpClient() {
        // 创建代理对象
        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));

        // 创建代理认证器
        Authenticator proxyAuthenticator = new Authenticator() {
            @Override
            public Request authenticate(Route route, Response response) throws IOException {
                if (response.request().header("Proxy-Authorization") != null) {
                    return null; // 如果已经尝试过认证,则放弃
                }
                
                // 构建代理认证信息
                String credential = Credentials.basic(proxyUser, proxyPass);
                return response.request().newBuilder()
                        .header("Proxy-Authorization", credential)
                        .build();
            }
        };

        // 配置OkHttpClient构建器
        return new OkHttpClient.Builder()
                .proxy(proxy)
                .proxyAuthenticator(proxyAuthenticator)
                .connectTimeout(30, TimeUnit.SECONDS) // 连接超时时间
                .readTimeout(30, TimeUnit.SECONDS)    // 读取超时时间
                .writeTimeout(30, TimeUnit.SECONDS)   // 写入超时时间
                .build();
    }
}

// 省份人口实体类(需要单独定义)
class ProvincePopulation {
    private String provinceName;
    private Long totalPopulation;
    private Long malePopulation;
    private Long femalePopulation;
    private Double growthRate;

    // 无参构造器、getter和setter方法
    public ProvincePopulation() {}

    public String getProvinceName() { return provinceName; }
    public void setProvinceName(String provinceName) { this.provinceName = provinceName; }
    public Long getTotalPopulation() { return totalPopulation; }
    public void setTotalPopulation(Long totalPopulation) { this.totalPopulation = totalPopulation; }
    public Long getMalePopulation() { return malePopulation; }
    public void setMalePopulation(Long malePopulation) { this.malePopulation = malePopulation; }
    public Long getFemalePopulation() { return femalePopulation; }
    public void setFemalePopulation(Long femalePopulation) { this.femalePopulation = femalePopulation; }
    public Double getGrowthRate() { return growthRate; }
    public void setGrowthRate(Double growthRate) { this.growthRate = growthRate; }

    @Override
    public String toString() {
        return String.format("省份:%s, 总人口:%d, 男性:%d, 女性:%d, 增长率:%.2f%%",
                provinceName, totalPopulation, malePopulation, femalePopulation, 
                growthRate != null ? growthRate * 100 : 0);
    }
}

步骤4:模拟服务与测试

由于没有真实API,我们可以写一个简单的模拟HTTP服务器(例如使用Spring Boot)来返回模拟的JSON数据,以便测试我们的爬虫。

模拟的JSON响应示例 (对于 北京市):

代码语言:txt
复制
{
  "provinceName": "北京市",
  "totalPopulation": 21890000,
  "malePopulation": 11180000,
  "femalePopulation": 10710000,
  "growthRate": 0.0112
}

三、优化与注意事项

  1. 控制并发度与礼貌爬取FixedThreadPool的大小直接决定了最大并发请求数。请不要设置过大(如100+),这可能会对你自己的网络或目标服务器造成巨大压力,甚至被视为DDoS攻击。礼貌爬取是首要原则。
  2. 异常处理:代码中必须进行完善的异常处理(try-catch),确保一个请求的失败不会影响整个爬虫任务的执行。
  3. 连接池管理OkHttpClient实例应被复用,它内部自带连接池,可以有效地减少TCP握手次数,提升性能。为每个请求创建一个新的Client是极其低效的做法。
  4. 结果收集的线程安全:多个线程同时向同一个集合写入数据时必须使用线程安全的集合类,如CopyOnWriteArrayListCollections.synchronizedList(),否则会导致数据错乱或丢失。

添加请求头:许多现代API要求校验User-Agent等请求头。在实际应用中,你需要在Request.Builder中添加适当的请求头来模拟真实浏览器行为。

代码语言:txt
复制
Request request = new Request.Builder()
    .url(formattedUrl)
    .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) ...")
    .build();

  1. 性能监控:可以添加简单的计时逻辑来评估并发带来的性能提升。

结论

通过结合Java强大的并发编程能力与OkHttp、Jackson等高效库,我们成功地构建了一个高性能的并发爬虫。这套架构不仅适用于爬取省份人口数据,经过简单的修改,完全可以复用于其他需要批量获取网络数据的场景,如商品价格监控、新闻聚合、社交媒体分析等。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、技术选型与设计思路
  • 二、实现步骤与代码详解
    • 步骤1:引入Maven依赖
    • 步骤2:定义省份数据实体类
    • 步骤3:编写并发爬虫主程序
    • 步骤4:模拟服务与测试
  • 三、优化与注意事项
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档