1. 为什么选择Jsoup+OkHttp组合
在Java生态中做爬虫开发,工具选型直接影响开发效率和项目成败。经过多年实战验证,Jsoup+OkHttp的组合已经成为Java爬虫领域的黄金搭档。这个组合的巧妙之处在于:OkHttp专注网络请求处理,Jsoup专注HTML解析,两者各司其职又完美互补。
OkHttp作为Square公司开发的HTTP客户端,其优势在于:
- 连接池复用机制降低延迟(实测比HttpURLConnection快3倍以上)
- 透明的GZIP压缩处理
- 完善的Cookie持久化支持
- 灵活的拦截器机制
而Jsoup作为HTML解析利器,其核心价值在于:
- 类似jQuery的DOM操作API(用过前端开发的会非常熟悉)
- 内置的HTML清理机制(自动修正残缺标签)
- 强大的CSS选择器支持
- 白名单机制的XSS防护
我曾在电商价格监控项目中对比过多种方案,最终这个组合的代码可维护性和运行效率都完胜其他方案。比如用纯HttpClient+正则表达式的方式,开发一个商品详情页解析器需要200行代码,而用Jsoup+OkHttp只需50行,且后期需求变更时修改量减少80%。
2. 环境准备与基础配置
2.1 Maven依赖配置
建议使用最新稳定版本(截至2023年8月):
xml复制<dependencies>
<!-- OkHttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
<!-- Jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
</dependency>
</dependencies>
注意:实际项目中建议结合Spring Boot的dependency-management统一管理版本号,避免依赖冲突。
2.2 基础工具类封装
先创建一个HttpUtil工具类处理通用逻辑:
java复制public class HttpUtil {
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
public static String get(String url, Map<String, String> headers) throws IOException {
Request.Builder builder = new Request.Builder().url(url);
if (headers != null) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response.body().string();
}
}
}
这个基础版本已经包含:
- 连接超时和读取超时设置
- 自动重试机制
- 灵活的请求头配置
- 响应状态码检查
3. 实战:电商价格监控爬虫
3.1 目标网站分析
以京东商品页为例(实际项目请遵守robots.txt规则),我们需要提取:
- 商品标题
- 当前价格
- 促销信息
- 商品评价数
使用Chrome开发者工具分析页面结构:
- 商品标题:位于
<div class="sku-name"> - 价格:
<span class="price J-p-100000123">格式 - 促销:
<div class="prom-tips">下的<span>标签 - 评价数:
<a href="#comment" id="comment-count">
3.2 页面解析实现
java复制public class JdProductParser {
public static Product parse(String html) {
Document doc = Jsoup.parse(html);
Product product = new Product();
// 标题提取(处理可能的多余空白)
product.setTitle(doc.selectFirst("div.sku-name").text().trim());
// 价格提取(注意价格可能是动态加载的)
Element priceEl = doc.selectFirst("span.price");
if (priceEl != null) {
product.setPrice(new BigDecimal(priceEl.text().replace("¥", "")));
}
// 促销信息(可能有多条)
Elements promItems = doc.select("div.prom-tips > span");
product.setPromotions(promItems.eachText());
// 评价数提取
Element commentEl = doc.selectFirst("#comment-count");
if (commentEl != null) {
String commentText = commentEl.text();
product.setCommentCount(parseCommentCount(commentText));
}
return product;
}
private static int parseCommentCount(String text) {
// 处理"20万+"这类格式
if (text.contains("万+")) {
return (int)(Float.parseFloat(text.replace("万+", "")) * 10000);
}
return Integer.parseInt(text.replace("+", ""));
}
}
3.3 反爬应对策略
3.3.1 请求头伪装
java复制public static Map<String, String> createCommonHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.put("Accept", "text/html,application/xhtml+xml");
headers.put("Accept-Language", "zh-CN,zh;q=0.9");
return headers;
}
3.3.2 IP轮换与限速
建议使用以下策略:
java复制// 在OkHttpClient配置中添加代理
client = new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080)))
// ...其他配置
.build();
// 限速控制
public static void rateLimit() {
try {
Thread.sleep(1000 + new Random().nextInt(1000)); // 1-2秒随机间隔
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
4. 高级技巧与性能优化
4.1 连接池优化
java复制public static OkHttpClient createOptimizedClient() {
return new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
.dispatcher(new Dispatcher(new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
)))
.build();
}
关键参数说明:
- 连接池大小50:适合中等规模爬虫
- 空闲连接保持5分钟:平衡资源占用和重用
- 线程池配置:根据服务器性能调整
4.2 异步请求模式
java复制public static void asyncFetch(List<String> urls, Consumer<String> callback) {
OkHttpClient client = createOptimizedClient();
CountDownLatch latch = new CountDownLatch(urls.size());
for (String url : urls) {
Request request = new Request.Builder()
.url(url)
.headers(Headers.of(createCommonHeaders()))
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
latch.countDown();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
callback.accept(response.body().string());
} finally {
latch.countDown();
}
}
});
}
try {
latch.await(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
4.3 断点续爬实现
基于文件存储的简单实现:
java复制public class CrawlStateManager {
private static final String STATE_FILE = "crawl_state.dat";
public static void saveState(Set<String> crawledUrls) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(STATE_FILE))) {
oos.writeObject(crawledUrls);
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
public static Set<String> loadState() {
File file = new File(STATE_FILE);
if (!file.exists()) return new HashSet<>();
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(file))) {
return (Set<String>) ois.readObject();
} catch (Exception e) {
return new HashSet<>();
}
}
}
5. 常见问题排查指南
5.1 乱码问题解决
典型症状:获取的中文内容显示为乱码
解决方案:
java复制// 在获取响应时指定编码
public static String getWithCharset(String url, String charset) throws IOException {
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return new String(response.body().bytes(), charset);
}
}
常见编码:
- GBK:部分老网站
- GB2312:中文传统编码
- UTF-8:现代网站主流
5.2 动态内容加载
对于JavaScript渲染的内容,可以考虑:
- 分析AJAX接口直接调用
- 使用无头浏览器方案(如Selenium)
- 查找隐藏在HTML中的JSON数据
示例:提取页面中的JSON数据
java复制public static JSONObject extractJsonFromScript(Document doc, String id) {
Element script = doc.selectFirst("script#" + id);
if (script != null) {
String jsonStr = script.html()
.replace("var data =", "")
.trim()
.replaceAll(";$", "");
return new JSONObject(jsonStr);
}
return null;
}
5.3 内存泄漏预防
关键措施:
- 确保关闭Response对象
java复制try (Response response = client.newCall(request).execute()) {
// 处理响应
}
- 限制下载内容大小
java复制client = new OkHttpClient.Builder()
.callTimeout(30, TimeUnit.SECONDS)
.build();
- 定期清理Jsoup文档对象
java复制doc.clearAttributes();
doc = null;
System.gc();
