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-delayou 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
ETageIf-Modified-Sincepara 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.