Парсинг на Rust: быстрее Python и без сборщика мусора

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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(())
}

Блоги

Рекомендаций по книгам