我们的目标是高效、稳定、可扩展地获取数据。因此,在技术选型上,我们选择了以下强大的Java库:
整体工作流程设计如下:
Callable
或Runnable
任务,提交给线程池。下面我们通过一个完整的示例代码来演示这一过程。请注意,由于实际的可爬取、返回规范JSON的省份人口API难以公开获得,本例将采用一个模拟服务来演示。核心的并发爬虫逻辑是完全一致且可复用于真实场景的。
首先,在你的pom.xml
文件中添加OkHttp和Jackson的依赖。
<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>
根据预期的JSON数据结构,我们定义一个Java Bean类ProvincePopulation
。
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);
}
}
这是整个项目的核心,我们实现了并发请求、解析和结果收集。
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);
}
}
由于没有真实API,我们可以写一个简单的模拟HTTP服务器(例如使用Spring Boot)来返回模拟的JSON数据,以便测试我们的爬虫。
模拟的JSON响应示例 (对于 北京市
):
{
"provinceName": "北京市",
"totalPopulation": 21890000,
"malePopulation": 11180000,
"femalePopulation": 10710000,
"growthRate": 0.0112
}
FixedThreadPool
的大小直接决定了最大并发请求数。请不要设置过大(如100+),这可能会对你自己的网络或目标服务器造成巨大压力,甚至被视为DDoS攻击。礼貌爬取是首要原则。try-catch
),确保一个请求的失败不会影响整个爬虫任务的执行。OkHttpClient
实例应被复用,它内部自带连接池,可以有效地减少TCP握手次数,提升性能。为每个请求创建一个新的Client是极其低效的做法。CopyOnWriteArrayList
或Collections.synchronizedList()
,否则会导致数据错乱或丢失。添加请求头:许多现代API要求校验User-Agent
等请求头。在实际应用中,你需要在Request.Builder
中添加适当的请求头来模拟真实浏览器行为。
Request request = new Request.Builder()
.url(formattedUrl)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) ...")
.build();
通过结合Java强大的并发编程能力与OkHttp、Jackson等高效库,我们成功地构建了一个高性能的并发爬虫。这套架构不仅适用于爬取省份人口数据,经过简单的修改,完全可以复用于其他需要批量获取网络数据的场景,如商品价格监控、新闻聚合、社交媒体分析等。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。