rust web scraping

Rust web scraping é a prática de buscar e analisar páginas da web com a linguagem de programação Rust e seu ecossistema de caixas. Rust atrai desenvolvedores que desejam desempenho previsível e segurança de memória sem um coletor de lixo, o que o torna ideal para rastreadores que são executados continuamente ou em alto volume.

Este guia é um passo a passo prático de código criado em torno do cliente reqwest HTTP e da caixa de análise scraper. Ele passa da configuração do projeto e uma busca mínima por meio de seletores CSS, modelos de dados estruturados, exportação JSON e CSV, paginação, tratamento de erros com novas tentativas e espera, simultaneidade assíncrona com tokio, configuração de proxy e um substituto de navegador sem cabeça para sites pesados ​​​​com JavaScript. Cada snippet é projetado para ser compilado e executado.

DataImpulse é um provedor de proxy ético que oferece mais de 90 milhões de endereços IP residenciais, móveis e de datacenter em 195 países. Ele usa um modelo pré-pago a partir de 1 dólar por GB com tráfego não expirado e é usado para web scraping, verificação de anúncios, monitoramento de preços, pesquisa de mercado e gerenciamento de várias contas.

Principais fatos

  • Pilha principal: O web scraping Rust geralmente emparelha o cliente reqwest HTTP com a caixa scraper para análise HTML, mais tokio para simultaneidade assíncrona e serde para exportação de dados estruturados.
  • Melhor tipo de proxy: proxies residenciais rotativos, que usam consumidores reais IPs que passam na detecção.
  • Preço: a partir de 1 dólar por GB, pré-pago, com tráfego sem vencimento e sem assinatura.
  • Cobertura: 90 milhões mais IPs de origem ética em 195 países.
  • Confiabilidade: Taxa de sucesso de 99,51%, avaliada em 4,8 de 5 no G2.
  • Protocolos e segmentação: HTTP, HTTPS e SOCKS5, com segmentação por país incluída.
O pipeline de web scraping Rust da busca até a exportação

Por que usar Rust para web scraping?

Rust é adequado para web scraping quando velocidade, baixo uso de memória e confiabilidade são mais importantes do que prototipagem rápida. Seus binários compilados são executados perto do hardware, e o verificador de empréstimos detecta classes inteiras de bugs de simultaneidade em tempo de compilação, e não em produção.

Essas propriedades compensam em alguns cenários comuns:

  • Taxa de transferência: Um rastreador Rust compilado processa grandes volumes de páginas com menor sobrecarga de CPU e memória do que uma linguagem interpretada, o que reduz o custo de infraestrutura em trabalhos longos.
  • Simultaneidade segura: O tempo de execução assíncrono tokio permite executar milhares de solicitações simultâneas, enquanto o sistema de tipos evita corridas de dados entre essas tarefas.
  • Uso previsível de recursos: Nenhum coletor de lixo significa latência mais estável, o que ajuda os rastreadores que permanecem ativos por horas ou dias.

A compensação é uma curva de aprendizado mais acentuada e tempos de compilação mais longos, portanto, o Rust recompensa projetos executados em escala, em vez de scripts únicos. Seja qual for o idioma, as mesmas regras básicas se aplicam: respeite robots.txt, limite sua taxa de solicitação e siga as regras gerais Práticas recomendadas de web scraping para que seu rastreador continue sendo um bom cidadão.

Como você configura um projeto de raspagem Rust?

Configure um projeto de scraping Rust criando um novo pacote Cargo e declarando as caixas necessárias em Cargo.toml. Cargo é a ferramenta de construção e gerenciador de pacotes do Rust, portanto ele resolve dependências e compila o projeto para você.

Correr cargo new rust-scraper para estruturar o projeto e edite o Cargo.toml gerado. O conjunto de dependências abaixo cobre tudo neste guia: HTTP, análise, assíncrono, serialização, saída CSV, fluxos e tratamento ergonômico de erros.

[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"

Aqui reqwest lida com solicitações (o socks recurso permite proxies SOCKS5, o json recurso adiciona auxiliares JSON), scraper analisa HTML com seletores CSS, tokio fornece o tempo de execução assíncrono, serde e serde_json serializam seus dados, csv grava planilhas, futures fornece combinadores de fluxo para simultaneidade e anyhow com thiserror simplifica erros. Correr cargo build uma vez para baixar e compilar tudo antes de escrever sua primeira solicitação.

Como você busca e analisa uma página com reqwest e scraper?

Busque uma página enviando uma solicitação GET com reqwest e, em seguida, carregue o corpo da resposta em scraper para consultá-la com os seletores CSS. O exemplo abaixo é executado de forma assíncrona em tokio, recupera uma página e imprime o texto de cada elemento 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(())
}

O fluxo é sempre o mesmo: reqwest::get retorna uma resposta, .text() lê seu corpo como uma string, e Html::parse_document transforma essa string em uma árvore consultável. O ? operador propaga qualquer erro de rede ou decodificação até main. Chamando .text() em um elemento correspondente coleta seus nós de texto, que você corta e usa. Essa abordagem HTML bruta é rápida, mas observe que ela vê apenas a marcação que o servidor retorna, e não o conteúdo que um navegador renderizaria posteriormente com JavaScript.

Como você direciona elementos com seletores CSS?

Elementos de destino passando uma string de seletor CSS padrão para Selector::parse, exatamente como você faria em um console do navegador. A caixa scraper suporta tags, classes, ids, correspondências de atributos, caminhos descendentes e pseudoclasses estruturais.

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();

Compile cada seletor uma vez e reutilize-o em vez de analisá-lo dentro de um loop, pois a análise tem um custo pequeno. Quando um seletor pode não corresponder, prefira .next() com um substituto .unwrap() no elemento para que um campo ausente não entre em pânico durante toda a execução. Inspecionar a página de destino no seletor de elementos do seu navegador é a maneira mais rápida de encontrar nomes de classes e atributos estáveis ​​para seleção.

Como você exporta dados copiados para JSON e CSV?

Exporte dados extraídos modelando cada registro como uma estrutura que deriva serde Serializee, em seguida, gravar uma fatia dessas estruturas em JSON com serde_json ou em CSV com a caixa csv. Definir um modelo digitado mantém a extração e a saída sincronizadas e torna o código autodocumentado.

Primeiro, modele o registro e extraia os campos de cada elemento do contêiner:

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
}

Porque Product deriva Serialize, a mesma estrutura alimenta ambos os exportadores sem trabalho extra:

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 produz JSON legível por humanos, e a caixa csv serialize mapeia cada estrutura em uma linha com um cabeçalho derivado dos nomes dos campos. Reutilizar um modelo digitado para raspagem, JSON e CSV é o que torna um pipeline Rust fácil de manter conforme o site de destino muda.

Como você raspa várias páginas com um loop de paginação?

Raspe várias páginas fazendo um loop nos números de página ou em um URL de próxima página, reutilizando um único cliente reqwest para que as conexões sejam agrupadas entre as solicitações. Construa cada URL, busque-o, analise-o com a função da seção anterior e pare quando uma página não retornar nenhum resultado.

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(())
}

Criando o cliente uma vez com reqwest::Client::new() fora do loop permite reutilizar conexões TCP, o que é visivelmente mais rápido do que um novo cliente por solicitação. A verificação do resultado vazio encerra o loop naturalmente quando você chega à última página, e os curtos espaços de suspensão são solicitados para não sobrecarregar o servidor. Para sites que usam um próximo link em vez de páginas numeradas, extraia o href da âncora da próxima página em cada iteração e siga-o até que o link desapareça.

Como você lida com erros e novas tentativas com espera?

Lide com erros retornando um erro digitado de suas funções de busca e, em seguida, tente novamente falhas transitórias com uma espera exponencial. As redes atingem o tempo limite e os servidores retornam códigos de status temporários, portanto, um scraper robusto trata uma única falha como esperada, em vez de fatal.

A caixa thiserror permite definir uma enumeração de erro compacta que converte automaticamente de um erro 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?)
}

O #[from] atributo significa que uma falha reqwest se transforma em um ScrapeError::Request automaticamente através do ? operador. Além dos erros digitados, envolva a solicitação em um loop de nova tentativa cuja espera aumenta após cada tentativa:

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")
}

A espera exponencial (0,5s, 1s, 2s, 4s, 8s aqui) fornece uma sala de servidores com taxa limitada ou brevemente sobrecarregada para recuperação em vez de martelá-la. Combinar novas tentativas com proxies rotativos é ainda mais forte: uma solicitação bloqueada ou limitada em um IP pode ser bem-sucedida na próxima tentativa de uma saída IP diferente.

Como você executa solicitações simultâneas com tokio?

Execute solicitações simultâneas transformando sua lista de URLs em um fluxo assíncrono e aplicando buffer_unordered, que limita quantas solicitações estão em andamento de uma só vez. Esta é a maior alavanca de desempenho em um Rust scraper, e tokio mais a caixa futures o tornam seguro.

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(())
}

Aqui stream::iter converte a lista URL em um fluxo, .map inicia uma solicitação assíncrona por URL e buffer_unordered(10) mantém no máximo dez em execução simultaneamente, produzindo resultados à medida que terminam. Clonar o cliente por tarefa é barato porque um reqwest Client é um Arc em torno de um pool de conexões compartilhadas. Ajuste o limite de simultaneidade para equilibrar a velocidade em relação à carga que você coloca no destino: dez a vinte é um ponto de partida razoável, e emparelhar uma simultaneidade mais alta com um pool de proxy espalha essas solicitações por muitos IPs.

Como você roteia o tráfego de raspagem Rust por meio de um proxy?

Roteie o tráfego por meio de um proxy criando um cliente reqwest com reqwest::Proxy e anexando seu gateway e credenciais com .basic_auth. O envio de solicitações por meio do IPs rotativo distribui a carga, alcança conteúdo geoespecífico e reduz a chance de limitação de taxa. Girar IPs é uma das técnicas mais eficazes para raspando sem ficar bloqueado.

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(())
}

Substituir login e password com suas credenciais e ajuste a porta para a rede que você precisa. Se o proxy rejeitar sua solicitação com status 407, a causa quase sempre são credenciais ausentes ou erradas; o guia em Erro HTTP 407 e o passo a passo em autenticação de proxy cubra como consertar isso. DataImpulse fornece proxies residenciais, proxies de datacenter, e proxies móveis sobre HTTP, HTTPS e SOCKS5, com sessões rotativas e fixas. A segmentação por país está incluída, enquanto a segmentação por estado, cidade, ZIP e ASN são complementos pagos.

Como você raspa páginas pesadas de JavaScript e quando Rust é a escolha certa?

Raspe páginas pesadas de JavaScript dirigindo um navegador sem cabeça, porque reqwest e scraper veem apenas o HTML bruto do servidor e nunca executam scripts. Quando o conteúdo aparece somente após a renderização do lado do cliente, procure uma caixa de automação do navegador como thirtyfour, que fala o protocolo WebDriver, ou chromiumoxide, que controla Chrome por meio do DevTools Protocol.

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(())
}

O exemplo thirtyfour se conecta a um chromedriver em execução, carrega uma página, aguarda um elemento renderizado e lê seu texto. Essa abordagem é muito mais pesada do que uma busca HTTP simples, portanto, use-a apenas para as páginas que realmente precisam dela e mantenha o caminho reqwest rápido para todo o resto. Para uma análise mais aprofundada dessa compensação, consulte os guias em raspando páginas da web dinâmicas e web scraping com JavaScript. A tabela abaixo resume como as caixas principais se encaixam:

Caixa Papel Executa JavaScript Melhor para
reqwest Cliente HTTP Não Buscando APIs HTML e JSON brutas
scraper Analisador HTML Não Extração de dados do seletor CSS
thirtyfour Cliente WebDriver Sim Automação completa do navegador e formulários
chromiumoxide DevTools Protocol Sim Controle rápido Chrome sem cabeça

Quanto à escolha da linguagem, escolha Rust quando precisar de desempenho sustentado e baixo uso de recursos em escala, e escolha Python quando você valoriza o desenvolvimento rápido e um ecossistema de scraping mais amplo, como Scrapy e Playwright. Um padrão comum é criar um protótipo de regras de extração em Python e, em seguida, portar o caminho ativo para Rust quando a lógica estiver estável e o volume aumentar. Seja qual for a sua escolha, a camada de rede é tão importante quanto o analisador, e os proxies de origem ética mantêm grandes trabalhos confiáveis.

Principais caixas Rust e suas funções de raspagem

Perguntas frequentes

Quais caixas Rust são melhores para web scraping?

O emparelhamento mais comum é reqwest para solicitações HTTP e scraper para análise HTML com seletores CSS. Adicione tokio para simultaneidade assíncrona, serde para exportação de dados e uma caixa de automação de navegador como thirtyfour ou chromiumoxide quando um site precisar de renderização JavaScript.

Rust pode raspar páginas renderizadas por JavaScript?

Não apenas com reqwest e scraper, pois eles veem apenas o HTML bruto que o servidor retorna e não podem executar scripts. Para extrair conteúdo renderizado pelo cliente, você dirige um navegador sem cabeça por meio de uma caixa WebDriver, como thirtyfour, ou uma caixa DevTools Protocol, como chromiumoxide.

Como você exporta dados copiados em Rust?

Modele cada registro como uma estrutura que deriva Serialize de serde e, em seguida, grave uma fatia dessas estruturas em JSON com serde_json ou em CSV com a caixa csv. Um modelo digitado pode alimentar ambos os formatos sem duplicar a lógica de extração.

Como você executa solicitações simultâneas sem sobrecarregar um site?

Transforme seu URLs em um fluxo assíncrono e aplique buffer_unordered com um limite fixo, que limita quantas solicitações são executadas de uma vez. Inicie cerca de dez a vinte solicitações simultâneas e combine maior simultaneidade com um pool de proxy rotativo para distribuir a carga por muitos IPs.

Preciso de um proxy para web scraping Rust?

Não para trabalhos pequenos, mas os proxies tornam-se importantes em escala para distribuir solicitações e alcançar conteúdo geoespecífico. Configure um com reqwest::Proxy e basic_auth e use IPs residencial ou móvel rotativo para reduzir limites e bloqueios de taxa.

Quando DataImpulse não é o ajuste certo?

Se você precisar de proxies de ISP estáticos, um API de raspagem totalmente gerenciado ou acesso a sites bancários e governamentais, o DataImpulse não é a ferramenta certa. Ele se concentra na rotação de proxies residenciais, móveis e de datacenter para coletar dados públicos e acessar conteúdo.

Raspe de forma confiável com proxies éticos

Depois que seu Rust scraper busca, analisa e exporta de forma limpa, uma rede proxy confiável é o que o mantém funcionando em escala. DataImpulse oferece IPs de origem ética e pré-pago a partir de 1 dólar por GB com tráfego sem vencimento, para que você possa criar uma conta e encaminhe suas primeiras solicitações em minutos.


Share article: