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));
// 初始 URL
crawl_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
这个爬虫框架提供了基础功能,我们可以根据具体需求扩展其功能。建议在实际使用时添加适当的日志记录、错误处理和遵守目标网站的爬取政策。