为什么 Rust 是爬虫领域的新王者?
\n\n如果你曾经用 Python 写过爬虫,你一定经历过这样的时刻:脚本运行着,数据下载着,但突然——内存错误,或者垃圾回收器在最不合时宜的时候“休息”一下。熟悉吗?现在想象一下,你的爬虫以 C 语言的速度运行,不消耗多余内存,而且安全可靠。欢迎来到 Rust 爬虫 的世界。
\n\nRust 不仅仅是一种时髦的语言。对于 数据抓取 任务,它提供了 Python 永远无法做到的东西:完全的内存控制、零垃圾回收开销以及惊人的速度。同时,用于处理 HTTP 和 HTML 的生态系统已经相当成熟。在本文中,我将向你展示如何构建一个真正的 Rust 爬虫,你可以将其用于价格监控、内容收集或 SEO 分析。
\n\n我们需要什么:Rust 爬虫工具集
\n\n对于 Rust 网络抓取,有三个核心工具:
\n\n- \n
- reqwest — HTTP 客户端。能够访问网站,支持异步和代理。 \n
- scraper(或 select.rs)— 用于解析 HTML 的库。允许使用 CSS 选择器。 \n
- tokio — 异步运行时。没有它,你的爬虫会在每次请求时变慢。 \n
让我们创建一个新项目并安装依赖项:
\n\ncargo new rust_parser\ncd rust_parser\ncargo add reqwest --features json\ncargo add scraper tokio\n\n完成。现在我们拥有了在 10 分钟内编写一个 Rust 爬虫 所需的一切。
\n\n第一个爬虫:从 Habr 获取标题
\n\n我们从简单的开始:抓取 Habr 上的文章标题。这是经典入门示例,就像 Rust 抓取 的“Hello, World!”。
\n\nuse scraper::{Html, Selector};\nuse reqwest;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n // 1. 发送 GET 请求\n let resp = reqwest::get("https://habr.com/ru/articles/")\n .await?\n .text()\n .await?;\n\n // 2. 解析 HTML\n let document = Html::parse_document(&resp);\n\n // 3. 为标题创建选择器\n let selector = Selector::parse("h2.article-title a").unwrap();\n\n // 4. 迭代并输出\n for element in document.select(&selector) {\n let title = element.text().collect::<Vec<_>>().join("");\n println!("{}", title.trim());\n }\n\n Ok(())\n}\n\n这里发生了什么?我们使用 reqwest 进行异步请求,scraper 解析 HTML,并使用 CSS 选择器 h2.article-title a 查找标题中的所有链接。运行 cargo run — 然后我们看到文章列表。很快,对吧?
异步爬取:每秒下载 100 个页面
\n\nRust 爬虫 的主要特点是异步性。在 Python 中,你会使用 asyncio 和 aiohttp,但 Rust 配合 tokio 提供了更多的控制。让我们并行抓取多个页面。
use scraper::{Html, Selector};\nuse reqwest;\nuse tokio::task;\n\nasync fn fetch_page(url: &str) -> Result<String, reqwest::Error> {\n let resp = reqwest::get(url).await?.text().await?;\n Ok(resp)\n}\n\nfn parse_titles(html: &str) -> Vec<String> {\n let document = Html::parse_document(html);\n let selector = Selector::parse("h2.article-title a").unwrap();\n document\n .select(&selector)\n .map(|el| el.text().collect::<Vec<_>>().join("").trim().to_string())\n .collect()\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let urls: Vec<String> = (1..=5)\n .map(|i| format!("https://habr.com/ru/articles/page{}/", i))\n .coll