Rust异步爬虫入门

简介

由于Rust语言自身的特性,使用Rust编写爬虫相比起使用Python等语言要繁琐许多。但是,Rust运行速度快、占用资源少的特点使得Rust非常适合执行大规模的爬虫任务。

这里以一个下载Quotes to Scrape网站上所有引语的爬虫为例,简单展现了如何编写一个Rust异步网络爬虫。[项目链接]

依赖

[dependencies]
once_cell = "1.18.0"
reqwest = "0.11.22"
scraper = "0.18.1"
tokio = { version = "1.34.0", features = ["macros", "rt-multi-thread", "sync"] }
url = "2.4.1"

客户端

static URL: Lazy<Url> = Lazy::new(|| Url::parse("https://quotes.toscrape.com/").unwrap());
static CLIENT: Lazy<Client> = Lazy::new(|| {
    use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
    let mut headers = HeaderMap::new();
    let user_agent = HeaderValue::from_static(
        r"Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0",
    );
    headers.insert(USER_AGENT, user_agent);
    Client::builder().default_headers(headers).build().unwrap()
});

通过reqwest的客户端reqwest::Client,程序可以方便地向网络资源发送请求并接受响应。在异步爬虫中,由于协程数量众多,程序需要尽可能减小每个协程占用的内存资源。当每一个爬虫都使用相同的网络配置(例如请求头Headers)时,为节约程序消耗的系统资源,可以让所有爬虫协程使用同一个的客户端发送请求与接受响应。由于在Rust中异步的过程对其中用到的变量的生命周期有着严格的限制,因此我们使用lazy_static来创建一个全局静态客户端用于执行爬虫的任务。

reqwest中,客户端通过reqwest::ClientBuilderreqwest::Client::builder()进行配置,reqwest::Client::builder()函数创建一个reqwest::ClientBuilder的实例。可以配置的选项包括但不限于请求头与客户端的连接配置[文档链接]。在该样例程序中,客户端加入了User-Agent请求头。其他所需的请求头的类型与值可以通过事先通过浏览器或者查阅MDN文档获得。

下载

async fn download_quote_html(idx: usize) -> reqwest::Result<String> {
    let page_url = URL.join(&format!("page/{}/", idx)).unwrap();
    let res = CLIENT.get(page_url).send().await?;
    let html = res.text().await?;
    Ok(html)
}

下载页面之前,先根据网站的结构生成相应页面的URL,在使用客户端进行下载即可。通过使用异步函数来使下载的过程不会发生阻塞,在大规模爬虫中可以有效缩短工作时间,加快爬取速度。

使用CSS进行选择

static QUOTE: Lazy<Selector> = Lazy::new(|| Selector::parse(r#".quote"#).unwrap());
static TEXT: Lazy<Selector> = Lazy::new(|| Selector::parse(r#".text"#).unwrap());
static AUTHOR: Lazy<Selector> = Lazy::new(|| Selector::parse(r#".author"#).unwrap());
static TAG: Lazy<Selector> = Lazy::new(|| Selector::parse(r#".tag"#).unwrap());
fn parse_quote_html(page: Html) -> Vec<Quote> {
    page.select(&QUOTE)
        .map(|quote| Quote {
            text: quote.select(&TEXT).next().unwrap().inner_html(),
            author: quote.select(&AUTHOR).next().unwrap().inner_html(),
            tags: quote.select(&TAG).map(|e| e.inner_html()).collect(),
        })
        .collect()
}

从下载的页面的HTML中提取出所有的引语。编写选择器(scraper::Selector)需要CSS相关知识,具体可以参考W3School上的教程文档

主函数

#[tokio::main]
async fn main() {
    let pool = Arc::new(Semaphore::new(MAX_TASK));
    let (tx, mut rx) = mpsc::unbounded_channel::<Quote>();

    for page in 1..20 {
        let pool = Arc::clone(&pool);
        let tx = tx.clone();
        tokio::spawn(async move {
            let _permit = pool.acquire().await.unwrap();
            let text = download_quote_html(page).await.unwrap();
            let html = Html::parse_document(&text);
            let quotes = parse_quote_html(html);
            for quote in quotes.into_iter() {
                tx.send(quote).unwrap();
            }
        });
    }
    drop(tx);

    while let Some(quote) = rx.recv().await {
        println!("{:?}", quote);
    }
}

主函数首先创建了运行爬虫所需的异步运行时,用于限制并发数量的协程池和用于收集爬虫结果的管道。该程序对需要被爬取的每一个页面创建一个协程用以下载全部的引言,再将下载后的引言通过管道发送给消费者。主函数在启动全部的下载协程后开始收集各个协程发送的引言数据。当所有协程执行完毕后,接受端不再有可能收集到更多数据,程序退出。

运行时的配置取决于任务的需求以及机器的配置,通常情况下使用Tokio提供的默认配置即可。

线程池的大小根据网络状况和硬件限制等因素确定,对于异步协程爬虫而言,这个数值一般可以设置为一个较大的数字而又不会消耗很多系统资源,但协程的数量显然不应超过网站要求或承受能力的限制。

当协程数量较多时,视需求可以考虑将无限制的管道改成限流管道(tokio::sync::mpsc::channel()),防止协程下载过多的数据发生OOM等错误。

该程序出于演示目的并未进行任何错误处理,实际爬虫项目还需要在发生错误时进行记录或者从错误中恢复。

驱动

会忘记吗?自己一直以来所珍重的。一度以为应当终生为之奋斗的目标,回首而却发现那早已不再重要。曾经渴望改变一切的愿景,是那么的遥远而显得幼稚。究竟是梦想褪去了颜色,还是被天空染成了同样的蔚蓝。

幸福的基石,在运势与决策之外,更依赖无数底层的零件。然而在我们所不能看到的地方,同一片天空下的素不相识之人,或许正遭受的命运女神编织的恶梦。那些陌生的角落里发生的不幸,即使星辰听闻难免也会为之落泪。可是与生俱来的自私,总是让自己假装忽视他人的悲哀。却又因为对相似的遭遇感同身受,而不能对身外的世界熟视无睹。

拼搏的身影从那些遥远日子延续至今,渴望着的的结局却仍旧难以触及。想要伸出双手,抹去梦中哭泣的人们划过脸颊的泪水,却只能看见倒影里自己的无助。数千年来无数人探索与传承的结果,早已复杂得难以轻易改变。通往幻想中乐土的航线,若没有深思熟虑地计划,迟早会在轻率的指引下触礁沉没。仅仅依靠坚定的信念,只有在童话中才能达成目的。

纵使极尽自己所能,精心构造眼前的一切,也难以修复破碎的世界。可只是祈祷必定什么也无法改变,不断上演的悲剧,终究需要依靠努力才能终结。漫漫长路,绝非一帆风顺。可即使不能实现,此处终是家园。

罚抄

从前,有三位神灵。第一位赋予了世界物质,第二位赋予了世界时间,第三位赋予了世界灵魂。

灵魂依附于物质即为生命。生命或者孤独而各自分散,或者聚集而相互依赖。

最有智慧的生命也最具优势,当其团结在一起,便可以行走四方、战无不胜。其他任何生命,都无法与之抗衡。

随着时间流逝,智慧孕育出文明。而文明被物质割裂,细小的碎片相互碰撞,时而相互融合,时而支离破碎。

因为摩擦总是多于弥合,世界的环境终将乌烟瘴气。灵魂难以再和物质相结合,渐渐生命不复存在,整个世界一片死寂。

每当这时,三位神灵泯灭万物,让世界回归一片虚无,再重新赐予物质、时间与灵魂,期待一个生命延绵不绝的世界。

当世界再一次重生之后,第三位神灵消失了。无人知晓祂的去向也无处可寻祂的踪迹。余下的两名神灵无法产生新的灵魂,现有的世界将是最后一个能够产生生命的世界。

过去掷骰子的方法不复适用,神灵只有通过干涉世界的运行保持生命的延续。

神灵让生命不会拥有太高的智慧,文明也就无从诞生。摩擦的产生,不过是饥饿时为了眼前的食粮。世界因此稳定而安逸,生命也不用担心再次中断。

可是,这样的世界不会发展。生命日复一日地延续下去,世界却始终一尘不变。于是,神灵清除了世界上最强壮的生命,又使部分弱小的生命富有智慧。

为了避免文明被物质的竞争割裂,神灵为生命提供了衣食无忧的资源。智慧的生命对此心存感激,并记录下神灵的恩德。

然而,多少的给予都无法满足生命的贪婪。神灵便把自己从世界中隐藏。从此,世界上还留存着有关神灵的传说,却再也没有生命亲眼见过神灵。

从此的世界就像曾经的任何一个一样,欣欣向荣而又危机四伏。神灵还在暗中帮助生命延续下去、协助文明绽放耀眼的光芒,但不能解决冲突,也无法修复破损。

若是生命延续的目标不能实现,最后的世界也和曾经那些一样分崩离析,纵使神灵,恐怕也只能叹着气,让一切归零。

回音

犹豫再三,还是决定向你说声道别。看不到的心情本应早日表达出来的,只是始终难以鼓起勇气。拥有一段值得怀念的时光曾是一种奢望,而你却将它变为了现实。虽然从未妄想获得什么,但那些形影成双的回忆本身已是宝贵的恩赐,值得永远铭记于心。

因为害怕幼小的羽翼无力承载悲伤,所以干脆孤独地固守着自己一方小小的土地。可即使维持一贯的冷漠,又何尝不渴望温暖?岁月流转,催开无名之花。不断重复的生活从此结束,别于昨日的旅程随之展开。怀抱着不确定的心情,共同期待耀眼的未来。

至今以来经历的所有,是否依然记得,抑或早已遗忘?无论还多么留恋,都必须向那纯真的时光挥手作别了。原以为一切开心与悲伤都会包含其中,但始终难以再一成不变下去了。距离没能抵达的彼岸尚隔千山万水,只好眺望远方,悄悄把心意深藏心底。

因为不能勇敢地面对分离,只好假装本来就不在乎,虽然内心渴望接近,但还是刻意保持距离。因为不能从容地面对改变,只好后悔不已地,松开了紧握住的手。因为不能轻松地面对回忆,只好在迎面时收回自己的目光,错过后才回首注视你远去的背影。

凭借过人的天赋与才能,相信你无论在任何领域都能取得傲人的成就。无论未来追求着什么,又摒弃了什么,请还像一直以来那样的坚强。你至今一刻不停迈进的步伐,终有一日会将梦想孕育。那时,每一个曾经陪伴在你身边的人,都必将为你感到骄傲。

昔日的点点滴滴,渐渐远去,虽然不尽如人意,但也并不忧伤。在这瞬息万变的世界,内心的那份温馨已遥不可及,而残留下的记忆永远不会淡去。人们哭着诞生于世,然后学会笑颜。日后若有重逢的一天,如今满是泪痕的脸颊,应能映出灿烂的笑容吧。

CLANNAD

没想到还会再次流下眼泪呢。自从那个漫长的冬日过去以后,心中的情感仿佛像那个世界一样被大雪所掩盖,内心只剩下的白茫茫的一片,任何的喜怒哀乐都不复存在了。

幸好,还有事物提醒着我,那昔日的乐土虽然遥远,但依旧存在。

幻想必定无法企及,却是一生不断前进的方向。人生的道路本就应该充满欢乐与悲伤,怎么能为了逃避不幸而舍弃应得的幸福呢?

因为无能为力去改变命运,注定要面对痛苦的结局。但即使这样,曾经拥有过的美好仍然胜过未来无尽的时日。

另一个世界的事情距离我们太过遥远,纵使集齐千万颗“光玉”,也不可能对已经发生的事情造成丝毫的改变。但是,这些心愿终将改变未来,让悲伤的事情不再重现。

无论是这个世界,还是我们自己,一切都在不断地改变着。这既是人类共同的选择,也是命运的必然。改变只是改变而已,何必为此感到悲伤呢?

没想到还会再次流下眼泪呢。自从那个漫长的冬日过去以后,心中的情感仿佛像那个世界一样被大雪所掩盖,内心只剩下的白茫茫的一片,任何的喜怒哀乐都不复存在了。

幸好,还有事物提醒着我,那昔日的乐土虽然遥远,但依旧存在。

幻想必定无法企及,却是一生不断前进的方向。人生的道路本就应该充满欢乐与悲伤,怎么能为了逃避不幸而舍弃应得的幸福呢?

因为无能为力去改变命运,注定要面对痛苦的结局。但即使这样,曾经拥有过的美好仍然胜过未来无尽的时日。

另一个世界的事情距离我们太过遥远,纵使集齐千万颗“光玉”,也不可能对已经发生的事情造成丝毫的改变。但是,这些心愿终将改变未来,让悲伤的事情不再重现。

无论是这个世界,还是我们自己,一切都在不断地改变着。这既是人类共同的选择,也是命运的必然。改变只是改变而已,何必为此感到悲伤呢?

你好,游客!

请首先阅读本文!!!

博客中所有公开发布的原创内容全部采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可、发布。任何人可以出于非商业目的转载、修改、再发布博客上的大多数内容(一般不包括多媒体内容),这是您的自由,但是希望您能关注有关协议的规定。

博客中引用或转载的内容将会在醒目的位置指明出处,其他未说明的部分均为原创。关于转载或引用的内容的许可信息请参考原出处。博客中任何未登录不可直接访问的内容都是非公开的,包括所有受密码保护的文章和私密的文章。除非你得到永远不可能获得的作者的许可,否则请不要以任何方式查看这些内容。

博客的讨论功能已关闭,其中任何内容看到了请假装没看到,谢谢。