跳到主要内容

设计爬虫系统

问题

如何用 Rust 设计一个高性能的 Web 爬虫?

答案

架构设计

核心实现

use reqwest::Client;
use scraper::{Html, Selector};
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex, Semaphore};

pub struct Crawler {
client: Client,
visited: Arc<Mutex<HashSet<String>>>,
concurrency: Arc<Semaphore>,
}

impl Crawler {
pub fn new(max_concurrent: usize) -> Self {
Self {
client: Client::builder()
.timeout(std::time::Duration::from_secs(10))
.user_agent("RustCrawler/1.0")
.build()
.unwrap(),
visited: Arc::new(Mutex::new(HashSet::new())),
concurrency: Arc::new(Semaphore::new(max_concurrent)),
}
}

/// 爬取单个页面
async fn fetch(&self, url: &str) -> Result<String, reqwest::Error> {
let _permit = self.concurrency.acquire().await.unwrap();
let resp = self.client.get(url).send().await?;
resp.text().await
}

/// 从 HTML 中提取链接
fn extract_links(html: &str, base_url: &str) -> Vec<String> {
let document = Html::parse_document(html);
let selector = Selector::parse("a[href]").unwrap();

document.select(&selector)
.filter_map(|el| el.value().attr("href"))
.filter_map(|href| {
if href.starts_with("http") {
Some(href.to_string())
} else if href.starts_with('/') {
Some(format!("{}{}", base_url, href))
} else {
None
}
})
.collect()
}

/// BFS 爬取
pub async fn crawl(&self, seed_urls: Vec<String>, max_pages: usize) {
let (tx, mut rx) = mpsc::channel::<String>(1000);

// 初始种子 URL
for url in seed_urls {
let _ = tx.send(url).await;
}

let mut count = 0;
while let Some(url) = rx.recv().await {
if count >= max_pages { break; }

// 去重
{
let mut visited = self.visited.lock().await;
if visited.contains(&url) { continue; }
visited.insert(url.clone());
}

// 爬取
match self.fetch(&url).await {
Ok(html) => {
count += 1;
println!("[{}] Crawled: {}", count, url);

// 提取新链接
let links = Self::extract_links(&html, &url);
for link in links {
let _ = tx.try_send(link);
}
}
Err(e) => eprintln!("Error crawling {}: {}", url, e),
}
}
}
}

关键设计决策

决策选择原因
并发控制Semaphore限制并发请求数
URL 去重HashSet / 布隆过滤器小规模用 Set,大规模用布隆
HTML 解析scraperCSS 选择器,类似 jQuery
HTTP 客户端reqwest异步、连接池、自动重定向
调度BFS(队列)广度优先保证层次
礼貌爬取延迟 + robots.txt尊重网站规则

常见面试问题

Q1: 大规模爬虫如何做 URL 去重?

答案

方案内存误判适用规模
HashSet百万级
布隆过滤器极低有(可控)亿级
Redis Set外部存储分布式
RocksDB磁盘超大规模

布隆过滤器用 bloomfilter crate,设置合适的假阳性率(如 0.01%),10 亿 URL 只需约 1GB 内存。

相关链接