網頁爬蟲本質是**有向圖遍歷**:網頁 = 節點,超連結 = 邊。核心流程:
| 策略 | 特點 | 優點 | 缺點 |
|---|---|---|---|
| BFS (佇列) | 層層推進 | 重要頁面通常離種子近、最短路徑、利於分散式 | 佇列佔記憶體、需去重集合 |
| DFS (棧/遞歸) | 一條路走到黑 | 實現簡單、記憶體較小 | 易陷入深度陷阱、黑洞、重要頁面延遲 |
**主流爬蟲均採用 BFS + 優先級佇列** (PageRank、更新頻率、權重評分)。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>org.htmlparser</groupId>
<artifactId>htmlparser</artifactId>
<version>2.1</version>
</dependency>
public class UrlQueue {
private final Queue<String> unvisited = new ConcurrentLinkedQueue<>();
private final Set<String> visited = ConcurrentHashMap.newKeySet();
private final int maxSize;
public UrlQueue(int maxSize) { this.maxSize = maxSize; }
public void addSeed(String... seeds) {
for (String s : seeds) addUnvisited(s);
}
public synchronized void addUnvisited(String url) {
if (url == null || url.isBlank()) return;
url = normalize(url);
if (!visited.contains(url) && !unvisited.contains(url)) {
unvisited.offer(url);
}
}
public String poll() {
String url = unvisited.poll();
if (url != null) visited.add(url);
return url;
}
public boolean isEmpty() { return unvisited.isEmpty(); }
public int visitedCount() { return visited.size(); }
public boolean isFinished() { return visitedCount() >= maxSize; }
private String normalize(String url) {
// 移除 fragment、統一協議大小寫、補全相對路徑等
return url.split("#")[0].trim();
}
}
public class PageDownloader {
private final CloseableHttpClient client;
private final String userAgent = "Mozilla/5.0 (compatible; MyBot/1.0)";
public PageDownloader() {
this.client = HttpClients.custom()
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(10000)
.build())
.setUserAgent(userAgent)
.build();
}
public String download(String url) {
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse resp = client.execute(get)) {
int status = resp.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK) return null;
HttpEntity entity = resp.getEntity();
return entity != null ? EntityUtils.toString(entity, "UTF-8") : null;
} catch (IOException e) {
return null; // 記錄日誌
}
}
public void close() throws IOException { client.close(); }
}
public class LinkExtractor {
public Set<String> extract(String baseUrl, String html) {
Set<String> links = new HashSet<>();
try {
Parser parser = new Parser(html);
parser.setEncoding("UTF-8");
NodeFilter aFilter = new TagNameFilter("A");
NodeFilter frameFilter = new NodeFilter() {
@Override public boolean accept(Node node) {
return node.getText().startsWith("frame src=");
}
};
NodeList list = parser.extractAllNodesThatMatch(new OrFilter(aFilter, frameFilter));
for (int i = 0; i < list.size(); i++) {
Node node = list.elementAt(i);
String href = null;
if (node instanceof LinkTag) {
href = ((LinkTag) node).getLink();
} else { // frame
String text = node.getText();
int start = text.indexOf("src=") + 5;
int end = text.indexOf(' ', start);
if (end == -1) end = text.indexOf('>', start);
href = text.substring(start, end).replace("\"", "");
}
if (href != null) {
links.add(resolve(baseUrl, href));
}
}
} catch (ParserException e) {
// 記錄
}
return links;
}
private String resolve(String base, String href) {
try {
return new URL(new URL(base), href).toString();
} catch (MalformedURLException e) {
return null;
}
}
}
public class SimpleCrawler {
private final UrlQueue queue;
private final PageDownloader downloader;
private final LinkExtractor extractor;
private final LinkFilter filter; // 只爬特定域名
public SimpleCrawler(String[] seeds, int maxPages, LinkFilter filter) {
this.queue = new UrlQueue(maxPages);
this.queue.addSeed(seeds);
this.downloader = new PageDownloader();
this.extractor = new LinkExtractor();
this.filter = filter;
}
public void start() {
while (!queue.isEmpty() && !queue.isFinished()) {
String url = queue.poll();
if (url == null) continue;
System.out.println("[" + queue.visitedCount() + "] 爬取: " + url);
String html = downloader.download(url);
if (html == null) continue;
// 儲存/處理 html...
savePage(url, html);
// 提取新連結
Set<String> links = extractor.extract(url, html);
for (String link : links) {
if (filter.accept(link)) {
queue.addUnvisited(link);
}
}
// 禮貌延遲
sleep(100);
}
downloader.close();
}
private void savePage(String url, String html) {
// 寫入檔案、資料庫、索引...
}
private void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static void main(String[] args) {
LinkFilter filter = url -> url.startsWith("https://example.com/");
new SimpleCrawler(new String[]{"https://example.com/"}, 1000, filter).start();
}
}
| 維度 | 教學版 | 生產級 |
|---|---|---|
| 去重 | 記憶體 Set | Bloom Filter + Redis/LevelDB/RocksDB |
| 佇列 | ConcurrentLinkedQueue | Kafka / RabbitMQ / Redis List (分散式) |
| 調度 | 單執行緒輪詢 | 多執行緒池 + 優先級佇列 (PriorityBlockingQueue) |
| 限流 | 固定 sleep | Token Bucket / Leaky Bucket 每域名限流 |
| 反爬 | User-Agent | 代理 IP 池、Cookie 管理、Headless Chrome (Selenium/Playwright) |
| 解析 | HtmlParser | Jsoup / XPath / 正則 / JSON API 直取 |
| 存儲 | 檔案 | Elasticsearch / Cassandra / HBase / S3 |
| 監控 | System.out | Prometheus + Grafana (QPS、延遲、錯誤率、去重率) |
robots.txt (User-agent, Disallow, Crawl-delay)Crawl-delay,不造成目標伺服器過載爬蟲核心 = **BFS + 去重 + 下載 + 解析 + 存儲**。教學版幾百行代碼即可跑通流程;生產級需在**分散式架構、智能調度、反反爬、合規治理**上深耕。Java 生態擁有 HttpClient、Jsoup、WebMagic、Nutch 等成熟框架,**建議直接基於 WebMagic/Nutch 二次開發**,而非從零造輪子。