前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 Rust 做异步数据采集的实践

使用 Rust 做异步数据采集的实践

作者头像
niqin.com
发布2022-06-30 16:51:49
1K0
发布2022-06-30 16:51:49
举报
文章被收录于专栏:Rust 生态与实践

数据采集,生态工具最完整、成熟的,笔者认为莫过于 Python 了,特别是其 Scrapy 库的强大和成熟,是很多项目和产品的必选。笔者以前在大数据项目中,数据采集部分,也是和团队同事一起使用。不管从工程中的那个视觉来说,笔者认为 scrapy 都是完全满足的。

本文是使用 Rust 生态中的数据采集相关 crate 进行数据采集的实践,是出于这样的目的:新的项目中,统一为 Rust 技术栈;想尝试下 Rust 的性能优势,是否在数据采集中也有优势。

所以, 本文更多的仅是 Rust 生态实践而言,并非是 Rust 做数据采集相比 Python 有优势。

好的,我们从头开始进行一次数据采集的完整实践,以站点 https://this-week-in-rust.org/ 为目标,采集所有的 Rust 周报

创建项目

我们使用 cargo,创建一个新项目。本项目我们要使用 Rust 的异步运行时 async-std,HTTP 客户端库 reqwest,数据采集库 scraper,以及控制台输出文字颜色标记库 colored。我们在创建项目后,一并使用 cargo-edit crate 将它们加入依赖项:

关于 cargo-edit 的安装和使用,请参阅文章《构建 Rust 异步 GraphQL 服务:基于 tide + async-graphql + mongodb(1)- 起步及 crate 选择》

代码语言:javascript
复制
cargo new rust-async-crawl-example
cd ./rust-async-crawl-example
cargo add async-std reqwest scraper colored

成功执行后,Cargo.toml 文件清单的 dependencies 区域将有上述 4 个 crate。但是对于 async-std,本次实践中,我们进使用其 attributes 特性;对于 reqwest,我们则要启用其 blocking 特性。我们修改 Cargo.toml 文件,最终为如下内容:

代码语言:javascript
复制

[package]
name = "rust-crawl-week"
version = "0.1.0"
authors = ["zzy <linshi@budshome.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-std = { version = "1.9.0", features = ["attributes"] }
reqwest = { version = "0.11.2", features = ["blocking"] }
scraper = "0.12.0"
colored = "2.0.0"

简要设计

数据采集,我们必定不会局限于一个站点。所以,我们参考 Python 中的库 scrapy 的思路,每个具体的爬虫,对应一个站点。因此,我们组织文件结构为 main.rs 是执行入口;sites.rs 或者 sites 模块,是具体各自站点爬中位置。本例中,我们只是对站点 https://this-week-in-rust.org/ 进行采集,所以将其编写在 sites.rs 文件中。实际的项目产品中,推荐使用 sites 模块,里面包含以各自站点命名的具体爬虫。

对于采集结果,我们要通过输出接口,将其输入到控制台、数据库、文档(文本、excel 等)。这些输出和写入的接口,也需要是在统一的位置,以便于后续扩展。

本实例中,我们将其打印输出到控制台。并在打印时,对于不同的站点、标题,以及 url 链接进行着色。

因此,本实践实例中,工程结构最终为:

此时,我们还未编译构建,所以没有 Cargo.lock 文件和 target 目录。您如果跟随本文实践,cargo build 后,会产生它们。下文不再说明。

main.rs

数据采集入口文件,其代码要尽可能简单和简洁。

代码语言:javascript
复制
mod sites;

#[async_std::main]
async fn main() {

    // this-week-in-rust.org
    match sites::this_week_in_rust_org().await {
        Ok(site) => println!("{:#?}", site),
        Err(_) => eprintln!("Error fetching this-week-in-rust.org data."),
    }

    // 其它站点
    // ……
}

对于多个站点,我们逐次增加即可,这样有利于简单的后续扩展。这儿需要再次说明:本例中,我们只是对站点 https://this-week-in-rust.org/ 进行采集,所以将其编写在 sites.rs 文件中。实际的项目产品中,推荐使用 sites 模块,里面包含以各自站点命名的具体爬虫。

注意println!("{:#?}", site),控制台输出时,我们已经对其采用了 Rust 中默认最美观易读的输出方式。之所以标注此代码,是因为对于第一次不够“人类工程学”的显示方式,我们后面要进行迭代。

sites.rs

第一次编码,采集数据并输出

首先,我们要定义两个结构体,分别表示站点信息,以及采集目标数据的信息(本例为标题、url 链接)。

代码语言:javascript
复制
#[derive(Debug)]
pub struct Site {
    name: String,
    stories: Vec<Story>,
}

#[derive(Debug)]
struct Story {
    title: String,
    link: Option<String>,
}

对于目标数据的采集,我们的思路很简单,三步走:

  1. 获取 HTML 文档;
  2. 萃取数据标题;
  3. 萃取数据 url 链接。

我们定义这三个方法,并在具体的站点爬虫 this_week_in_rust_org 中,进行调用:

代码语言:javascript
复制
use reqwest::{blocking, Error};
use scraper::{ElementRef, Html, Selector};
use std::result::Result;

pub async fn this_week_in_rust_org() -> Result<Site, Error> {
    let s = Selector::parse("div.col-md-12 a").unwrap();
    let body = get_html("https://this-week-in-rust.org/blog/archives/index.html").await?;
    let stories = body
        .select(&s)
        .map(|element| Story {
            title: parse_title(element),
            link: parse_link(element),
        })
        .collect();
    let site = Site {
        name: "this-week-in-rust.org".to_string(),
        stories,
    };

    Ok(site)
}

async fn get_html(uri: &str) -> Result<Html, Error> {
    Ok(Html::parse_document(&blocking::get(uri)?.text()?))
}

fn parse_link(element: ElementRef) -> Option<String> {
    let mut link: Option<String> = None;
    if let Some(link_str) = element.value().attr("href") {
        let link_str = link_str.to_owned();
        link = Some(link_str);
    }

    link
}

fn parse_title(element: ElementRef) -> String {
    element.inner_html()
}

这段代码是很易读的,代码既是最好的文档。注意获取 HTML 文档的函数 get_html 和 爬虫调用函数 this_week_in_rust_org 是异步的,而萃取链接函数 parse_link 和萃取标题函数 parse_title 则不是。因为具体的萃取,是在一个数据解析进程中执行的,异步与否笔者认为意义不大。当然,您如果有兴趣,可以改为异步函数,进行性能对比。

第一次编码完成,我们编译、运行看看部分输出结果:

安装依赖较多,如果时间较长,请配置 Cargo 国内镜像源。

这个输出数据是 json 格式的,并且文字也没有颜色区分。对于其它输入调用接口,非常合适。比如数据库和导出文档等。但是对于人眼阅读来说,则有些不够友好,我们希望输出就是标题和其链接就可以了。

第二次编码,输出数据格式优化

第一次编码中,我们使用的是 Rust 默认的 Display trait。我们要实现自定义输出数据格式,也就是需要对 SiteStory 2 个结构体实现自定义 Display trait。

代码语言:javascript
复制
use colored::Colorize;
use std::fmt;

impl fmt::Display for Site {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "{}", self.name.blue().bold())?;
        for story in &self.stories {
            writeln!(f, "{}", story)?;
        }
        Ok(())
    }
}

impl fmt::Display for Story {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match &self.link {
            Some(link) => write!(f, "\t{}\n\t\t({})", self.title.green(), link),
            None => write!(f, "\t{}", self.title),
        }
    }
}

此时,我们 main.rs 中的打印,甚至不需要指定 Display 方式的:

代码语言:javascript
复制
mod sites;

#[async_std::main]
async fn main() {

    // this-week-in-rust.org
    match sites::this_week_in_rust_org().await {
        Ok(site) => println!("{}", site),
        Err(_) => eprintln!("Error fetching this-week-in-rust.org data."),
    }

    // 其它站点
    // ……
}

我们现在看看输出结果:

人眼阅读,这种方式合适一些,并且 url 链接,可以直接点击。

感兴趣的朋友,可以参阅 github 完整代码仓库。

谢谢您的阅读!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust 生态与实践 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 创建项目
  • 简要设计
  • main.rs
  • sites.rs
    • 第一次编码,采集数据并输出
      • 第二次编码,输出数据格式优化
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档