Rust异步爬虫实现与优化

发布于:2025-07-06 ⋅ 阅读:(15) ⋅ 点赞:(0)

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

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