採用兩個 Map<String, Boolean> 追蹤狀態:
Boolean 表示是否已遍歷。流程:
oldMap(值為 false)。oldMap 中 false 的連結,發送 HTTP GET。<a href="..."> 連結。/)。oldMap/newMap 出現,加入 newMap。true(已遍歷)。newMap 不為空,合併進 oldMap 並遞迴下一輪;否則結束。package action;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.*;
public class WebCrawlerDemo {
public static void main(String[] args) {
new WebCrawlerDemo().myPrint("https://www.example.com");
}
public void myPrint(String baseUrl) {
Map<String, Boolean> oldMap = new LinkedHashMap<>();
String host = extractHost(baseUrl);
oldMap.put(baseUrl, false);
oldMap = crawlLinks(host, oldMap);
oldMap.forEach((url, visited) -> System.out.println("連結:" + url));
}
private String extractHost(String url) {
Pattern p = Pattern.compile("(https?://)?[^/\\s]*");
Matcher m = p.matcher(url);
return m.find() ? m.group() : "";
}
private Map<String, Boolean> crawlLinks(String host, Map<String, Boolean> oldMap) {
Map<String, Boolean> newMap = new LinkedHashMap<>();
Pattern linkPattern = Pattern.compile("<a.*?href=[\"']?((https?://)?/?[^\"']+)[\"']?.*?>(.+)</a>");
for (Map.Entry<String, Boolean> entry : oldMap.entrySet()) {
if (entry.getValue()) continue; // 已遍歷
String currentUrl = entry.getKey();
try {
URL url = new URL(currentUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(2000);
conn.setReadTimeout(2000);
if (conn.getResponseCode() == 200) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = linkPattern.matcher(line);
while (matcher.find()) {
String newLink = matcher.group(1).trim();
// 正規化
if (!newLink.startsWith("http")) {
newLink = newLink.startsWith("/") ? host + newLink : host + "/" + newLink;
}
if (newLink.endsWith("/")) newLink = newLink.substring(0, newLink.length() - 1);
// 去重且同網域
if (!oldMap.containsKey(newLink) && !newMap.containsKey(newLink)
&& newLink.startsWith(host)) {
newMap.put(newLink, false);
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
oldMap.replace(currentUrl, false, true); // 標記已遍歷
}
if (!newMap.isEmpty()) {
oldMap.putAll(newMap);
return crawlLinks(host, oldMap); // 遞迴下一輪
}
return oldMap;
}
}
連結:https://www.example.com
連結:https://www.example.com/about
連結:https://www.example.com/contact
連結:https://www.example.com/products
...(依網站規模輸出所有可達連結)
| 問題 | 改進方向 |
|---|---|
| 遞迴深度過大導致 StackOverflow | 改用迴圈 + Queue 實作 BFS,移除遞迴。 |
| 單執行緒速度慢 | 引入執行緒池(ExecutorService)並發抓取,需加鎖保護 Map。 |
| 記憶體隨連結數增長 | 若連結超過 10 萬,建議改用資料庫或 Bloom Filter 去重。 |
| 正則解析 HTML 不穩定 | 改用 Jsoup 解析 DOM,更健壯。 |
| 無 robots.txt / 禮貌性控制 | 加入 robots.txt 解析與 Crawl-Delay 遵守。 |
本文示範以原生 HttpURLConnection + 正則 + BFS 實作簡易爬蟲,適合理解原理與小規模抓取。生產環境建議採用 Jsoup、HttpClient、多執行緒、分散式佇列(如 Redis + Kafka)等成熟元件,並嚴格遵守目標網站爬蟲協議。