重構 Material Design 風格新聞 App 時,需要穩定的數據源。參考知乎日報、鳳凰新聞等公開 API 固然方便,但為了鍛鍊技術並掌握數據掌控權,決定自建爬蟲服務:抓取目標網站新聞列表與詳情頁,解析存入數據庫,再暴露 RESTful API 供 App 調用。
@Scheduled 或 Quartz 定時抓取增量數據。public static String doGet(String urlStr) throws CrawlerException {
HttpURLConnection conn = null;
try {
URL url = new URL(urlStr);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; NewsBot/1.0)");
if (conn.getResponseCode() == 200) {
return IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8);
}
throw new CrawlerException("HTTP 狀態碼: " + conn.getResponseCode());
} catch (IOException e) {
throw new CrawlerException("請求失敗: " + urlStr, e);
} finally {
if (conn != null) conn.disconnect();
}
}
工具類:使用 IOUtils (commons-io) 或手寫流轉字符串,注意編碼處理。
目標頁面結構示例:
<div id="article">
<div id="article_title"><h1><a href="...">標題內容</a></h1></div>
<div id="article_detail">
<span>2024-01-15</span>
<span>來源: 新華網</span>
<span>閱讀: <script src="/click?id=7428"></script> 477</span>
</div>
<div id="article_content"><p>正文內容...<img src="/uploads/1.jpg"/></p></div>
</div>
Document doc = Jsoup.parse(html);
Element article = doc.getElementById("article");
// 標題
String title = article.getElementById("article_title").text();
// 詳情區:三個 span
Element detail = article.getElementById("article_detail");
Elements spans = detail.getElementsByTag("span");
String publishDate = spans.get(0).text(); // "2024-01-15"
String source = spans.get(1).text().replace("來源: ", ""); // "新華網"
閱讀數由 JS 請求 /click?id=7428 動態寫入,Jsoup 無法執行 JS。解決方案:直接請求該接口,正則提取數字。
String clickUrl = "https://site.com/click?id=" + newsId;
String jsContent = doGet(clickUrl); // 返回 "document.write(478)"
int readCount = Integer.parseInt(jsContent.replaceAll("\\D+", ""));
Element contentEl = article.getElementById("article_content");
String bodyHtml = contentEl.html(); // 保留標籤供 WebView 富文本展示
// 提取圖片 URL(相對路徑需補全域名)
Elements imgs = contentEl.getElementsByTag("img");
List imgUrls = new ArrayList<>();
for (Element img : imgs) {
String src = img.attr("src");
if (!src.startsWith("http")) src = BASE_URL + src;
imgUrls.add(src);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NewsArticle {
private int id;
private String title;
private String publishDate;
private String source;
private int readCount;
private String bodyHtml;
private List imageUrls;
}
public NewsArticle fetchArticle(int newsId) {
String url = BASE_ARTICLE_URL + newsId + ".html";
String html = HttpUtil.doGet(url);
Document doc = Jsoup.parse(html);
Element article = doc.getElementById("article");
if (article == null) throw new CrawlerException("文章不存在: " + newsId);
String title = article.getElementById("article_title").text();
Element detail = article.getElementById("article_detail");
Elements spans = detail.getElementsByTag("span");
String date = spans.get(0).text();
String source = spans.get(1).text().replace("來源: ", "");
String clickHtml = HttpUtil.doGet(CLICK_API + newsId);
int readCount = Integer.parseInt(clickHtml.replaceAll("\\D+", ""));
Element content = article.getElementById("article_content");
String body = content.html();
List images = content.getElementsByTag("img").stream()
.map(img -> resolveUrl(img.attr("src")))
.collect(Collectors.toList());
return new NewsArticle(newsId, title, date, source, readCount, body, images);
}
User-Agent、Referer、Cookie。Thread.sleep(random(1000, 3000)),使用令牌桶限流。robots.txt 協議。Jsoup 以輕量、API 友好、選擇器強大成為 Java 爬蟲首選。結合對目標站點 DOM 結構的精準分析、JS 動態數據的接口逆向、工程化的容錯與限流,即可構建穩定可用的新聞數據服務。後續可擴展為分布式爬蟲框架(如 WebMagic、Crawler4j)應對更複雜場景。