In this Article
Rust 网页抓取是指借助 Rust 编程语言及其 crate 生态系统获取并解析网页。Rust 无需垃圾回收器即可提供可预测的性能和内存安全性,因此特别适合持续运行或处理大规模任务的爬虫。
本指南以 reqwest HTTP 客户端和 scraper 解析库为核心,采用实战代码优先的方式讲解。内容涵盖项目配置、最小化抓取、CSS 选择器、结构化数据模型、JSON 和 CSV 导出、分页、带重试与退避的错误处理、基于 tokio 的异步并发、代理配置,以及适用于 JavaScript 密集型网站的无头浏览器方案。每段代码均可编译运行。
DataImpulse 是一家注重合规与道德采购的代理服务商,在 195 个国家和地区提供超过 9000 万个住宅、移动和数据中心 IP 地址。采用按量付费模式,价格每 GB 1 美元起,流量永不过期,适用于网页抓取、广告验证、价格监控、市场研究和多账号管理。
主要事实
- 核心堆栈: Rust 网页抓取通常使用 reqwest HTTP 客户端搭配 scraper crate 解析 HTML,再配合 tokio 实现异步并发、serde 导出结构化数据。
- 最佳代理类型: 轮换住宅代理,使用来自真实用户设备的 IP。
- 价格: 每 GB 1 美元起,即用即付,流量不过期,无需订阅。
- 覆盖范围: 覆盖 195 个国家和地区,提供超过 9000 万个道德采购的 IP。
- 可靠性: 99.51% 的成功率,在 G2 上评分为 4.8 分(满分 5 分)。
- 协议和目标: 支持 HTTP、HTTPS 和 SOCKS5,并可定位目标国家和地区。

为什么用 Rust 进行网页抓取?
当速度、低内存占用和可靠性比快速原型更重要时,Rust 很适合网页抓取。其编译后的二进制程序可高效接近硬件运行,借用检查器还能在编译阶段而非生产环境中发现整类并发错误。
这些特性在常见场景中尤为有价值:
- 吞吐量: 与解释型语言相比,编译后的 Rust 爬虫能以更低的 CPU 和内存开销处理大量页面,从而降低长期任务的基础设施成本。
- 安全并发: 异步运行时 tokio 支持数千个并发请求,类型系统则可防止任务之间发生数据竞争。
- 可预测的资源使用: 没有垃圾回收器意味着延迟更稳定,有助于爬虫连续稳定运行数小时乃至数天。
代价是学习曲线更陡、编译时间更长,因此 Rust 更适合大规模、长期运行的项目,而非一次性脚本。无论使用哪种语言,都应遵循同样的基本原则:尊重 robots.txt、限制请求频率,并遵循网页抓取最佳实践,让你的爬虫成为合规的网络参与者。
如何搭建 Rust 网页抓取项目?
创建新的 Cargo 包,并在 Cargo.toml 中声明所需 crate,即可搭建 Rust 网页抓取项目。Cargo 是 Rust 的构建工具和包管理器,能够解析依赖并编译项目。
运行 cargo new rust-scraper 创建项目,然后编辑生成的 Cargo.toml。以下依赖涵盖本指南的全部内容:HTTP、解析、异步、序列化、CSV 输出、流处理以及易用的错误处理。
[package]
name = "rust-scraper"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.12", features = ["json", "socks"] }
scraper = "0.20"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
csv = "1.3"
futures = "0.3"
anyhow = "1"
thiserror = "1"
其中,reqwest 负责处理请求(socks 功能启用 SOCKS5 代理,json 功能提供 JSON 辅助功能),scraper 用 CSS 选择器解析 HTML,tokio 提供异步运行时,serde 和 serde_json 用于数据序列化,csv 用于写入表格文件,futures 提供并发流组合器,anyhow 与 thiserror 用于简化错误处理。编写第一个请求前,先运行 cargo build 下载并编译依赖。
如何用 reqwest 和 scraper 获取并解析页面?
使用 reqwest 发送 GET 请求获取页面,再将响应正文交给 scraper,通过 CSS 选择器查询内容。下面的示例在 tokio 中异步运行,获取页面并打印每个 H1 元素的文本。
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://example.com";
let body = reqwest::get(url).await?.text().await?;
let document = Html::parse_document(&body);
let heading = Selector::parse("h1").unwrap();
for element in document.select(&heading) {
let text: String = element.text().collect();
println!("{}", text.trim());
}
Ok(())
}
流程始终相同:reqwest::get 返回响应,.text() 将响应正文读为字符串,Html::parse_document 再把字符串转换为可查询的树。? 运算符会将网络或解码错误传播给 main。对匹配元素调用 .text() 可收集其文本节点,随后即可修剪并使用这些文本。这种直接处理原始 HTML 的方法很快,但它只能看到服务器返回的标记,无法看到浏览器随后用 JavaScript 渲染的内容。
如何使用 CSS 选择器定位元素?
将标准 CSS 选择器字符串传给 Selector::parse,即可定位目标元素,用法与浏览器控制台类似。scraper crate 支持标签、类、id、属性匹配、后代路径和结构伪类。
use scraper::Selector;
// Match by tag
let links = Selector::parse("a").unwrap();
// Match by class
let prices = Selector::parse(".price").unwrap();
// Match by id
let main = Selector::parse("#main-content").unwrap();
// Match by attribute value
let external = Selector::parse("a[rel=\"nofollow\"]").unwrap();
// Descendant path (a title inside a product card)
let titles = Selector::parse("div.product h2.title").unwrap();
// Direct child plus pseudo-class
let first_row = Selector::parse("table > tbody > tr:first-child").unwrap();
每个选择器应只解析一次并重复使用,不要在循环内反复解析,尽管单次解析开销很小。当选择器可能匹配不到元素时,优先使用带默认值的 .next(),而不是对元素调用 .unwrap(),以免缺失字段导致整个程序 panic。在浏览器开发者工具中检查目标页面,是找到稳定类名和属性的最快方式。
如何将抓取数据导出为 JSON 和 CSV?
可将每条记录建模为派生 Serialize 的结构体,再用 serde_json 将结构体切片写入 JSON,或用 csv crate 写入 CSV,从而导出抓取数据。类型化模型能让提取与输出保持一致,也让代码更易读。
首先,对记录进行建模并从每个容器元素中提取字段:
use scraper::{Html, Selector};
use serde::Serialize;
#[derive(Debug, Serialize)]
struct Product {
name: String,
price: String,
url: String,
}
fn parse_products(html: &str) -> Vec<Product> {
let document = Html::parse_document(html);
let card = Selector::parse("div.product").unwrap();
let name_sel = Selector::parse("h2.title").unwrap();
let price_sel = Selector::parse("span.price").unwrap();
let link_sel = Selector::parse("a").unwrap();
let mut products = Vec::new();
for card_el in document.select(&card) {
let name = card_el
.select(&name_sel)
.next()
.map(|e| e.text().collect::<String>().trim().to_string())
.unwrap_or_default();
let price = card_el
.select(&price_sel)
.next()
.map(|e| e.text().collect::<String>().trim().to_string())
.unwrap_or_default();
let url = card_el
.select(&link_sel)
.next()
.and_then(|e| e.value().attr("href"))
.unwrap_or_default()
.to_string();
products.push(Product { name, price, url });
}
products
}
由于 Product 派生了 Serialize,同一个结构体可直接为两个导出器提供数据:
use std::fs::File;
use std::io::Write;
fn export_json(products: &[Product]) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(products)?;
let mut file = File::create("products.json")?;
file.write_all(json.as_bytes())?;
Ok(())
}
fn export_csv(products: &[Product]) -> anyhow::Result<()> {
let mut writer = csv::Writer::from_path("products.csv")?;
for product in products {
writer.serialize(product)?;
}
writer.flush()?;
Ok(())
}
serde_json 的 to_string_pretty 会生成便于阅读的 JSON,csv crate 的 serialize 则将每个结构体映射为一行,并根据字段名生成表头。抓取、JSON 和 CSV 共用同一种类型模型,可让 Rust 管道在目标网站变化时更易维护。
如何使用分页循环抓取多个页面?
可通过循环页码或下一页 URL 抓取多个页面,并复用同一个 reqwest 客户端以在请求之间复用连接。构造每个 URL 后获取页面,使用上一节的函数解析,并在页面不再返回结果时停止。
use scraper::Html;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = reqwest::Client::new();
let mut all_products = Vec::new();
for page in 1..=10 {
let url = format!("https://example.com/products?page={}", page);
let body = client.get(&url).send().await?.text().await?;
let products = parse_products(&body);
// Stop when a page returns no results
if products.is_empty() {
break;
}
all_products.extend(products);
// Pause between requests to stay polite
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
println!("Collected {} products", all_products.len());
Ok(())
}
在循环外只创建一次 reqwest::Client::new(),即可复用 TCP 连接,明显快于每次请求都新建客户端。到达最后一页时,空结果检查会自然结束循环;短暂暂停能拉开请求间隔,避免压垮服务器。对于使用下一页链接而非页码的网站,请在每次迭代中提取下一页锚点的 href 并持续跟随,直至链接消失。
如何通过退避处理错误和重试?
可让抓取函数返回类型化错误,并使用指数退避重试暂时性故障。网络会超时,服务器也会返回临时状态码,因此健壮的爬虫应将单次故障视为可预期情况,而非致命错误。
thiserror crate 可用于定义简洁的错误枚举,并自动从 reqwest 错误转换:
use thiserror::Error;
#[derive(Error, Debug)]
enum ScrapeError {
#[error("request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("unexpected status code: {0}")]
Status(reqwest::StatusCode),
#[error("selector matched no elements")]
Empty,
}
async fn fetch_page(client: &reqwest::Client, url: &str) -> Result<String, ScrapeError> {
let response = client.get(url).send().await?;
if !response.status().is_success() {
return Err(ScrapeError::Status(response.status()));
}
Ok(response.text().await?)
}
#[from] 属性表示 reqwest 错误会通过 ? 运算符自动转换为 ScrapeError::Request。除了类型化错误外,还应将请求放入重试循环,并在每次尝试后延长等待时间:
use std::time::Duration;
async fn fetch_with_backoff(client: &reqwest::Client, url: &str) -> anyhow::Result<String> {
let max_attempts = 5;
for attempt in 1..=max_attempts {
match client.get(url).send().await {
Ok(resp) if resp.status().is_success() => {
return Ok(resp.text().await?);
}
Ok(resp) => {
eprintln!("attempt {attempt}: status {}", resp.status());
}
Err(err) => {
eprintln!("attempt {attempt}: {err}");
}
}
// Exponential backoff: 0.5s, 1s, 2s, 4s, 8s
let backoff = Duration::from_millis(500 * 2u64.pow(attempt - 1));
tokio::time::sleep(backoff).await;
}
anyhow::bail!("giving up on {url} after {max_attempts} attempts")
}
指数退避(此处为 0.5 秒、1 秒、2 秒、4 秒、8 秒)能让受速率限制或短暂过载的服务器有时间恢复,避免持续施压。结合轮换代理效果更佳:在一个 IP 上被拦截或限制的请求,可能会在下一次使用不同出口 IP 时成功。
如何使用 tokio 运行并发请求?
将 URL 列表转换为异步流并应用 buffer_unordered,即可运行并发请求;它会限制同时处理的请求数量。这是提升 Rust 爬虫性能最有效的手段之一,tokio 加上 futures crate 可确保并发安全。
use futures::stream::{self, StreamExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = reqwest::Client::new();
let urls: Vec<String> = (1..=50)
.map(|n| format!("https://example.com/item/{}", n))
.collect();
let bodies = stream::iter(urls)
.map(|url| {
let client = client.clone();
async move {
let text = client.get(&url).send().await?.text().await?;
Ok::<_, reqwest::Error>((url, text))
}
})
.buffer_unordered(10); // at most 10 requests in flight
bodies
.for_each(|result| async {
match result {
Ok((url, body)) => println!("{}: {} bytes", url, body.len()),
Err(err) => eprintln!("error: {}", err),
}
})
.await;
Ok(())
}
这里,stream::iter 将 URL 列表转换为流,.map 为每个 URL 发起异步请求,buffer_unordered(10) 最多同时运行十个请求,并在完成后产出结果。每个任务克隆客户端的成本很低,因为 reqwest 的 Client 通过 Arc 共享连接池。可调整并发上限以平衡速度与目标网站负载:10 到 20 是合理起点;更高并发应配合代理池,将请求分散到多个 IP。
如何通过代理路由 Rust 抓取流量?
使用 reqwest::Proxy 构建 reqwest 客户端,并通过 .basic_auth 附加网关与凭据,即可让流量经由代理转发。轮换 IP 能分散负载、访问特定地理位置的内容,并降低被速率限制的概率。轮换 IP 是避免抓取被拦截的有效手段之一。
use reqwest::Proxy;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Authenticated gateway; rotates the exit IP on each request
let proxy = Proxy::all("http://gw.dataimpulse.com:823")?
.basic_auth("login", "password");
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(30))
.build()?;
let body = client
.get("https://httpbin.org/ip")
.send()
.await?
.text()
.await?;
println!("{}", body);
Ok(())
}
请将 login 和 password 替换为自己的凭据,并按所需代理网络调整端口。若代理以 407 状态拒绝请求,几乎总是因为凭据缺失或有误;HTTP 错误 407 指南及代理身份验证教程说明了修复方法。DataImpulse 提供住宅代理、数据中心代理和移动代理,支持 HTTP、HTTPS 和 SOCKS5,并提供轮换与粘性会话。国家和地区定位包含在服务中,州、城市、ZIP 和 ASN 定位则为付费附加项。
如何抓取 JavaScript 密集型页面?何时该选择 Rust?
对于 JavaScript 密集型页面,应驱动真实的无头浏览器进行抓取,因为 reqwest 和 scraper 只能看到服务器返回的原始 HTML,无法执行脚本。当内容仅在客户端渲染后出现时,可使用浏览器自动化 crate,例如 thirtyfour(使用 WebDriver 协议)或 chromiumoxide(通过 DevTools Protocol 控制 Chrome)。
use thirtyfour::prelude::*;
#[tokio::main]
async fn main() -> WebDriverResult<()> {
let caps = DesiredCapabilities::chrome();
let driver = WebDriver::new("http://localhost:9515", caps).await?;
driver.goto("https://example.com").await?;
// Wait for client-side content to render, then read it
let heading = driver.find(By::Css("h1")).await?;
println!("{}", heading.text().await?);
driver.quit().await?;
Ok(())
}
thirtyfour 示例会连接到运行中的 chromedriver、加载页面、等待元素渲染并读取文本。这种方法比普通 HTTP 获取开销大得多,因此只应将其用于确有需要的页面,其他页面仍使用快速的 reqwest 路径。若想深入了解这一权衡,请参阅抓取动态网页和使用 JavaScript 进行网页抓取。下表概述主要 crate 的组合方式:
| crate | 角色 | 运行 JavaScript | 最适合 |
|---|---|---|---|
| reqwest | HTTP 客户端 | 不 | 获取原始 HTML 和 JSON API |
| scraper | HTML 解析器 | 不 | 通过 CSS 选择器提取数据 |
| thirtyfour | WebDriver客户端 | 是的 | 完整的浏览器自动化与表单操作 |
| chromiumoxide | DevTools Protocol | 是的 | 快速控制无头 Chrome |
在语言选择上,若需要大规模持续性能和较低资源占用,请选择 Rust;若更看重开发速度及更丰富的抓取生态系统(如 Scrapy 和 Playwright),请选择 Python。常见做法是先用 Python 为提取规则制作原型,待逻辑稳定、规模增长后,再将性能关键路径迁移到 Rust。无论选择何种语言,网络层都与解析器同样重要;道德采购的代理可提升大型任务的可靠性。

常见问题
哪些 Rust crate 最适合网页抓取?
最常见的组合是用 reqwest 发起 HTTP 请求、用 scraper 解析 HTML 和使用 CSS 选择器。再添加 tokio 实现异步并发、serde 导出数据;当网站需要 JavaScript 渲染时,可添加 thirtyfour 或 chromiumoxide 等浏览器自动化 crate。
Rust 可以抓取 JavaScript 渲染的页面吗?
仅靠 reqwest 和 scraper 不行,因为它们只能看到服务器返回的原始 HTML,无法执行脚本。要抓取客户端渲染的内容,可通过 WebDriver crate(如 thirtyfour)或 DevTools Protocol crate(如 chromiumoxide)驱动无头浏览器。
如何导出 Rust 爬取的数据?
将每条记录建模为派生 serde 序列化功能的结构体,然后用 serde_json 将结构体切片写入 JSON,或用 csv crate 写入 CSV。同一种类型化模型可服务两种格式,无需重复提取逻辑。
如何在不使站点超载的情况下运行并发请求?
将 URL 转换为异步流,并应用设置固定上限的 buffer_unordered,以限制同时运行的请求数。可从大约 10 到 20 个并发请求开始;更高的并发应搭配轮换代理池,将负载分散到多个 IP。
我需要 Rust 网络抓取代理吗?
小规模任务未必需要,但在大规模分发请求和访问特定地理位置内容时,代理非常重要。使用 reqwest::Proxy 和 basic_auth 配置代理,并使用轮换住宅或移动 IP,以减少速率限制和拦截。
何时不适合使用 DataImpulse?
如果你需要静态 ISP 代理、完全托管的抓取 API,或需要访问银行与政府网站,DataImpulse 并不适合。它专注于通过轮换住宅、移动和数据中心代理收集公开数据及访问内容。
使用道德代理可靠地进行抓取
当 Rust 爬虫已能稳定地获取、解析和导出数据后,可靠的代理网络可帮助它持续大规模运行。DataImpulse 提供按量付费、道德采购的 IP,价格每 GB 1 美元起,流量永不过期;你可以创建账户,几分钟内即可通过代理发送第一个请求。
