使用Rust async构建高性能爬虫
在之前的三篇文章(异步编程async、[Rust async原理剖析](https://xx/Rust async原理剖析)和async在实际场景中的应用)中,我们从异步编程的基本概念、Rust async的底层原理,最后到async在实际场景中的应用逐步对异步编程进行展开。本篇我们继续围绕异步编程这个主题,将之前介绍的内容应用到一个实战项目中–使用Rust async构建高性能爬虫。
为什么用Rust
Rust是是一门现代系统编程语言,专注于安全性、速度和并发性。它的性能接近C/C++,自身的并发模型和Tokio提供的成熟的异步运行时,能轻松支持百万协程并发,而且其在编译期就能保证内存安全和线程安全,并且无GC风险,避免了爬虫系统中常见的内存泄漏与崩溃风险,所以Rust非常适合开发高性能的爬虫系统。
Rust上述的这些优点也给它带来了一些使用门槛,如果你只是想抓几个页面,并没有太高的性能要求,那Rust可能不太适合你,或许用Python或者Go更能快的满足你的需求。
使用Rust开发爬虫系统涉及到的依赖包如下:
需求 | 依赖包(推荐) |
---|---|
并发运行时 | tokio |
HTTP下载 | reqwest |
HTML解析 | scraper |
URL去重 | dashmap |
架构设计
为了构建一个高性能的异步爬虫系统,对其进行模块化设计,各模块主要包括:
- 请求器:主要用于构建HTTP请求,自定义User_Agent等请求相关的功能
- 请求执行器:主要负责发起HTTP请求,并发设置以及延迟请求等相关执行功能
- 结果解析器:主要负责处理HTML响应,提取目标数据以及新链接处理等功能
- 数据Pipeline:主要负责将结构化的数据进行异步存储
- 任务调度器:主要负责管理爬虫任务的调度
模块实现
请求器(Requester)
请求器模块主要负责构建标准的HTTP请求,这里通过reqwest::RequestBuilder
设置请求头、自定义的User-Agent
等属性。
声明一个Requester
结构体,增加一些请求相关的属性,比如user_agent
和延迟请求时间delay_ms
,示例代码如下:
use reqwest::{Client, RequestBuilder};
use std::time::Duration;
#[derive(Clone)]
pub struct Requester {
client: Client,
pub user_agent: String,
pub delay_ms: u64,
}
impl Requester {
pub fn new(user_agent: &str, delay_ms: u64) -> Self {
let client = Client::builder()
.user_agent(user_agent)
.build()
.unwrap();
Self {
client,
user_agent: user_agent.to_string(),
delay_ms,
}
}
pub fn build_request(&self, url: &str) -> RequestBuilder {
self.client.get(url)
}
}
Code language: PHP (php)
请求执行器(Fetcher)
请求执行器负责异步发起HTTP请求,这里通过tokio
和reqwest
来实现并发请求。
声明一个Fetcher
结构体,通过Requseter
发起请求,示例代码如下:
use crate::requester::Requester;
use tokio::time::{sleep, Duration};
pub struct Fetcher;
impl Fetcher {
pub async fn fetch(requester: &Requester, url: &str) -> Option<String> {
sleep(Duration::from_millis(requester.delay_ms)).await;
let request = requester.build_request(url);
match request.send().await {
Ok(resp) => match resp.text().await {
Ok(body) => Some(body),
Err(e) => {
eprintln!("failed to read response: {e:?}");
None
}
},
Err(e) => {
eprintln!("failed to request: {e:?}");
None
}
}
}
}
Code language: PHP (php)
结果解析器(Parser)
结果解析器处理HTML响应,提取目标数据和新的连接,这里使用scraper
进行解析。
由于爬取网站的多样性,将解析器Parser
抽象成一个trait
,通过实现不同的Parser
来完成不同的解析任务。示例代码如下:
use async_trait::async_trait;
#[async_trait]
pub trait Parser: Send + Sync {
async fn parse(&self, html: &str) -> ParseResult;
}
pub struct ParseResult {
pub data: Vec<String>,
pub new_links: Vec<String>
}
Code language: PHP (php)
数据Pipeline(Pipeliner)
数据Pipeline负责将结构化的数据进行存储,例如存储到数据库。
由于存储类型的多样性,也将Pipeliner
声明为一个trait
,示例代码如下:
use async_trait::async_trait;
#[async_trait]
pub trait Pipeline: Send + Sync {
async fn process(&self, data: Vec<T>);
}
Code language: PHP (php)
任务调度器(Scheduler)
任务调度器主要负责爬虫任务的调度,是其核心模块,这里使用tokio::sync::mpsc
作为任务队列,并使用dashmap
对URL去重。
use tokio::sync::mpsc::{self, Sender, Receiver};
use dashmap::DashSet;
use std::sync::Arc;
pub struct Scheduler {
seen: Arc<DashSet<String>>,
sender: Sender<String>,
}
impl Scheduler {
pub fn new(sender: Sender<String>) -> Self {
Self {
seen: Arc::new(DashSet::new()),
sender,
}
}
pub fn try_enqueue(&self, url: String) {
if self.seen.insert(url.clone()) {
let _ = self.sender.try_send(url);
}
}
}
Code language: PHP (php)
健壮性设计
上述模块只是爬虫系统中的基础模块,但是为了保证爬虫系统能长时间的稳定高效的自动运行,还需要在以下几个方面进行完善,保证系统的健壮性。
- 重试机制
由于网络不稳定或者链接失效,请求失败是很常见的,所以需要对这种情况进行处理,使其能够进行自动重试并记录失效链接。 - 任务隔离
爬虫系统中会有很多爬取任务,避免各个任务任务之间相互干扰而影响整个系统,可以使用tokio::spawn
对任务进行隔离。 - 链接校验
在爬取过程中,会遇到很多URL链接,为了避免引起过多的重试机制,在爬取之前,应该校验URL是否合法并且完整。
性能提升
随着爬虫系统的长期稳定运行以及需求的不断增加,需要进一步提升爬虫的性能,可以从以下几个方面进行性能提升。
- 连接复用
reqwest
默认基于hyper
支持连接池,可直接使用其进行TCP复用连接,减少HTTP请求的连接。 - 多任务调度
将爬虫中的各模块中的任务调度拆分为多个任务队列,提升各自调度的吞吐量。 - 分布式部署
对整个爬虫系统进行分布式改造,进行多实例部署,基于消息队列对URL进行分发,各个节点并行爬取。 - 增量爬取
避免任务中途失败,任务重启之后从新进行爬取,可将其爬取进度保存下来,以便进行增量爬取。
总结
Rust由于专注于安全性、速度和并发性,为其提供了系统级的性能和内存安全保障,而且Rust还具有丰富的社区生态,使开发者能够方便高效的构建高性能、稳定和可扩展的爬虫系统。
本文通过对爬虫系统进行了内部分析,并将其拆分多个模块,随后通过完善其中的一些问题,增强系统的健壮性,最后根据一些优化策略来提升系统的性能,以便适应更多的需求。