Atualizado para 2026. Versões dos crates:
reqwest 0.13,scraper 0.27,tokio 1.x,encoding_rs 0.8. Mudança importante: a partir doreqwest 0.13, o backend TLS padrão é o rustls (Rust puro), e não o OpenSSL/native-tls do sistema.
Índice
- Introdução: por que Rust para o scraping
- Como baixamos a página (cliente HTTP)
- Bibliotecas para o parsing do conteúdo
- Como resolver os problemas de codificação
- Multithreading e assincronia
- Uso de proxies
- Scraping através do TOR
- Trabalhar com HTTPS / SSL
- Trabalhar com cookies
- Status da resposta e cabeçalhos
- Armazenamento de URLs e filas (um panorama)
- Extra: o que costuma ser esquecido
- Cortesia, robots.txt, rate limiting
- Novas tentativas e backoff
- Páginas JavaScript (navegadores headless)
- User-Agent e proteções anti-bots
- Tratamento de erros e logging
- Arquitetura de um crawler completo
- Principais vantagens e desvantagens da implementação em Rust
- Aspectos legais e éticos
1. Introdução
O web scraping consiste em obter automaticamente o HTML/JSON/XML das páginas e extrair deles dados estruturados. Todo scraper se compõe de duas grandes partes:
- a camada de rede — baixa a página (cliente HTTP);
- a camada de parsing — converte o HTML «bruto» nos campos de que você precisa (parser + seletores).
Depois entram proxies, concorrência, contorno de proteções anti-bots, armazenamento da fila de links etc. Rust se destaca porque oferece velocidade comparável à de C e consumo mínimo de memória com paralelismo seguro — exatamente o que é crítico quando você baixa milhões de páginas.
O Cargo.toml inicial, ao qual vamos acrescentando funcionalidades aos poucos:
[package]
name = "parser-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.13", features = ["json", "gzip", "brotli"] }
tokio = { version = "1", features = ["full"] }
scraper = "0.27"
encoding_rs = "0.8"
anyhow = "1" # tratamento de erros práticoPáginas oficiais dos crates básicos: reqwest, tokio, scraper, encoding_rs, anyhow.
2. Como baixamos a página
No ecossistema de Rust há vários clientes HTTP. Para o scraping, em 99% dos casos a escolha é o reqwest.
| Crate | Quando usar |
|---|---|
reqwest |
A opção principal. Async + blocking, proxies, cookies, TLS: vem com tudo. |
ureq |
Cliente síncrono leve, sem tokio. Para scripts simples. |
isahc |
Cliente async baseado na libcurl. |
hyper |
De baixo nível. Necessário quando você constrói seu próprio cliente/servidor. |
Documentação: docs.rs/reqwest.
2.1 A requisição mais simples (async)
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let body = reqwest::get("https://example.com")
.await? // esperamos a resposta
.text() // lemos o corpo como string
.await?;
println!("{body}");
Ok(())
}2.2 A forma correta — um Client reutilizável
reqwest::get cria um cliente novo a cada chamada. Isso sai caro: perde-se o pool de conexões (keep-alive). Crie um único Client e clone-o — por dentro ele é um Arc, e o clone é barato.
use std::time::Duration;
use reqwest::Client;
fn build_client() -> anyhow::Result<Client> {
let client = Client::builder()
// nos passamos por um navegador comum
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/124.0 Safari/537.36")
.timeout(Duration::from_secs(30)) // timeout global da requisição
.connect_timeout(Duration::from_secs(10)) // timeout de estabelecimento da conexão
.gzip(true) // descompressão automática de gzip
.brotli(true) // descompressão automática de brotli
.build()?;
Ok(client)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = build_client()?;
let resp = client
.get("https://example.com")
.header("Accept-Language", "pt-BR,pt;q=0.9")
.send()
.await?;
println!("Status: {}", resp.status());
let html = resp.text().await?;
println!("Tamanho do HTML: {}", html.len());
Ok(())
}2.3 A variante síncrona (sem tokio)
Se você não quer carregar um runtime async para um script pequeno, existe o blocking:
reqwest = { version = "0.13", features = ["blocking"] }fn main() -> anyhow::Result<()> {
let body = reqwest::blocking::get("https://example.com")?.text()?;
println!("{body}");
Ok(())
}Não chame o cliente
blockingdentro de um runtime async: isso provoca um panic. Escolha um dos dois caminhos.
3. Bibliotecas para o parsing
Uma vez baixado o HTML, é preciso analisá-lo. A regra de ouro: não parseie HTML com expressões regulares. O HTML não é uma linguagem regular; essa abordagem quebra na primeira aspa sem escape. As regex só fazem sentido para extrair detalhes menores de um texto já localizado.
| Crate | Abordagem | Observações |
|---|---|---|
scraper |
Seletores CSS | O mais popular. Wrapper sobre o html5ever, do Servo. |
dom_query |
Seletores CSS + manipulação | Alternativa recente; sabe modificar o DOM. |
select |
DSL própria de predicados | Mais antigo, porém funcional. |
html5ever |
tokenizador de baixo nível | Parser com qualidade de navegador. Usado dentro do scraper. |
lol_html |
rewriter em streaming | Da Cloudflare. Para documentos muito grandes, «em pleno voo». |
quick-xml |
XML / RSS / sitemap | Parser XML rápido em streaming. |
serde_json |
JSON | Para respostas de API e JSON embutido. |
3.1 scraper + seletores CSS
Documentação e exemplos: docs.rs/scraper.
use scraper::{Html, Selector};
fn parse_articles(html: &str) -> anyhow::Result<()> {
let document = Html::parse_document(html);
// Convém compilar os seletores uma única vez (fora do loop).
let item_sel = Selector::parse("article.post").unwrap();
let title_sel = Selector::parse("h2.title > a").unwrap();
let date_sel = Selector::parse("time.published").unwrap();
for item in document.select(&item_sel) {
let title = item
.select(&title_sel)
.next()
.map(|e| e.text().collect::<String>().trim().to_string())
.unwrap_or_default();
// o link, do atributo href
let link = item
.select(&title_sel)
.next()
.and_then(|e| e.value().attr("href"))
.unwrap_or("");
// a data, do atributo datetime
let date = item
.select(&date_sel)
.next()
.and_then(|e| e.value().attr("datetime"))
.unwrap_or("");
println!("{title} | {date} | {link}");
}
Ok(())
}Truques úteis do scraper:
element.text().collect::<String>()— reúne todo o texto interno (incluindo o aninhado).element.value().attr("href")— pega um atributo.element.html()/element.inner_html()— devolve o HTML original.- Os seletores aceitam
[attr="value"],:nth-child,>, (descendentes) etc.
3.2 JSON de uma API
Muitas vezes é mais simples pegar os dados não do HTML, mas da API JSON oculta que a própria página consulta. Abra o DevTools → aba Network → localize a requisição que devolve JSON. É mais confiável do que qualquer parsing da marcação. A desserialização é feita com serde + serde_json.
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Product {
id: u64,
name: String,
price: f64,
}
async fn fetch_products(client: &reqwest::Client) -> anyhow::Result<Vec<Product>> {
let products = client
.get("https://shop.example.com/api/products")
.send()
.await?
.json::<Vec<Product>>() // desserialização direta para structs
.await?;
Ok(products)
}3.3 Sitemap e RSS com quick-xml
Os mapas do site (sitemap.xml) são a melhor maneira de conhecer todas as URLs de um site sem percorrer seus links. Eles são parseados como XML comum com o quick-xml (ou com um crate especializado como o sitemap).
4. Codificações e caracteres especiais
Uma dor clássica ao scrapear a parte mais antiga da web lusófona: muitos sites veteranos servem o conteúdo em Windows-1252 ou ISO-8859-1 (Latin-1), e não em UTF-8.
Por que quebra
O método resp.text() determina a codificação a partir do cabeçalho Content-Type: text/html; charset=.... Se o cabeçalho não indica o charset e ele só aparece no HTML (<meta charset="windows-1252">), o reqwest assume por padrão que é UTF-8 — e você recebe «mojibake»: informa��o em vez de informação (ou, no caso inverso, café em vez de café).
A solução: ler os bytes e decodificar por conta própria
Pegue os bytes «brutos» com .bytes() e decodifique-os com a codificação correta usando o encoding_rs (o mesmo motor que o Firefox usa).
use encoding_rs::{Encoding, WINDOWS_1252, UTF_8};
async fn get_text_win1252(client: &reqwest::Client, url: &str) -> anyhow::Result<String> {
let resp = client.get(url).send().await?;
let bytes = resp.bytes().await?;
// Decodificamos como Windows-1252.
let (text, _enc, had_errors) = WINDOWS_1252.decode(&bytes);
if had_errors {
eprintln!("Atenção: houve erros durante a decodificação");
}
Ok(text.into_owned())
}Detecção automática da codificação
Melhor do que cravar a codificação no código é detectá-la. O algoritmo:
- Primeiro, olhe o
charsetdo cabeçalhoContent-Type. - Se não estiver lá, procure
<meta charset=...>/<meta http-equiv="Content-Type">nos primeiros kilobytes do HTML. - Se também não aparecer aí, tente adivinhá-la estatisticamente (crate
chardetng).
use encoding_rs::Encoding;
use reqwest::header::CONTENT_TYPE;
async fn get_text_smart(client: &reqwest::Client, url: &str) -> anyhow::Result<String> {
let resp = client.get(url).send().await?;
// 1) tentamos pegar o charset do cabeçalho
let header_charset = resp
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.and_then(|ct| ct.split("charset=").nth(1))
.map(|s| s.trim().to_string());
let bytes = resp.bytes().await?;
// 2) se não está no cabeçalho, procuramos no <meta> (simplificado: primeiros 1024 bytes)
let charset = header_charset.or_else(|| {
let head = String::from_utf8_lossy(&bytes[..bytes.len().min(1024)]);
head.to_lowercase()
.split("charset=")
.nth(1)
.map(|s| s.trim_matches(|c: char| !c.is_ascii_alphanumeric() && c != '-')
.to_string())
});
// 3) escolhemos a codificação (UTF-8 por padrão)
let enc = charset
.as_deref()
.and_then(|name| Encoding::for_label(name.as_bytes()))
.unwrap_or(encoding_rs::UTF_8);
let (text, _, _) = enc.decode(&bytes);
Ok(text.into_owned())
}Alternativa: o método
resp.text_with_charset("windows-1252")doreqwestusa a codificação indicada como reserva quando o charset não vem no cabeçalho. É mais simples, mas não cobre o caso «o cabeçalho diz UTF-8 e na verdade é 1252».
5. Multithreading e assincronia
O scraping é quase sempre I/O-bound: o processador fica ocioso enquanto os pacotes viajam pela rede. Por isso, em Rust, quem ganha aqui não são as «threads», e sim a assincronia sobre o tokio: milhares de requisições simultâneas em uma ou duas threads do sistema operacional.
Distinga duas tarefas:
- Baixar (I/O-bound) → async/
tokio, muitas conexões simultâneas. - Parsear (CPU-bound: o html5ever castiga a CPU) → com grandes volumes, mova o trabalho para o
rayonou paratokio::task::spawn_blocking, para não bloquear o runtime async.
5.1 Concorrência com limite — buffer_unordered
A forma mais idiomática: convertemos o fluxo de URLs em um fluxo de futures, e buffer_unordered(N) executa no máximo N por vez (do crate futures).
use futures::stream::{self, StreamExt};
async fn crawl_many(client: &reqwest::Client, urls: Vec<String>) {
let concurrency = 20; // no máximo 20 requisições por vez
let results = stream::iter(urls)
.map(|url| {
let client = client.clone(); // o clone é barato (Arc por dentro)
async move {
match client.get(&url).send().await {
Ok(resp) => {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
(url, status.as_u16(), body.len())
}
Err(e) => {
eprintln!("Erro {url}: {e}");
(url, 0, 0)
}
}
}
})
.buffer_unordered(concurrency)
.collect::<Vec<_>>()
.await;
for (url, status, len) in results {
println!("{status} {len:>8} {url}");
}
}5.2 Limite com Semaphore
Quando as tarefas são disparadas com tokio::spawn, o limite é mantido com um semáforo:
use std::sync::Arc;
use tokio::sync::Semaphore;
async fn crawl_with_semaphore(client: reqwest::Client, urls: Vec<String>) {
let sem = Arc::new(Semaphore::new(20)); // no máximo 20 «em voo»
let mut handles = Vec::new();
for url in urls {
let client = client.clone();
let sem = sem.clone();
handles.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap(); // esperamos uma vaga livre
let _ = client.get(&url).send().await;
// a permissão é liberada ao sair do escopo
}));
}
for h in handles {
let _ = h.await;
}
}5.3 Parsing CPU-bound com rayon
Se você já tem milhares de HTMLs baixados e precisa parseá-los rápido, isso é trabalho para todos os núcleos (rayon):
use rayon::prelude::*;
fn parse_all(pages: Vec<String>) -> Vec<usize> {
pages
.par_iter() // iterador paralelo
.map(|html| {
let doc = scraper::Html::parse_document(html);
doc.select(&scraper::Selector::parse("a").unwrap()).count()
})
.collect()
}6. Proxies
Os proxies servem para (a) não esbarrar em um bloqueio por IP durante o scraping em massa e (b) contornar restrições geográficas. O reqwest aceita proxies HTTP, HTTPS e SOCKS5.
Para SOCKS, ative a feature:
reqwest = { version = "0.13", features = ["socks"] }6.1 Um proxy por cliente
use reqwest::{Client, Proxy};
fn client_with_proxy() -> anyhow::Result<Client> {
let proxy = Proxy::all("http://proxy.example.com:8080")?
.basic_auth("user", "password"); // se for necessária autenticação
let client = Client::builder()
.proxy(proxy)
.build()?;
Ok(client)
}Proxy::http(...), Proxy::https(...) e Proxy::all(...) fixam o proxy para os esquemas correspondentes. SOCKS5:
let proxy = reqwest::Proxy::all("socks5://127.0.0.1:1080")?;6.2 Rotação de um pool de proxies
Cada Client fica vinculado a um único proxy. Para rotacionar, o mais prático é manter um cliente por proxy e escolhê-los em revezamento (round-robin):
use std::sync::atomic::{AtomicUsize, Ordering};
use reqwest::{Client, Proxy};
struct ProxyPool {
clients: Vec<Client>,
idx: AtomicUsize,
}
impl ProxyPool {
fn new(proxies: &[&str]) -> anyhow::Result<Self> {
let clients = proxies
.iter()
.map(|p| {
Client::builder()
.proxy(Proxy::all(*p)?)
.build()
.map_err(Into::into)
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(Self { clients, idx: AtomicUsize::new(0) })
}
/// Devolve o próximo cliente do ciclo (round-robin).
fn next(&self) -> &Client {
let i = self.idx.fetch_add(1, Ordering::Relaxed) % self.clients.len();
&self.clients[i]
}
}Os proxies residenciais/móveis com rotação automática do lado do provedor costumam entregar um único endereço «gateway»; nesse caso, você não precisa rotacionar nada: basta um único cliente.
7. Scraping através do TOR
O TOR oferece anonimato e uma «rotação» de IP gratuita (novo circuito → novo nó de saída). Há dois caminhos.
7.1 O caminho simples: TOR externo + SOCKS5
Inicie o TOR do sistema (o daemon tor ou o Tor Browser), que sobe um proxy SOCKS5 em 127.0.0.1:9050 (no Tor Browser, o 9150). A partir daí, funciona como qualquer proxy SOCKS:
use reqwest::{Client, Proxy};
fn tor_client() -> anyhow::Result<Client> {
// IMPORTANTE: socks5h (com a letra h), não socks5.
// 'h' = resolução DNS do lado do proxy (dentro do TOR);
// caso contrário, haverá vazamentos de DNS e os .onion não funcionarão.
let proxy = Proxy::all("socks5h://127.0.0.1:9050")?;
let client = Client::builder()
.proxy(proxy)
.build()?;
Ok(client)
}Verificação de que o tráfego passa pelo TOR:
async fn check_tor(client: &reqwest::Client) -> anyhow::Result<()> {
let txt = client
.get("https://check.torproject.org/api/ip")
.send().await?
.text().await?;
println!("{txt}"); // {"IsTor":true,"IP":"..."}
Ok(())
}A troca de circuito (IP nova) é feita através da porta de controle do TOR (normalmente a 9051): é preciso enviar o sinal NEWNYM. Dá para fazer manualmente com o protocolo da porta de controle ou com um crate que o encapsule. Depois de um NEWNYM, convém respeitar uma pausa (o TOR limita a frequência de troca a cerca de uma vez a cada 10 segundos).
7.2 TOR embutido: arti
O Arti é a implementação do TOR em Rust puro do próprio Tor Project. Ele permite embutir o TOR diretamente na aplicação, sem daemon externo. A API cliente de alto nível está no crate arti-client (docs.rs).
arti-client = "..." # confira a versão atual: cargo add arti-client
tor-rtcompat = "..."use arti_client::{TorClient, TorClientConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = TorClientConfig::default();
// Subimos o cliente TOR embutido e aguardamos o bootstrap.
let tor = TorClient::create_bootstrapped(config).await?;
// Depois é possível abrir streams TCP anônimos (AsyncRead/AsyncWrite)
// e enviar HTTP sobre eles manualmente ou via hyper.
let mut stream = tor.connect(("example.com", 80)).await?;
// ... envio da requisição HTTP pelo stream ...
Ok(())
}Existe também um crate «cola», o artiqwest, que roteia as requisições HTTP através do arti com uma API no estilo do reqwest (get/post), incluindo os .onion e os websockets.
Desvantagens do arti: a API ainda não está estabilizada (até a 1.x pode haver breaking changes) e não cobre todas as funções do C-Tor. Em compensação, você não precisa de um processo externo e o deploy é mais simples.
Aviso: os nós de saída do TOR estão saturados, são lentos e com frequência aparecem bloqueados nos sites populares. O TOR é bom para o anonimato e para acessar os .onion, mas ruim como «pool gratuito de proxies rápidos».
8. HTTPS / SSL
A partir do reqwest 0.13, o HTTPS funciona «de fábrica»: o backend padrão é o rustls (Rust puro, sem depender do OpenSSL do sistema). Normalmente não há nada para configurar.
8.1 Escolha do backend TLS
# rustls (padrão) — multiplataforma, não precisa de OpenSSL
reqwest = { version = "0.13" }
# ou o TLS do sistema (schannel no Windows, Secure Transport no macOS, OpenSSL no Linux)
reqwest = { version = "0.13", default-features = false, features = ["native-tls"] }
# ou OpenSSL compilado estaticamente (prático para distribuir o binário)
reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored"] }Backends TLS: rustls, native-tls, openssl.
8.2 Ignorar os erros de certificado (perigoso!)
Às vezes é preciso scrapear um site com certificado autoassinado ou vencido. Dá para desativar a verificação — mas somente para testes e hosts confiáveis, porque isso elimina a proteção contra ataques MITM:
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true) // ⚠ inseguro
.build()?;8.3 Certificado raiz próprio / certificado de cliente
use reqwest::{Certificate, Identity};
// Adicionar uma CA corporativa ou autoassinada:
let ca = Certificate::from_pem(&std::fs::read("my-ca.pem")?)?;
// Certificado de cliente (mTLS):
let id = Identity::from_pem(&std::fs::read("client.pem")?)?;
let client = reqwest::Client::builder()
.add_root_certificate(ca)
.identity(id)
.build()?;9. Cookies
Os cookies são necessários para sessões, autenticação e para contornar algumas proteções. O reqwest sabe armazená-los e anexá-los automaticamente entre requisições.
Ative a feature:
reqwest = { version = "0.13", features = ["cookies"] }9.1 Cookie store automático
let client = reqwest::Client::builder()
.cookie_store(true) // ativar o armazenamento automático de cookies
.build()?;
// 1) fazemos login — o servidor devolverá Set-Cookie e o cliente vai memorizá-los
client.post("https://site.example/login")
.form(&[("user", "alice"), ("pass", "secret")])
.send().await?;
// 2) as requisições seguintes sairão automaticamente com esses cookies
let dashboard = client.get("https://site.example/dashboard")
.send().await?
.text().await?;9.2 Um cookie jar próprio (acesso aos valores / reutilização)
Quando você precisa ler ou definir cookies manualmente, ou transferi-los de uma sessão para outra:
use std::sync::Arc;
use reqwest::cookie::{Jar, CookieStore};
use reqwest::Url;
let jar = Arc::new(Jar::default());
// Colocar um cookie manualmente, de antemão:
let url: Url = "https://site.example/".parse()?;
jar.add_cookie_str("session=abc123; Domain=site.example; Path=/", &url);
let client = reqwest::Client::builder()
.cookie_provider(jar.clone()) // usamos o nosso jar
.build()?;
// depois das requisições, é possível ler do jar os cookies acumulados9.3 Cookies manuais no cabeçalho
Se você não quer ativar a gestão automática, pode passar os cookies diretamente como cabeçalho:
let resp = client.get(url)
.header(reqwest::header::COOKIE, "session=abc123; lang=pt")
.send().await?;10. Status da resposta e cabeçalhos
Antes de parsear o HTML, quase sempre convém verificar se a página chegou bem (200), e não como 404/403/429/5xx.
use reqwest::StatusCode;
use reqwest::header::{CONTENT_TYPE, CONTENT_LENGTH, LOCATION, RETRY_AFTER};
async fn fetch(client: &reqwest::Client, url: &str) -> anyhow::Result<Option<String>> {
let resp = client.get(url).send().await?;
let status = resp.status();
println!("HTTP {} ({})", status.as_u16(), status.canonical_reason().unwrap_or(""));
// Verificações práticas por categoria de status:
if status.is_success() { // 2xx
// lemos os cabeçalhos que interessam
let headers = resp.headers();
if let Some(ct) = headers.get(CONTENT_TYPE).and_then(|v| v.to_str().ok()) {
println!("Content-Type: {ct}");
// parseamos apenas HTML; as imagens são ignoradas
if !ct.contains("text/html") {
return Ok(None);
}
}
if let Some(len) = headers.get(CONTENT_LENGTH) {
println!("Content-Length: {len:?}");
}
let body = resp.text().await?;
return Ok(Some(body));
}
if status.is_redirection() { // 3xx
if let Some(loc) = resp.headers().get(LOCATION).and_then(|v| v.to_str().ok()) {
println!("Redirecionamento para: {loc}");
}
}
if status == StatusCode::TOO_MANY_REQUESTS { // 429
// o servidor pede para esperar
if let Some(ra) = resp.headers().get(RETRY_AFTER).and_then(|v| v.to_str().ok()) {
println!("Estamos sendo freados. Retry-After: {ra} s");
}
}
Ok(None)
}Métodos úteis:
resp.status()→StatusCode; com.is_success(),.is_client_error(),.is_server_error(),.is_redirection().resp.error_for_status()— converte os 4xx/5xx emErr; prático com?.resp.headers()→HeaderMap, iterável como um map.resp.url()— a URL final após os redirecionamentos.resp.content_length()— o tamanho do corpo, se conhecido.
Por padrão, o
reqwestsegue os redirecionamentos por conta própria (até 10). O comportamento é ajustado com.redirect(reqwest::redirect::Policy::none())ou.limited(n).
11. Armazenamento de URLs e filas
Todo rastreador (crawler) é, em essência, um loop: «tirar uma URL da fila → baixar → extrair os links novos → devolvê-los à fila». Aqui são necessárias duas estruturas:
- a fila (frontier) — o que baixar em seguida;
- o conjunto de visitadas (visited/seen) — para não baixar a mesma coisa duas vezes.
11.1 Em memória (para tarefas pequenas)
use std::collections::{VecDeque, HashSet};
struct Frontier {
queue: VecDeque<String>,
seen: HashSet<String>,
}
impl Frontier {
fn new() -> Self {
Self { queue: VecDeque::new(), seen: HashSet::new() }
}
/// Adiciona a URL se ela ainda não foi vista.
fn push(&mut self, url: String) {
if self.seen.insert(url.clone()) { // insert devolve false se já existia
self.queue.push_back(url);
}
}
fn pop(&mut self) -> Option<String> {
self.queue.pop_front()
}
}Para o acesso concorrente a partir de várias tarefas async, a fila é montada sobre canais: tokio::sync::mpsc, flume ou crossbeam-channel. Os workers leem do canal e escrevem nele os links novos.
11.2 Persistência (para rastreamentos grandes e longos)
Com milhões de URLs a memória se esgota, e se o processo cair você perde o progresso. Por isso a fila e as «visitadas» são levadas para um armazenamento externo:
| Armazenamento | Crate | Quando |
|---|---|---|
| Redis | redis |
Fila distribuída entre vários workers. |
| SQLite | rusqlite / sqlx |
Um único processo com persistência simples. |
| PostgreSQL | sqlx |
Grandes volumes, análises, várias máquinas. |
| RocksDB / sled | rocksdb / sled |
Armazenamento chave-valor local muito rápido. |
Deduplicação em grande escala: guardar todas as URLs em um HashSet sai caro. Emprega-se: - a normalização de URLs (remover o #fragment, ordenar os parâmetros da query, passar o host para minúsculas) com o crate url — caso contrário, uma mesma página entrará com URLs diferentes; - um hash da URL (por exemplo, xxhash-rust / blake3) em vez da string inteira; - o filtro de Bloom (bloomfilter) — uma estrutura probabilística compacta de «talvez vista / com certeza não vista».
Esta seção é apenas um panorama. Na prática, a escolha depende da escala: para alguns milhares de páginas, basta um
HashSetem memória; para um crawler industrial, fila no Redis + filtro de Bloom + normalização de URLs.
12. Extra
O que não entrou na lista inicial, mas sem o qual um scraper real quebra ou acaba bloqueado.
12.1 Cortesia, robots.txt e rate limiting
- robots.txt — o arquivo em que o site indica o que pode ser percorrido e o que não pode. O scraping ético (e, às vezes, o juridicamente prudente) o respeita. Crates:
texting_robots,robotstxt. - Pausas entre requisições ao mesmo domínio, para não derrubar o site nem ganhar um bloqueio. A variante mais simples é
tokio::time::sleep; a profissional, o limitadorgovernor(token bucket):
use std::num::NonZeroU32;
use governor::{Quota, RateLimiter};
// no máximo 5 requisições por segundo
let limiter = RateLimiter::direct(Quota::per_second(NonZeroU32::new(5).unwrap()));
// antes de cada requisição:
limiter.until_ready().await;
// client.get(...).send().await?;12.2 Novas tentativas e backoff
A rede é instável: timeouts, 503, quedas de conexão. São necessárias novas tentativas com espera exponencial (1 s → 2 s → 4 s...). O mais simples são os crates reqwest-middleware + reqwest-retry:
reqwest-middleware = "0.5"
reqwest-retry = "0.9"use reqwest_middleware::ClientBuilder;
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
let client = ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
// daqui em diante, client.get(...).send().await — as novas tentativas rodam sozinhas12.3 Páginas JavaScript (navegadores headless)
reqwest + scraper só enxergam o HTML original. Se o conteúdo é desenhado pelo JavaScript (uma SPA em React/Vue), ele não estará nesse HTML. Opções:
- Encontrar a API oculta (veja o §3.2) — quase sempre o melhor caminho: mais rápido, mais confiável, mais leve.
- Controlar um navegador real (que de fato renderiza o JS):
| Crate | Protocolo | Observações |
|---|---|---|
chromiumoxide |
CDP (Chrome DevTools) | Async; controla o Chrome diretamente. |
thirtyfour |
WebDriver | Compatível com Selenium; API de alto nível prática. |
fantoccini |
WebDriver | Mais leve que o thirtyfour. |
headless_chrome |
CDP | Wrapper síncrono sobre o CDP. |
Um navegador consome dezenas de vezes mais recursos, então use-o apenas onde o JS for imprescindível.
12.4 User-Agent, cabeçalhos e proteções anti-bots
Os sites distinguem bots de pessoas. A camuflagem mínima:
- um
User-Agentverossímil (nãoreqwest/0.13!); - um conjunto realista de cabeçalhos:
Accept,Accept-Language,Accept-Encoding,Referer,Sec-Fetch-*; - rotação de User-Agent e de proxies;
- pausas em ritmo humano.
As proteções sérias (Cloudflare, DataDome, PerimeterX) verificam ainda o fingerprint TLS (JA3/JA4) e a ordem dos cabeçalhos HTTP/2. Um reqwest comum entrega uma impressão digital própria «de Rust», diferente da do Chrome. Para contornar isso existem crates que imitam a impressão digital do navegador com base no curl-impersonate, por exemplo o rquest. É uma «corrida armamentista»: não há garantias.
12.5 Tratamento de erros e logging
- Erros:
anyhowpara aplicações (um?prático e com contexto),thiserrorpara bibliotecas (tipos de erro próprios). Não entre em panic a cada 404: trate-o como um resultado normal. - Logging/tracing:
tracing+tracing-subscriber(oulog+env_logger). Os logs mostram onde estão os gargalos e os bloqueios.
use anyhow::Context;
let html = client.get(url).send().await
.with_context(|| format!("não foi possível baixar {url}"))?
.text().await
.context("não foi possível ler o corpo")?;13. Arquitetura do crawler
O esquema de um scraper de nível industrial que reúne tudo o que vimos:
Princípios-chave: - um único Client compartilhado (pool de conexões) que é clonado para os workers; - a concorrência é limitada com um semáforo e a velocidade por domínio, com um limitador; - cada chamada de rede vem envolta em retry/backoff; - a fila e o conjunto seen são a única fonte de verdade sobre o progresso.
Se você não quer montar tudo manualmente, existem frameworks de rastreamento prontos para usar, por exemplo o
spider.
14. Vantagens e desvantagens
Vantagens da implementação em Rust
- Desempenho: velocidade próxima à de C/C++. Com grandes volumes, Rust supera várias vezes o Python (
requests/BeautifulSoup) e o Go em CPU e memória. - Memória: consumo mínimo e sem pausas de GC — importante com milhões de páginas e rastreamentos de longa duração.
- Multithreading sem medo: o sistema de tipos e o borrow checker caçam as corridas de dados em tempo de compilação. Uma vantagem enorme para um scraper concorrente.
- Confiabilidade: tratamento explícito de erros (
Result) eOptionem vez denull— menos quedas em produção. - Um único binário estático: fácil de fazer o deploy, sem carregar um interpretador nem dependências.
- Ecossistema async maduro:
tokio+reqwestsão production-grade.
Desvantagens
- Curva de entrada: borrow checker, lifetimes, async — leva mais tempo de aprendizado do que um scraper em Python montado «em uma tarde».
- Velocidade de desenvolvimento: um protótipo em Python/
scrapyé escrito mais rápido. Para uma tarefa pontual, Rust pode ser exagero. - Conteúdo dinâmico: há menos soluções prontas «de fábrica» para renderizar JS e contornar proteções anti-bots do que em Python (onde existem Playwright, Scrapy, undetected-chromedriver etc.).
- Compilação: tempos de build longos, sobretudo com dependências pesadas.
- Menos frameworks completos: o Python tem um
scrapymaduro e completo; em Rust, o habitual é montar o pipeline peça por peça (embora existam ospidere outros).
Conclusão: Rust se justifica quando o scraping é massivo, permanente e sensível a recursos (milhões de páginas, requisitos rígidos de velocidade e memória, um serviço de vida longa). Para um «scrapear 500 páginas» pontual, o Python costuma sair mais rápido em esforço total.
15. Aspectos legais e éticos
Tecnicamente é possível fazer muita coisa, o que não significa que se deva. Em resumo:
- robots.txt e ToS: respeite o
robots.txte os termos de uso do site. - Carga: não derrube o servidor alheio — limite a frequência das requisições e scrapeie em horários de baixo movimento.
- Dados pessoais: a coleta deles é regulada por lei (a LGPD no Brasil, o GDPR na UE e normas equivalentes em outros países). Aja com cautela.
- Direitos autorais: o conteúdo pode estar protegido; a cópia e a republicação em massa podem ser ilegais.
- Identifique-se: é sensato incluir um contato no User-Agent, para que o administrador do site possa escrever para você em vez de bloquear às cegas.
Isto é uma orientação geral, não uma consultoria jurídica: em casos duvidosos, consulte um advogado.
Referência rápida de crates
| Tarefa | Crate(s) |
|---|---|
| Cliente HTTP | reqwest (async/blocking), ureq (sync) |
| Parsing de HTML | scraper, dom_query, select |
| JSON / XML | serde_json, quick-xml |
| Codificações legadas | encoding_rs, chardetng |
| Runtime async | tokio, futures |
| Paralelismo de CPU | rayon |
| Rate limiting | governor |
| Novas tentativas | reqwest-middleware, reqwest-retry |
| TOR | TOR externo + socks, ou então arti-client / artiqwest |
| Navegador headless | chromiumoxide, thirtyfour, fantoccini |
| robots.txt | texting_robots |
| Filas/armazenamento | redis, rusqlite / sqlx, sled, bloomfilter |
| Erros/logs | anyhow, thiserror, tracing |
| Framework pronto | spider |