Rust 语言在爬虫领域的应用相对较少,尽管 Rust 的 async/await
已稳定,但其与线程安全、Pin
等概念的结合仍较复杂,而爬虫高度依赖并发处理,进一步提高了开发成本。这就导致了使用Rust语言爬虫用的人很少。
下面是一个使用 Rust 编写的异步爬虫示例,支持并发请求、深度控制和去重功能。该爬虫使用 Tokio 作为异步运行时,Reqwest 处理 HTTP 请求,Select 解析 HTML。
use std::{collections::HashSet, sync::Arc, time::Duration};use select::{document::Document,predicate::{Name, Attr},
};
use tokio::{sync::{Mutex, Semaphore},time,
};
use url::Url;// 爬虫配置
const MAX_DEPTH: usize = 3; // 最大爬取深度
const MAX_PAGES: usize = 50; // 最大爬取页面数
const MAX_CONCURRENT_REQUESTS: usize = 10; // 最大并发请求数
const USER_AGENT: &str = "Mozilla/5.0 (compatible; AsyncCrawler/1.0)";#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {let start_url = "https://www.rust-lang.org/";println!("Starting crawl from: {}", start_url);// 共享状态let visited = Arc::new(Mutex::new(HashSet::new()));let page_count = Arc::new(Mutex::new(0));let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS));// 初始 URLcrawl_page(start_url.to_string(),0,visited.clone(),page_count.clone(),semaphore.clone(),).await?;println!("Crawling completed!");Ok(())
}/// 爬取单个页面
async fn crawl_page(url: String,depth: usize,visited: Arc<Mutex<HashSet<String>>>,page_count: Arc<Mutex<usize>>,semaphore: Arc<Semaphore>,
) -> Result<(), Box<dyn std::error::Error>> {// 检查深度限制if depth > MAX_DEPTH {return Ok(());}// 检查是否已访问{let mut visited_set = visited.lock().await;if visited_set.contains(&url) {return Ok(());}visited_set.insert(url.clone());}// 获取信号量许可 (控制并发)let _permit = semaphore.acquire().await?;// 创建 HTTP 客户端let client = reqwest::Client::builder().user_agent(USER_AGENT).timeout(Duration::from_secs(5)).build()?;// 发送请求let response = match client.get(&url).send().await {Ok(res) => res,Err(e) => {eprintln!("Request failed: {} - {}", url, e);return Ok(());}};// 检查状态码if !response.status().is_success() {eprintln!("HTTP error: {} - {}", url, response.status());return Ok(());}// 获取页面内容let html = match response.text().await {Ok(html) => html,Err(e) => {eprintln!("Failed to get text: {} - {}", url, e);return Ok(());}};// 更新页面计数器let mut count = page_count.lock().await;*count += 1;println!("[{}/{}] Depth {}: {}", *count, MAX_PAGES, depth, url);// 检查页面限制if *count >= MAX_PAGES {return Ok(());}// 解析页面并提取链接let base_url = Url::parse(&url)?;let document = Document::from(html.as_str());let links: Vec<String> = document.find(Name("a")).filter_map(|a| a.attr("href")).filter_map(|href| base_url.join(href).ok()).map(|url| url.to_string()).collect();// 限制请求速率time::sleep(Duration::from_millis(100)).await;// 创建新爬取任务let mut tasks = vec![];for link in links {let visited = visited.clone();let page_count = page_count.clone();let semaphore = semaphore.clone();tasks.push(tokio::spawn(async move {crawl_page(link, depth + 1, visited, page_count, semaphore).await}));}// 等待所有任务完成for task in tasks {let _ = task.await;}Ok(())
}
功能说明
1、异步并发:
- 使用 Tokio 的异步任务 (
tokio::spawn
) - 通过信号量 (
Semaphore
) 限制最大并发请求数
2、爬取控制:
MAX_DEPTH
:限制爬取深度MAX_PAGES
:限制最大页面数- 请求超时设置 (5 秒)
- 请求间延迟 (100ms)
3、智能解析:
- 使用
url
库处理相对/绝对路径 - 通过
select
库解析 HTML 并提取链接 - 只处理
<a>
标签的href
属性
4、状态管理:
- 使用
Mutex
保护共享状态 - 使用
HashSet
记录已访问 URL - 原子计数器跟踪已爬取页面数
使用说明
1、添加依赖到 Cargo.toml
:
[dependencies]
tokio = { version = "1.0", features = ["full"] }
reqwest = "0.11"
select = "0.6"
url = "2.4"
2、可配置参数:
// 在代码顶部修改这些常量:
const MAX_DEPTH: usize = 3; // 最大爬取深度
const MAX_PAGES: usize = 50; // 最大爬取页面数
const MAX_CONCURRENT_REQUESTS: usize = 10; // 并发请求数
const USER_AGENT: &str = "..."; // 自定义 User-Agent
3、运行:
cargo run
这个爬虫框架提供了基础功能,我们可以根据具体需求扩展其功能。建议在实际使用时添加适当的日志记录、错误处理和遵守目标网站的爬取政策。