Rust para Web Crawling e Scraping em 2026: reqwest, scraper e Boas Práticas

Como construir crawlers e scrapers em Rust com reqwest, scraper, fantoccini e Tokio em 2026: arquitetura, concorrência, respeito a robots.txt, proxies, rate limit e erros em produção.

Por que Rust é uma base sólida para crawling e scraping

Coletar dados da web em escala é um problema de engenharia tanto quanto de parsing. O trabalho envolve milhares de conexões simultâneas, controle fino de timeouts, retentativas, filas de URLs, respeito aos limites do servidor, tratamento de respostas parciais e tolerância a falhas por domínio. Rust oferece exatamente o perfil de ferramentas para isso: um runtime assíncrono maduro com Tokio, um cliente HTTP de alto nível com Reqwest sobre Hyper, e tipos que impedem grande parte dos bugs de memória e concorrência que assombram pipelines equivalentes em linguagens dinâmicas.

Para quem busca vagas Rust e quer construir carreira em backend, engenharia de dados ou infraestrutura, escrever um crawler robusto é um exercício completo. Ele exige entender bem async, canais, isolamento de erros, configuração, observabilidade com tracing e testes. O ecossistema brasileiro de empresas usando Rust cresce em fintechs, logtechs e plataformas de dados, e coleta de dados públicos aparece com frequência em entrevistas e projetos reais. Quem já construiu o projeto web crawler da comunidade encontra aqui uma continuação natural: mesma base, mas com scraping estruturado e boas práticas de produção.

O kit básico: reqwest + scraper + tokio

A pilha mais comum para coleta de dados em Rust começa com três crates. reqwest cuida das requisições HTTP assíncronas. scraper faz a seleção de elementos no HTML usando seletores CSS, sobre uma árvore construída com html5ever, a mesma base usada por navegadores do projeto Servo. tokio provê o runtime e utilidades como canais, timers e tarefas concorrentes.

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "gzip"] }
scraper = "0.20"
anyhow = "1"

Um exemplo mínimo de scraping estruturado:

use anyhow::Result;
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<()> {
    let client = reqwest::Client::builder()
        .user_agent("rustlang-br-crawler/1.0 (+https://rustlang.com.br)")
        .build()?;

    let resp = client
        .get("https://example.com/artigos")
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&resp);
    let titulo = Selector::parse("h1.titulo")?;
    let links = Selector::parse("a.lista__item")?;

    for el in document.select(&links) {
        let href = el.value().attr("href").unwrap_or_default();
        let texto = el.text().collect::<Vec<_>>().join("");
        println!("{href} -> {}", texto.trim());
    }

    Ok(())
}

Note três decisões importantes que já aparecem nesse trecho. Primeiro, o user_agent é explícito e identificável, com URL de contato. Segundo, usamos error_for_status() para transformar respostas HTTP 4xx e 5xx em erros Rust, evitando processar HTMLs de erro como se fossem conteúdo. Terceiro, mantemos o Client reutilizado entre chamadas: ele mantém o pool de conexões e o cache de TLS, o que reduz drasticamente o custo de muitas requisições ao mesmo domínio.

Arquitetura de um crawler de verdade

O exemplo acima é suficiente para extração pontual, mas um crawler de produção precisa de uma arquitetura explícita. Os componentes costumam ser cinco: uma fronteira de URLs, um dispatcher com controle de concorrência por domínio, um fetcher de páginas, um parser que extrai dados e novos links e um sink que persiste o resultado.

A fronteira de URLs é uma fila persistentável. Pode ser um banco como PostgreSQL via sqlx ou SQLite embutido, um Redis ou uma fila de mensagens. O importante é que cada URL tenha estado (pendente, em_progresso, feito, erro) para permitir retomada sem retrabalho. Um crawler que cai no meio da execução e recomeça tudo do zero se torna um problema operacional rápido.

O dispatcher controla paralelismo. A armadilha clássica é abrir centenas de tarefas contra o mesmo domínio e tomar 429 Too Many Requests ou um banimento. A solução é manter uma fila separada por domínio e um semáforo por domínio limitando a concorrência. O Tokio oferece tokio::sync::Semaphore e canais como mpsc que cabem perfeitamente nesse papel.

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, Semaphore};

struct DominioLimiter {
    semaforos: Mutex<HashMap<String, Arc<Semaphore>>>,
    max_por_dominio: usize,
}

impl DominioLimiter {
    fn novo(max_por_dominio: usize) -> Self {
        Self { semaforos: Mutex::new(HashMap::new()), max_por_dominio }
    }

    async fn acquire(&self, dominio: &str) -> tokio::sync::OwnedSemaphorePermit {
        let sem = {
            let mut mapa = self.semaforos.lock().await;
            mapa.entry(dominio.to_string())
                .or_insert_with(|| Arc::new(Semaphore::new(self.max_por_dominio)))
                .clone()
        };
        sem.acquire_owned().await.unwrap()
    }
}

Esse padrão mantém a concorrência global alta sem esmagar servidores individuais. É a diferença entre um crawler que escala e um que vira ruído na infraestrutura alheia.

Respeito a robots.txt e boas práticas éticas

Crawling é uma atividade pública, mas isso não significa livre de responsabilidade. Antes de qualquer coleta em escala, leia e respeite o /robots.txt do domínio. O arquivo declara quais caminhos cada User-agent pode visitar e, frequentemente, um Crawl-delay que recomenda um intervalo mínimo entre requisições. O crate robots_txt faz o parsing correto e deve ser consultado sempre que um novo domínio entra na fronteira.

Outras práticas que distinguem um crawler profissional de um script apressado:

  • Identificação clara: use um user-agent com nome do projeto e contato.
  • Rate limit explícito: nunca confie apenas no semáforo. Adicione um atraso entre requisições por domínio, ajustado ao Crawl-delay ou a um mínimo conservador.
  • Timeouts realistas: defina tempos máximos de conexão e de resposta para evitar tarefas presas em servidores lentos.
  • Retentativa com backoff: erre em 429 e 5xx com espera exponencial, mas evite retentar 4xx definitivos.
  • Cabeçalhos condicionais: use ETag e If-Modified-Since para pular páginas que não mudaram.
  • Profundidade e priorização: nem toda URL merece a mesma urgência. Defina profundidade máxima e priorize por domínio e por relevância.

Coleta de dados públicos não precisa ser agressiva para ser útil. Quem mantém um crawler por meses aprende que servidores bem tratados raramente bloqueiam.

Quando um navegador headless é necessário

A maior parte do conteúdo ainda vem em HTML estático, mas cada vez mais sites renderizam tudo em JavaScript. Nesses casos, reqwest baixa um shell vazio e o scraper não encontra os elementos esperados. A solução é executar um navegador controlado por uma API de automação.

Em Rust, a opção madura para automação com WebDriver é fantoccini, que conversa com ChromeDriver ou GeckoDriver. Outra alternativa é chromiumoxide, mais integrado ao CDP (Chrome DevTools Protocol). O custo é maior: cada página abre um processo de navegador, consome memória e demora mais para carregar.

use fantoccini::{Client, ClientBuilder, Locator};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cliente = ClientBuilder::native()
        .connect("http://localhost:4444")
        .await?;

    cliente.goto("https://example.com/busca?q=rust").await?;
    cliente
        .wait_for_find(Locator::Css(".resultado__item"))
        .await?;

    let html = cliente.source().await?;
    let documento = scraper::Html::parse_document(&html);
    // ... seletores normais do scraper ...

    cliente.close().await?;
    Ok(())
}

A recomendação prática é usar navegador headless apenas quando estritamente necessário e limitar o escopo à renderização inicial. Assim que o HTML estiver disponível, transfira o parsing para scraper, mais leve e rápido, e feche a página. Híbridos assim mantêm o custo controlável.

Erros, retentativas e isolamento por domínio

Coleta em escala encontra todo tipo de falha: DNS, timeout, TLS expirado, redirect infinito, HTML malformado, mudança de layout, bloqueio por IP e queda de rede. Um crawler robusto precisa tratar cada falha como um evento normal, não como exceção.

A combinação de anyhow e thiserror resolve bem a fronteira de erro. Use thiserror para os erros esperados do domínio (URL inválida, robots proibindo, schema desconhecido) e anyhow para orquestrar handlers. Registre tudo com tracing, com campos estruturados por URL e domínio, para depurar depois.

Importante: nunca deixe uma falha em um domínio derrubar o restante. Cada tarefa de coleta deve ser isolada, com timeout próprio e captura de erro na fronteira. Um domínio que falha dez vezes seguidas pode ser marcado como pausado por algumas horas e reavaliado depois. Esse tipo de circuit breaker é o que mantém um crawler rodando por semanas sem intervenção manual.

Proxies, sessões e autenticação

Em alguns cenários, coleta legítima passa por proxies. Pode ser para distribuir carga, acessar conteúdo regional ou respeitar limites por IP. O reqwest aceita proxies HTTP, HTTPS e SOCKS5 e permite rotação por requisição.

Sessões autenticadas exigem cuidado extra. Cookies devem ser armazenados em um cookie_store do reqwest, e cabeçalhos como Authorization precisam ser renovados conforme expiram. Para qualquer coleta que envolva login, verifique sempre os termos de serviço do site e prefira APIs oficiais quando existirem. Boa parte dos bloqueios que parecem arbitrários vem de comportamentos que o site já documenta como proibidos.

Quando vale a pena sair de Python e escrever em Rust

Muitos pipelines de dados começam em Python com Scrapy, BeautifulSoup e requests. Para volumes moderados, esse stack é excelente. A migração para Rust faz sentido quando três condições aparecem juntas: o volume de páginas por dia passa da casa das centenas de milhares, a latência por requisição importa para o custo total e a estabilidade de execução prolongada vira prioridade.

Rust não substitui a velocidade de prototipação do Python, mas brilha em consumo previsível de CPU e memória. Um crawler Rust de produção pode rodar em uma máquina menor, manter dezenas de milhares de conexões controladas e passar dias sem reiniciar. Para quem já decidiu aprender Rust e busca um projeto onde a linguagem aparece inteira — async, concorrência, erros, configuração, observabilidade —, um crawler é um dos melhores exercícios possíveis, com valor direto para a carreira Rust e portfólio real.

Conclusão

Web crawling e scraping em Rust em 2026 são realidade madura. A combinação de reqwest, scraper, Tokio e, quando necessário, fantoccini cobre praticamente todo o espectro de coleta de dados, do site estático à aplicação renderizada em JavaScript. A vantagem real não é apenas velocidade: é a possibilidade de construir pipelines que rodam por semanas, respeitam o lado do servidor e produzem dados limpos com erros tratados explicitamente.

Quem está começando pode replicar o projeto web crawler e evoluir para um scraper estruturado. Quem já trabalha com pipelines de dados encontra em Rust uma base estável para crescer sem reescrever a cada mudança de volume — algo que empresas brasileiras que usam Rust para ingestão de dados já descobriram na prática. Em um mercado que paga bem e busca engenheiros capazes de operar sistemas distribuídos com segurança, dominar essa stack é um diferencial concreto.