用Java爬取腾讯新闻API接口数据,手把手教你解析JSON和绕过常见反爬(附完整代码)
·
Java实战:腾讯新闻API数据采集与智能解析全攻略
最近在帮朋友做一个新闻聚合项目时,发现腾讯新闻的API接口设计得相当巧妙——既保持了良好的数据开放性,又设置了恰到好处的访问门槛。这让我想起三年前第一次尝试爬取新闻数据时,面对复杂的JSON结构和各种反爬机制的手足无措。本文将分享一套经过实战检验的Java解决方案,不仅能稳定获取数据,还能智能处理各种异常情况。
1. 逆向分析腾讯新闻API接口
打开Chrome开发者工具,进入腾讯新闻娱乐版块页面,向下滚动时会发现一个关键接口:
https://pacaio.match.qq.com/xw/site?ext=ent&channel=ent&num=20&page=1
这个接口有几个值得注意的设计特点:
- 参数动态化 :
_t参数是时间戳,用于防止缓存 - 分页设计 :通过修改
page参数实现分页,但超过100页后会触发频率限制 - 分类标识 :
ext和channel参数共同决定新闻分类
通过反复测试,我整理出各分类对应的参数组合:
| 分类 | ext参数 | channel参数 |
|---|---|---|
| 娱乐 | ent | ent |
| 体育 | sports | sports |
| 财经 | finance | finance |
| 科技 | tech | tech |
提示:实际请求时需要添加完整的User-Agent和必要的Cookie,否则会返回403状态码
2. 构建稳健的Java请求模块
直接使用原生HttpURLConnection虽然可行,但在实际项目中我更推荐使用OkHttp3。下面是一个经过生产环境验证的请求工具类:
public class NewsFetcher {
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(new RetryInterceptor(3))
.build();
public static String fetchNewsList(String category, int page) throws IOException {
String url = String.format("https://pacaio.match.qq.com/xw/site?ext=%s&channel=%s&num=20&page=%d&_t=%d",
category, category, page, System.currentTimeMillis()/1000);
Request request = new Request.Builder()
.url(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.header("Referer", "https://xw.qq.com/")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
return response.body().string();
}
}
}
class RetryInterceptor implements Interceptor {
private int maxRetries;
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException exception = null;
for (int i = 0; i <= maxRetries; i++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
} catch (IOException e) {
exception = e;
}
if (i < maxRetries) {
try {
Thread.sleep(1000 * (i + 1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
throw exception != null ? exception : new IOException("Max retries reached");
}
}
这个实现有几个关键优化点:
- 连接池管理 :OkHttpClient自动维护连接池,比每次新建HttpURLConnection高效
- 超时控制 :区分连接超时和读取超时
- 自动重试 :对网络波动导致的失败自动重试3次
- 异常处理 :统一处理各种HTTP异常状态
3. 复杂JSON结构解析技巧
腾讯新闻返回的JSON数据结构嵌套较深,使用Jackson库可以更优雅地处理。首先定义对应的Java模型:
@JsonIgnoreProperties(ignoreUnknown = true)
public class NewsItem {
private String app_id;
private String title;
private String source;
private String url;
private String update_time;
private int comment_num;
private List<String> multi_imgs;
// getters and setters
}
public class NewsResponse {
private int ret;
private String msg;
private List<NewsItem> data;
// getters and setters
}
解析时可以充分利用Jackson的TypeReference特性:
public class NewsParser {
private static final ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static List<NewsItem> parseNewsList(String json) throws IOException {
NewsResponse response = mapper.readValue(json, NewsResponse.class);
if (response.getRet() != 0) {
throw new IOException("API error: " + response.getMsg());
}
return response.getData();
}
}
遇到的一些特殊处理场景:
- 日期格式转换 :腾讯新闻使用多种时间格式,需要自定义反序列化器
- 空字段处理 :配置FAIL_ON_UNKNOWN_PROPERTIES为false避免解析失败
- 数据校验 :检查ret字段确保接口返回正常
4. 高级反爬应对策略
经过多次测试,我发现腾讯新闻主要采用以下几种反爬机制:
- 请求频率限制 :单个IP超过10次/分钟会临时封禁
- 请求头验证 :缺少Referer或User-Agent直接拒绝
- Cookie验证 :部分接口需要携带特定Cookie
- 行为检测 :连续相同参数的请求会触发验证
应对方案可以这样实现:
public class AntiAntiCrawler {
private static final List<String> USER_AGENTS = Arrays.asList(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)"
);
private static final List<String> REFERERS = Arrays.asList(
"https://xw.qq.com/",
"https://www.google.com/",
"https://www.baidu.com/"
);
public static Request.Builder addAntiCrawlerHeaders(Request.Builder builder) {
Random random = new Random();
return builder
.header("User-Agent", USER_AGENTS.get(random.nextInt(USER_AGENTS.size())))
.header("Referer", REFERERS.get(random.nextInt(REFERERS.size())))
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
.header("X-Requested-With", "XMLHttpRequest");
}
public static void randomDelay() {
try {
Thread.sleep(1000 + new Random().nextInt(2000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
实际调用时:
Request request = AntiAntiCrawler.addAntiCrawlerHeaders(new Request.Builder())
.url(url)
.build();
AntiAntiCrawler.randomDelay();
Response response = client.newCall(request).execute();
5. 新闻正文提取的进阶方案
获取到新闻列表后,正文提取是个更大的挑战。腾讯新闻的正文页面有三种渲染方式:
- 静态HTML :直接包含在页面中
- JSON内嵌 :藏在script标签的JavaScript变量里
- 动态加载 :通过XHR二次请求
这里分享一个能处理所有情况的通用方案:
public class ContentExtractor {
private static final Pattern JSON_CONTENT_PATTERN =
Pattern.compile("window\\.__INIT_DATA__\\s*=\\s*(\\{.*?\\})", Pattern.DOTALL);
public static String extractContent(String html) {
// 尝试匹配JSON内容
Matcher matcher = JSON_CONTENT_PATTERN.matcher(html);
if (matcher.find()) {
try {
JSONObject json = new JSONObject(matcher.group(1));
return parseJsonContent(json);
} catch (JSONException e) {
// 降级处理
}
}
// 降级到HTML解析
return parseHtmlContent(html);
}
private static String parseJsonContent(JSONObject json) {
// 实现JSON结构解析逻辑
}
private static String parseHtmlContent(String html) {
Document doc = Jsoup.parse(html);
Elements paragraphs = doc.select(".article-content p");
return paragraphs.stream()
.map(Element::text)
.collect(Collectors.joining("\n\n"));
}
}
这个方案的优势在于:
- 多层降级 :优先解析JSON,失败后降级到HTML解析
- 健壮性强 :能应对各种页面结构变化
- 维护方便 :各解析逻辑相互独立
6. 数据存储与更新策略
最后分享下我在实际项目中的存储方案设计。使用Spring Data JPA + 定时任务的架构:
@Entity
public class NewsArticle {
@Id
private String articleId;
private String title;
private String source;
@Column(columnDefinition = "TEXT")
private String content;
@Temporal(TemporalType.TIMESTAMP)
private Date publishTime;
private Integer commentCount;
@ElementCollection
private List<String> imageUrls;
// 自动设置更新时间
@PreUpdate
public void preUpdate() {
this.updateTime = new Date();
}
}
public interface NewsRepository extends JpaRepository<NewsArticle, String> {
@Query("SELECT n FROM NewsArticle n WHERE n.publishTime > :since")
List<NewsArticle> findRecentNews(@Param("since") Date since);
}
@Scheduled(fixedRate = 30 * 60 * 1000)
public void scheduledNewsUpdate() {
List<String> categories = Arrays.asList("ent", "sports", "finance", "tech");
categories.forEach(category -> {
try {
String json = NewsFetcher.fetchNewsList(category, 1);
List<NewsItem> items = NewsParser.parseNewsList(json);
items.forEach(item -> {
if (!newsRepository.existsById(item.getApp_id())) {
String content = fetchAndParseContent(item.getUrl());
NewsArticle article = convertToArticle(item, content);
newsRepository.save(article);
}
});
} catch (IOException e) {
log.error("Failed to update news for category: " + category, e);
}
});
}
这套存储方案有几个设计亮点:
- 去重设计 :基于articleId防止重复存储
- 增量更新 :只抓取最新数据
- 异常隔离 :单个分类失败不影响其他分类
- 自动维护 :定时任务保持数据新鲜度
在数据量较大时(超过10万条),可以考虑添加Elasticsearch作为全文检索引擎,提升查询效率。
更多推荐

所有评论(0)