🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

使用 reqwest 和 scraper 的 Rust 网页抓取:实用指南

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

21-Jul-2026

TL;DR:

  • 在2026年的Rust网页抓取中运行两个crate:reqwest用于HTTP请求,scraper用于CSS选择器解析,tokio驱动异步运行时。
  • 编译器强制你处理每一个失败路径,因此一个可以构建的Rust抓取器通常是一个能够应对格式错误的标记和非200响应的抓取器。
  • tokio::spawn将逐页抓取器变为并发抓取器只需五行代码,本指南在五页中运行,获得50条记录。
  • reqwestscraper都不执行JavaScript,因此客户端渲染的页面返回的标记解析为零条记录。
  • Scrapeless通用抓取API首先渲染页面并返回HTML,而相同未更改的scraper代码解析为10条记录。
  • Scrapeless免费计划开始,并对你的目标运行pivot示例。

Rust在抓取工作中出现是因为:抓取器通常是数据管道的一部分,不间断地对没人控制的标记运行数小时。一个借用检查的二进制文件,没有垃圾回收器的暂停,并在每个边界上显式处理Result,非常适合这个工作。

本指南创建一个使用当前crate版本的工作抓取器,运行它,然后准确显示纯Rust栈停止的地方——以测量数字而非警告。

你需要的东西

Rust网页抓取需要三个crate和一个稳定的工具链。以下版本是在撰写时< a href="https://crates.io" rel="nofollow">Rust社区crate注册表上的当前发布版本,并且这里的每个示例都是在这些版本上编译和运行的:

Crate Version Job
reqwest 0.13.4 HTTP客户端
scraper 0.27.0 HTML解析和CSS选择器
tokio 1.53.0 异步运行时
serde_json 1 API响应的JSON处理

scraper crate封装了html5ever,这是在Servo中使用的相同解析引擎,因此它遵循HTML解析规范,而不是将标记视为要正则表达式匹配的文本。这在实际页面中很重要,因为未闭合的标签是正常的。

安装

创建项目并添加依赖:

bash Copy
cargo new rust-scraper
cd rust-scraper

然后在Cargo.toml中声明依赖块:

toml Copy
[package]
name = "rust-scraper"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.13.4", features = ["json"] }
scraper = "0.27.0"
tokio = { version = "1.53.0", features = ["macros", "rt-multi-thread"] }
serde_json = "1"

reqwest上的json功能引入请求和响应序列化。tokio上的macrosrt-multi-thread功能使#[tokio::main]属性工作。

抓取和解析页面

一个完整的Rust抓取器比其名声所暗示的要短。这一抓取器抓取一个服务器渲染的页面,选择每个引用容器,并从每个容器中提取两个字段:

rust Copy
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body = reqwest::get("https://quotes.toscrape.com/")
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&body);
    let quote_sel = Selector::parse("div.quote").unwrap();
    let text_sel = Selector::parse("span.text").unwrap();
    let author_sel = Selector::parse("small.author").unwrap();

    let mut count = 0;
    for quote in document.select(&quote_sel) {
        let text = quote.select(&text_sel).next().map(|e| e.inner_html());
        let author = quote.select(&author_sel).next().map(|e| e.inner_html());
        if let (Some(t), Some(a)) = (text, author) {
            if count == 0 {
                println!("第一条引用: {} — {}", t, a);
            }
            count += 1;
        }
    }
    println!("解析的引用数量: {}", count);
    Ok(())
}

运行后会输出:

text Copy
第一条引用: “我们所创造的世界是我们思维的过程。没有改变我们的思维,就无法改变它。” — 阿尔伯特·爱因斯坦
解析的引用数量: 10

那段代码中的三个细节承载着大部分重量。

error_for_status()将4xx或5xx响应转换为Err,而不是让你将错误页面解析为数据。没有它,403的正文会变成零个选择器匹配,看起来与空的结果集相同。那些状态类别之间的区别在HTTP语义规范中定义。
Selector::parse 是可失败的,因为选择器字符串是在匹配时不进行解释而是编译的。在循环外编译选择器——而不是每个元素都编译——就是为何嵌套的 select 调用保持便宜。语法遵循 W3C 选择器级别 4 规范

main 中传播出的 ? 操作符将 Box<dyn std::error::Error> 变成了一个实用的返回类型。每个可失败的调用都会回溯到相同的位置,而该过程在失败时以非零状态退出——当抓取程序从调度器运行时这很有用。

并发抓取页面

Rust 的并发特性是这里使用它的主要理由,而 tokio::spawn 就是体现这一点的地方。每个页面成为一个独立的任务,所有任务共享一个连接池客户端,并按顺序收集结果:

rust Copy
use scraper::{Html, Selector};

async fn page_quote_count(client: &reqwest::Client, page: u32) -> Result<usize, reqwest::Error> {
    let url = format!("https://quotes.toscrape.com/page/{page}/");
    let body = client.get(&url).send().await?.error_for_status()?.text().await?;
    let quote_sel = Selector::parse("div.quote").unwrap();
    Ok(Html::parse_document(&body).select(&quote_sel).count())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    let tasks: Vec<_> = (1..=5)
        .map(|page| {
            let client = client.clone();
            tokio::spawn(async move { (page, page_quote_count(&client, page).await) })
        })
        .collect();

    let mut total = 0;
    for task in tasks {
        let (page, result) = task.await?;
        let count = result?;
        println!("page {page}: {count} quotes");
        total += count;
    }
    println!("total quotes across 5 pages: {total}");
    Ok(())
}

执行结果:

text Copy
page 1: 10 quotes
page 2: 10 quotes
page 3: 10 quotes
page 4: 10 quotes
page 5: 10 quotes
total quotes across 5 pages: 50

reqwest::Client 便宜,是因为克隆时共享底层连接池。每个任务创建一个新客户端会每次打开一个新池并丢弃这一好处。保持页面范围限制在目标可以舒适提供的大小——并发是完成已知工作负载的工具,而不是发出尽可能多的同时请求。

准备将其指向渲染服务器端的目标了吗?创建一个免费的 Scrapeless 账户,并保留你已有的解析代码。

reqwest 和 scraper 的局限

这两个 crate 均不执行 JavaScript。reqwest 返回服务器发送的字节,而 scraper 正好解析这些字节——因此在一个在浏览器中构建内容的页面上,选择器什么也匹配不到。

这是可测量的而非理论的。上述使用的网站在 /js/ 路径上发布了相同数据的客户端渲染版本。将相同的解析逻辑应用于它:

rust Copy
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body = reqwest::get("https://quotes.toscrape.com/js/")
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&body);
    let quote_sel = Selector::parse("div.quote").unwrap();
    let count = document.select(&quote_sel).count();

    println!("html bytes: {}", body.len());
    println!("quotes parsed: {}", count);
    Ok(())
}
text Copy
html bytes: 5808
quotes parsed: 0

请求成功。状态为 200。解析器在 5,808 字节的有效 HTML 上正常工作,但其中没有包含任何引用元素——它们是在脚本运行后写入 DOM 的。只检查 HTTP 失败的抓取程序将其视为空页面而不是缺少能力,这就是先前调用 error_for_status() 是必要但不足的原因。

使用通用抓取 API 渲染页面

Scrapeless 通用抓取 API 通过在云浏览器中渲染页面并返回生成的 HTML 来填补这一空白,这意味着 Rust 端保持为简单的 HTTP 调用。将 js_render 设置为 true,则响应体包含脚本后 DOM。

使用环境中的密钥进行身份验证:

bash Copy
export SCRAPELESS_API_KEY="your_api_key_here"

然后只需替换抓取层——下面的选择器代码与第一个示例相同:

rust Copy
use scraper::{Html, Selector};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("SCRAPELESS_API_KEY")?;

    let payload = json!({

"actor": "unlocker.webunlocker",
"input": {
"url": "https://quotes.toscrape.com/js/",
"js_render": true,
"headless": true
}
});

Copy
let envelope: serde_json::Value = reqwest::Client::new()
    .post("https://api.scrapeless.com/api/v2/unlocker/request")
    .header("x-api-token", api_key)
    .json(&payload)
    .send()
    .await?
    .error_for_status()?
    .json()
    .await?;

let body = envelope["data"].as_str().unwrap_or_default();

let document = Html::parse_document(body);
let quote_sel = Selector::parse("div.quote").unwrap();
let text_sel = Selector::parse("span.text").unwrap();
let author_sel = Selector::parse("small.author").unwrap();

let mut count = 0;
for quote in document.select(&quote_sel) {
    let text = quote.select(&text_sel).next().map(|e| e.inner_html());
    let author = quote.select(&author_sel).next().map(|e| e.inner_html());
    if let (Some(t), Some(a)) = (text, author) {
        if count == 0 {
            println!("第一句名言: {} — {}", t, a);
        }
        count += 1;
    }
}
println!("HTML字节数: {}", body.len());
println!("解析的名言数量: {}", count);
Ok(())

}

text Copy
第一句名言: “我们所创造的世界是我们思维的结果。没有改变我们的思维,就无法改变它。” — 阿尔伯特·爱因斯坦
HTML字节数: 8985
解析的名言数量: 10

同一个页面,相同的选择器,相同的库版本。唯一的变化是哪个层级获取了HTML,当有效记录数量从0变为10时,传输的字节从5,808增加到8,985字节——区别在于脚本写入DOM的名言标记。

响应作为JSON信封到达,渲染的文档在data中作为字符串,这就是为什么示例首先解析为serde_json::Value并将data传递给Html::parse_document。渲染选项的详细信息可以在Scrapeless文档中找到,而相同的js_render行为在JS渲染指南中有更深入的讨论。

故障排除

选择器在您可以在浏览器中看到的页面上匹配不到任何内容。 首先打印响应长度,如上面的JS示例所示。几千字节但没有匹配通常意味着内容是客户端渲染的,而不是选择器错误。查看页面源代码,而不是检查器——检查器显示的是脚本运行后的DOM,这并不是reqwest接收到的内容。

Selector::parse在启动时崩溃。 选择器字符串是无效的CSS。伪元素和一些jQuery风格的扩展不属于规范的一部分,因此无法编译。

构建在TLS后端上失败。 reqwest编译了一个TLS栈;在一个最小的容器上,系统需要一个C工具链和CMake可用,或者您可以通过功能标志切换到纯Rust的rustls后端。

inner_html()返回标记而不是文本。 该方法返回元素内部的所有内容,包括标签。使用text()并在一个字段能够包含嵌套元素时收集片段。

在针对实时目标指向任何这些内容之前,请检查网站的条款及其/robots.txt指令,它遵循机器人排除协议标准。确保收集公共数据,并保持在目标可以舒适提供的量内。

结论

Rust为您提供了一个明确处理失败并且无太多程序随意并行化的抓取器——reqwest用于传输,scraper用于选择器,tokio用于并发,以及一个不会让您忽视Result的编译器。该堆栈彻底覆盖了服务器渲染的页面。

但它不执行JavaScript,这一差距的代价是无声的零,而不是错误。测量它是有用的习惯:在服务器渲染的页面上10条记录,在客户端渲染的页面上0条,最后在Rust解析之前有10条记录得以渲染。

从Scrapeless免费计划开始,以便针对您自己的目标运行渲染步骤,并在给工作定价时查看当前的Scrapeless定价

常见问题

问:哪个Rust库最适合网络抓取?
reqwestscraper配合使用即可处理大部分工作。reqwest使用异步优先的API处理HTTP请求,scraper则提供对html5ever解析树的CSS选择器查询。当页面在客户端构建其内容时,才需要使用无头浏览器库或渲染API。

问:与Python相比,Rust适合网页抓取吗?

当抓取程序运行时间较长、并发运行或无人值守时,Rust是一个不错的选择,因为它没有垃圾回收暂停,并且编译器强制处理每个错误路径。对于一次性提取,Python在生态系统广度和迭代速度上仍然占优。解析概念在两者之间直接转移。

问:reqwest能执行JavaScript吗?

不能。reqwest是一个HTTP客户端,只返回服务器发送的字节。在客户端渲染的页面上,这意味着有效的HTML,但其中没有内容——上面的示例将5,808个字节解析为零条记录。渲染必须在其他地方进行,或是在返回渲染后的DOM的API中。

问:我需要async和tokio来实现一个简单的抓取程序吗?

不一定——reqwest在一个特性标志下提供一个阻塞客户端,这对于单个顺序请求是可以的。一旦你抓取多个页面,异步客户端就值得引入tokio依赖,因为在这里tokio::spawn将顺序抓取转换为并发抓取。

问:我如何保持Rust爬虫在网站变化时不崩溃?

对结构进行断言,而不是盲目信任。在提取字段之前,检查容器选择器是否匹配合理数量的元素,并将突然出现的零视为失败,而不是空结果。在启动时编译选择器也能立即显示无效的CSS,而不是在爬取过程中。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录