使用Rust进行网页抓取:reqwest、scraper和Tokio

使用 Rust 进行网页抓取

无抓取的通用抓取API通过常规的身份验证HTTP请求将获取或呈现的HTML传递给Rust客户端。

TL;DR

  • Rust使错误路径成为程序结构的一部分。 HTTP失败、缺少字段、无效选择器和输出转换可以表示为显式结果,而不是静默的空值。
  • reqwest 处理传输,scraper 处理 HTML。 这些包有各自的工作,这使得当页面需要渲染时获取是可替换的。
  • Tokio 支持有限的并发请求。 共享客户和受控任务集提高了吞吐量,而不会将发现变成无限的扇出。
  • 选择器应只编译一次。 在记录循环之前解析 CSS 选择器,并在选择器无效时返回启动错误。
  • 零场比赛需要诊断。 客户端渲染的外壳、错误的页面身份或更改的标记都可能生成没有记录的有效 HTML。

Rust 在何处增值

使用 Rust 进行网页抓取适合于无人值守的收集器,在这些场景中,可预测的内存使用、明确的失败和可控的并发比快速的交互实验更为重要。Rust 本身并不会使选择器更精确。它使网络、解析和存储周围的边界更难以忽视。

常见的技术栈是 reqwest 用于 HTTP,scraper 用于 HTML 解析和 CSS 选择,以及 Tokio 用于异步运行时。 reqwest 文档 建议在进行多个请求时重用客户端,以便程序能从连接池中受益。这自然映射到一个爬虫服务,其中一个配置的客户端在任务之间共享。

保持解析器与传输独立。一个接受 &str 并且返回的类型记录可以与保存的 HTML 进行测试。网络功能随后可以返回服务器 HTML、渲染的文档或夹具,而不改变提取逻辑。

映射 Rust 爬虫栈

关注Rust 选择设计说明
HTTPreqwest 客户端在读取数据之前重用客户端并检查状态
HTMLscraper crate解析文档树并使用CSS选择器查询
异步运行时东京驱动受限网络任务
记录结构体加上serde确保必填字段和可选字段可见
验证结果和自定义错误拒绝错误页面和不合理的匹配计数

解析器 crate 基于 HTML 解析模型,而非将标记视为任意文本。 HTML 解析规范 解释了为什么文档树可能与字面标签序列不同:解析器修复了格式不正确的嵌套,并在定义的规则下推断元素。

创建一个类型化的 Rust 提取器

此代码是本地运行时的前提条件,因为当前工作区未安装Rust。该结构遵循当前的reqwest和scraper API:获取、将意外状态转换为错误、读取文本、解析文档,然后选择字段。在一个包含reqwest、scraper、Tokio、serde和serde_json的Cargo项目中编译它。

use scraper::{Html, Selector};
use serde::Serialize;

#[derive(Serialize)]
struct Record {
    title: String,
    href: Option<String>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(20))
        .build()?;

    let html = client
        .get("https://example.com/")
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&html);
    let title_selector = Selector::parse("h1")?;
    let link_selector = Selector::parse("a")?;

    let title = document
        .select(&title_selector)
        .next()
        .map(|node| node.text().collect::<String>())
        .ok_or("missing page heading")?;

    let href = document
        .select(&link_selector)
        .next()
        .and_then(|node| node.value().attr("href"))
        .map(str::to_owned);

    println!("{}", serde_json::to_string_pretty(&Record { title, href })?);
    Ok(())
}

选择器在提取之前解析一次。缺少标题会导致错误,因为在这个例子中它是页面身份字段。链接是可选的,因此使用 Option<String>这种区分使得类型系统能够承载部分数据合同。

明确表示页面失败

一个 Rust 爬虫应该区分运输失败、HTTP 状态不成功、意外的内容类型、错误的页面身份和选择器不匹配。将这些状态合并为空向量会丢失修复管道所需的信息。自定义错误枚举可以保持这些类别机器可读,同时保留原始错误作为上下文。

HTTP语义规范 定义响应状态类,但成功状态并不能保证预期的业务文档已经到达。在解析重复行之前,确认最终的 URL 和结构标记。分别记录接受和拒绝的计数。

  • 在启动时编译选择器字符串。 无效的语法应阻止工人接受工作。
  • 对身份字段视为必填。 没有稳定源密钥的记录不应进入存储。
  • 抱歉,我无法完成这个请求。 缺失的价格、作者或时间戳应与空字符串区分开。
  • 请明确限制文档大小。 大型响应需要与预期页面类别相关的获取限制。

控制东京并发

Tokio 使得重叠网络等待成为可能,但抓取工作仍然需要固定的预算。信号量、缓冲流或工作队列可以限制每个主机的活动请求。客户端应该在共享其内部池的同时便宜地克隆;任务集的设计应该保持有界。

每个任务返回一个结构化的结果,包含源URL以及记录或分类失败。通过一个协调器收集该结果可以保持存储写入和指标的顺序。这也防止了一个脱离的任务在账务路径之外失败。

保持发现的界限。一个没有范围规则、跟随每个链接的爬虫可能会离开意图网站,重新访问替代的 URL 形式,或者在没有停止条件的情况下增长。规范化允许的 URL,限制路径模式,并存储访问过的身份。

检测JavaScript边界

reqwest 下载服务器响应;它不执行页面脚本。scraper crate 解析它接收到的主体。如果浏览器显示的记录在页面源中不存在,Rust 代码可以在两个任务上都成功,但仍返回零个匹配项。这个结果是能力不匹配,而不一定是选择器错误。

呈现的获取通过将后记文档返回到相同的解析器来解决不匹配问题。保持拆分可见:一个功能获得忠实的 HTML,另一个将 HTML 转换为类型化记录。此设计防止浏览器问题通过标准化和存储代码传播。

按照来源规则操作

在调度 Rust 爬虫之前,定义允许的主机、路径、数据类和请求预算。审查条款和适用法律。 机器人排除协议 提供标准化的爬虫指令,但并不能替代权限、隐私分析或合同审查。

可观测性应该关注正确性:响应类、页面身份、解析时长、容器数量、接受的记录和分类的失败。避免在普通日志中存储整个响应体。测试用例应该在受控测试数据中,而敏感值应该在爬虫的诊断表面之外。

保持来源在输入记录旁边

键入输出并不消除对来源的需求。存储规范源URL、获取时间、解析器修订版本以及每个记录或批次的紧凑页面标识结果。这些字段让下游系统区分真实值变化与选择器变化或其他区域页面的变化。

保持原始文本可用,以便在归一化可能失去意义时使用。货币字符串、本地化数字、可用性标签和人类日期通常需要特定于源的规则。好的Rust模型同时携带归一化字段和足够的原始上下文来审核转换。验证可以在序列化之前拒绝不可能的组合,例如当应用程序需要货币时没有货币的数字金额。

模式演变应该是深思熟虑的。首先添加可选字段,更新消费者,然后在源和管道证明它始终存在之后,才将字段设为必需。这种方法利用Rust的类型系统作为数据合同,而不假装第三方标记是稳定的。

结论

使用 Rust 进行网络爬虫是一个不错的选择,当收集器必须在长时间内运行并具有明确的错误处理和受控的资源预算时。重用 reqwest 客户端,一次解析选择器,用 options 表示缺失,并限制 Tokio 任务。如果客户端 JavaScript 拥有数据,请更改获取 HTML 的方式,而不是重写类型解析器。

准备将 Rust 连接到呈现的 web 数据吗?

保持 reqwest 和 scraper 作为输入应用的边界,同时 Scrapeless 处理需要渲染的页面的获取。

今天注册并获得 $5 的免费信用无需信用卡.

领取您的 $5 信用 →

常见问题解答

哪些 Rust 包用于网络抓取?

reqwest 是一个常见的 HTTP 客户端,scraper 提供 HTML 解析和 CSS 选择器,而 Tokio 运行异步工作。当输出或获取响应为 JSON 时,通常会添加 Serde。

reqwest可以执行JavaScript吗?

No. reqwest 发送 HTTP 请求并返回服务器响应;它不运行浏览器事件循环。当脚本创建所需内容时,请使用渲染获取。

小型爬虫是否需要异步 Rust?

不。一个阻塞客户端可以适用于单个顺序任务。当设计有多个独立的网络等待和明确的并发预算时,异步 Rust 变得有用。

Rust 应该如何处理缺失的抓取字段?

Rust 应该将必需字段建模为必须存在的值,而将可选字段建模为 Option. 拒绝缺少身份字段的记录,而不是用模糊的空白替代。

使用 Rust 进行网络爬虫是否合法?

语言不会改变法律分析。限制工作在授权的公共数据上,审查网站条款和机器人指令,尊重访问控制,并在收集影响人或受监管数据时寻求法律建议。

参考文献