Почему Rust — это новый король парсинга?
Если вы когда-нибудь писали парсер на Python, вы знаете этот момент: скрипт работает, данные качаются, но вдруг — ошибка памяти, или сборщик мусора решает «передохнуть» в самый неподходящий момент. Знакомо? А теперь представьте, что ваш парсер летает со скоростью C, не жрёт лишнюю память и при этом безопасен. Добро пожаловать в мир парсинга на Rust.
Rust — это не просто модный язык. Для задач скрапинга данных он даёт то, что Python никогда не сможет: полный контроль над памятью, нулевые накладные расходы на сборщик мусора и невероятную скорость. При этом экосистема для работы с HTTP и HTML уже достаточно зрелая. В этой статье я покажу вам, как собрать реальный парсер на Rust, который вы сможете использовать для мониторинга цен, сбора контента или SEO-анализа.
Что нам понадобится: инструментарий Rust для парсинга
Для web scraping на Rust есть три кита:
- reqwest — HTTP-клиент. Умеет ходить по сайтам, поддерживает асинхронность и прокси.
- scraper (или select.rs) — библиотека для парсинга HTML. Позволяет использовать CSS-селекторы.
- tokio — асинхронный рантайм. Без него ваш парсер будет тормозить на каждом запросе.
Давайте создадим новый проект и установим зависимости:
cargo new rust_parser
cd rust_parser
cargo add reqwest --features json
cargo add scraper tokio
Готово. Теперь у нас есть всё, чтобы написать парсер на Rust за 10 минут.
Первый парсер: получаем заголовки с Habr
Начнём с простого: спарсим заголовки статей с Хабра. Это классика жанра, как «Hello, World!» для скрапинга Rust.
use scraper::{Html, Selector};
use reqwest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Отправляем GET-запрос
let resp = reqwest::get("https://habr.com/ru/articles/")
.await?
.text()
.await?;
// 2. Парсим HTML
let document = Html::parse_document(&resp);
// 3. Создаём селектор для заголовков
let selector = Selector::parse("h2.article-title a").unwrap();
// 4. Итерируемся и выводим
for element in document.select(&selector) {
let title = element.text().collect::<Vec<_>>().join("");
println!("{}", title.trim());
}
Ok(())
}
Что здесь происходит? Мы используем reqwest для асинхронного запроса, scraper для разбора HTML и CSS-селектор h2.article-title a, чтобы найти все ссылки в заголовках. Запускаем cargo run — и видим список статей. Быстро, правда?
Асинхронный парсинг: качаем 100 страниц за секунду
Главная фишка парсинга на Rust — асинхронность. В Python вы бы юзали asyncio и aiohttp, но Rust с tokio даёт ещё больше контроля. Давайте спарсим сразу несколько страниц параллельно.
use scraper::{Html, Selector};
use reqwest;
use tokio::task;
async fn fetch_page(url: &str) -> Result<String, reqwest::Error> {
let resp = reqwest::get(url).await?.text().await?;
Ok(resp)
}
fn parse_titles(html: &str) -> Vec<String> {
let document = Html::parse_document(html);
let selector = Selector::parse("h2.article-title a").unwrap();
document
.select(&selector)
.map(|el| el.text().collect::<Vec<_>>().join("").trim().to_string())
.collect()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let urls: Vec<String> = (1..=5)
.map(|i| format!("https://habr.com/ru/articles/page{}/", i))
.collect();
let mut tasks = Vec::new();
for url in urls {
tasks.push(task::spawn(async move {
let html = fetch_page(&url).await.unwrap();
parse_titles(&html)
}));
}
for task in tasks {
let titles = task.await?;
for title in titles {
println!("{}", title);
}
}
Ok(())
}
Скорость? В реальных тестах асинхронный парсинг Rust обрабатывает 100 страниц быстрее, чем Python успеет открыть соединение к 10. И никаких блокировок — tokio сам решает, когда переключить контекст.
Скрапинг данных с защитой: User-Agent и прокси
Реальные сайты не любят ботов. Если вы попробуете спарсить, скажем, Ozon или Wildberries без заголовков, вас быстро забанят. В Rust это решается легко — добавляем User-Agent и прокси в reqwest.
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.proxy(reqwest::Proxy::http("http://proxy.example.com:8080")?)
.build()?;
let resp = client
.get("https://httpbin.org/headers")
.send()
.await?
.text()
.await?;
println!("{}", resp);
Ok(())
}
Теперь ваш скрапинг данных на Rust выглядит как настоящий браузер. А если добавить ротацию прокси и случайные задержки, вы сможете парить даже самые защищённые сайты.
Парсинг сложных страниц: JSON и динамический контент
Не все данные лежат в HTML. Многие современные сайты (например, Avito или Instagram) отдают контент через AJAX — JSON-ответы. Парсинг Rust отлично справляется и с этим. Допустим, нам нужно получить данные из API, которое возвращает JSON:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Post {
userId: u32,
id: u32,
title: String,
body: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let posts: Vec<Post> = reqwest::Client::new()
.get("https://jsonplaceholder.typicode.com/posts")
.send()
.await?
.json()
.await?;
for post in &posts[..5] {
println!("{}: {}", post.id, post.title);
}
Ok(())
}