Raspagem de Web em Rust com reqwest e scraper: Um Guia Prático
Expert in Web Scraping Technologies
TL;DR:
- A raspagem na web com Rust em 2026 utiliza dois crates:
reqwestpara a requisição HTTP escraperpara a análise de seletores CSS, comtokiocontrolando o runtime assíncrono. - O compilador força você a lidar com cada caminho de falha, então um raspador em Rust que compila é geralmente um raspador que sobrevive a marcações malformadas e respostas não-200.
tokio::spawntransforma um crawler que processa página por página em um crawler concorrente em cerca de cinco linhas, e este guia o executa através de cinco páginas para 50 registros.- Nem
reqwestnemscraperexecutam JavaScript, então uma página renderizada pelo cliente retorna uma marcação que é analisada limpidamente para zero registros. - A API de Raspagem Universal Scrapeless renderiza a página primeiro e retorna HTML que o mesmo código
scraperinalterado analisa para 10 registros. - Comece no plano gratuito do Scrapeless e execute o exemplo pivot contra seu próprio alvo.
Rust aparece no trabalho de raspagem por um motivo específico: o crawler é normalmente a parte de um pipeline de dados que opera sem supervisão por horas contra marcações que ninguém controla. Um binário verificado por borrows, sem coletor de lixo, que pausa e trata explicitamente Result em cada limite, é uma boa escolha para esse trabalho.
Este guia constrói um raspador funcional com os lançamentos atuais dos crates, o executa e então mostra exatamente onde a pilha de Rust puro para — com números medidos em vez de um aviso.
O Que Você Precisa
A raspagem na web com Rust precisa de três crates e de uma ferramenta estável. As versões abaixo são os lançamentos publicados atuais no registro de crates da comunidade Rust no momento da escrita, e cada exemplo aqui foi compilado e executado exatamente contra essas versões:
| Crate | Versão | Função |
|---|---|---|
reqwest |
0.13.4 | Cliente HTTP |
scraper |
0.27.0 | Análise de HTML e seletores CSS |
tokio |
1.53.0 | Runtime assíncrono |
serde_json |
1 | Manipulação de JSON para respostas de API |
O crate scraper envolve html5ever, o mesmo motor de análise usado no Servo, portanto, segue a especificação de análise HTML ao invés de tratar a marcação como texto para regex. Isso é importante em páginas reais, onde tags não fechadas são normais.
Instalação
Crie o projeto e adicione as dependências:
bash
cargo new rust-scraper
cd rust-scraper
Então, declare o bloco de dependências em Cargo.toml:
toml
[package]
name = "rust-scraper"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.13.4", features = ["json"] }
scraper = "0.27.0"
tokio = { version = "1.53.0", features = ["macros", "rt-multi-thread"] }
serde_json = "1"
O recurso json no reqwest puxa a serialização de requisições e respostas. Os recursos macros e rt-multi-thread no tokio são o que fazem o atributo #[tokio::main] funcionar.
Buscar e Analisar uma Página
Um raspador completo em Rust é menor do que sua reputação sugere. Este busca uma página renderizada pelo servidor, seleciona cada contêiner de citação e extrai dois campos de cada um:
rust
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let body = reqwest::get("https://quotes.toscrape.com/")
.await?
.error_for_status()?
.text()
.await?;
let document = Html::parse_document(&body);
let quote_sel = Selector::parse("div.quote").unwrap();
let text_sel = Selector::parse("span.text").unwrap();
let author_sel = Selector::parse("small.author").unwrap();
let mut count = 0;
for quote in document.select("e_sel) {
let text = quote.select(&text_sel).next().map(|e| e.inner_html());
let author = quote.select(&author_sel).next().map(|e| e.inner_html());
if let (Some(t), Some(a)) = (text, author) {
if count == 0 {
println!("primeira citação: {} — {}", t, a);
}
count += 1;
}
}
println!("citações analisadas: {}", count);
Ok(())
}
Executá-lo imprime:
text
primeira citação: “O mundo, como o criamos, é um processo do nosso pensamento. Ele não pode ser mudado sem mudar nosso pensamento.” — Albert Einstein
citações analisadas: 10
Três detalhes nesse código carregam a maior parte do peso.
error_for_status() converte uma resposta 4xx ou 5xx em um Err ao invés de permitir que você analise uma página de erro como se fosse um dado. Sem ele, um corpo 403 se torna zero correspondências de selector e parece idêntico a um conjunto de resultados vazio. A distinção entre essas classes de status está definida em a especificação de semântica HTTP.
Selector::parse é falho porque uma string de seletor é compilada, não interpretada no momento da correspondência. Compilar seletores uma vez fora do loop — em vez de por elemento — é o que mantém as chamadas select aninhadas baratas. A sintaxe segue a especificação do W3C Selectors Level 4.
O operador ? que se propaga para fora de main é o que transforma Box<dyn std::error::Error> em um tipo de retorno prático. Cada chamada falha se desdobra para o mesmo lugar, e o processo sai não-zero em caso de falha — útil quando o scraper é executado a partir de um agendador.
Raspagem de Páginas Concorrentemente
A história de concorrência do Rust é o principal argumento para usá-lo aqui, e tokio::spawn é onde isso aparece. Cada página se torna uma tarefa independente, todas elas compartilham um cliente com conexão em pool, e os resultados são coletados em ordem:
rust
use scraper::{Html, Selector};
async fn page_quote_count(client: &reqwest::Client, page: u32) -> Result<usize, reqwest::Error> {
let url = format!("https://quotes.toscrape.com/page/{page}/");
let body = client.get(&url).send().await?.error_for_status()?.text().await?;
let quote_sel = Selector::parse("div.quote").unwrap();
Ok(Html::parse_document(&body).select("e_sel).count())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let tasks: Vec<_> = (1..=5)
.map(|page| {
let client = client.clone();
tokio::spawn(async move { (page, page_quote_count(&client, page).await) })
})
.collect();
let mut total = 0;
for task in tasks {
let (page, result) = task.await?;
let count = result?;
println!("pagina {page}: {count} citações");
total += count;
}
println!("total de citações em 5 páginas: {total}");
Ok(())
}
A execução:
text
pagina 1: 10 citações
pagina 2: 10 citações
pagina 3: 10 citações
pagina 4: 10 citações
pagina 5: 10 citações
total de citações em 5 páginas: 50
reqwest::Client é barato para clonar porque o clone compartilha o pool de conexões subjacente. Criar um cliente novo por tarefa abriria um novo pool a cada vez e descartaria o benefício. Mantenha o intervalo de páginas limitado e dimensionado para o que o alvo pode servir confortavelmente — a concorrência é uma ferramenta para concluir uma carga de trabalho conhecida, não para emitir o máximo de solicitações simultâneas que o tempo de execução permitir.
Pronto para apontar isso para um alvo que renderiza no lado do servidor? Crie uma conta gratuita no Scrapeless e mantenha o código de parsing que você já tem.
Onde reqwest e scraper Param
Nenhum dos crates executa JavaScript. reqwest retorna os bytes que o servidor enviou, e scraper analisa exatamente esses bytes — portanto, em uma página que constrói seu conteúdo no navegador, os seletores não correspondem a nada.
Isso é mensurável em vez de teórico. O site usado acima publica um gêmeo renderizado pelo cliente dos mesmos dados em /js/. Apontando a lógica de parsing idêntica para ele:
rust
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let body = reqwest::get("https://quotes.toscrape.com/js/")
.await?
.error_for_status()?
.text()
.await?;
let document = Html::parse_document(&body);
let quote_sel = Selector::parse("div.quote").unwrap();
let count = document.select("e_sel).count();
println!("bytes html: {}", body.len());
println!("citações analisadas: {}", count);
Ok(())
}
text
bytes html: 5808
citações analisadas: 0
A solicitação foi bem-sucedida. O status foi 200. O analisador funcionou corretamente em 5,808 bytes de HTML válido que simplesmente não continham nenhum elemento de citação — eles são escritos no DOM após a execução de um script. Um scraper que apenas verifica falhas de HTTP trata isso como uma página vazia ao invés de uma capacidade ausente, razão pela qual a chamada anterior error_for_status() é necessária, mas não suficiente.
Renderize a Página Com a API de Raspagem Universal
A API de Raspagem Universal Scrapeless fecha essa lacuna ao renderizar a página em um navegador na nuvem e retornar o HTML resultante, o que significa que o lado Rust permanece uma chamada HTTP simples. Defina js_render como true e o corpo da resposta contém o DOM pós-script.
Autentique-se com sua chave do ambiente:
bash
export SCRAPELESS_API_KEY="sua_chave_api_aqui"
Então troque apenas a camada de fetch — o código do seletor abaixo é idêntico ao primeiro exemplo:
rust
use scraper::{Html, Selector};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("SCRAPELESS_API_KEY")?;
let payload = json!({
"ator": "unlocker.webunlocker",
"entrada": {
"url": "https://quotes.toscrape.com/js/",
"js_render": true,
"headless": true
}
});
let envelope: serde_json::Value = reqwest::Client::new()
.post("https://api.scrapeless.com/api/v2/unlocker/request")
.header("x-api-token", api_key)
.json(&payload)
.send()
.await?
.error_for_status()?
.json()
.await?;
let body = envelope["data"].as_str().unwrap_or_default();
let document = Html::parse_document(body);
let quote_sel = Selector::parse("div.quote").unwrap();
let text_sel = Selector::parse("span.text").unwrap();
let author_sel = Selector::parse("small.author").unwrap();
let mut count = 0;
for quote in document.select("e_sel) {
let text = quote.select(&text_sel).next().map(|e| e.inner_html());
let author = quote.select(&author_sel).next().map(|e| e.inner_html());
if let (Some(t), Some(a)) = (text, author) {
if count == 0 {
println!("primeira citação: {} — {}", t, a);
}
count += 1;
}
}
println!("bytes html: {}", body.len());
println!("citações analisadas: {}", count);
Ok(())
}
```text
primeira citação: “O mundo como o criamos é um processo do nosso pensamento. Não pode ser mudado sem mudar nosso pensamento.” — Albert Einstein
bytes html: 8985
citações analisadas: 10
Mesma página, mesmos seletores, mesmas versões de crate. A única mudança é qual camada buscou o HTML, e a contagem de registros muda de 0 para 10 enquanto o payload cresce de 5.808 para 8.985 bytes — a diferença é a marcação da citação que o script escreveu no DOM.
A resposta chega como um envelope JSON com o documento renderizado em data como uma string, que é por isso que o exemplo analisa para serde_json::Value primeiro e passa data para Html::parse_document. Detalhes das opções de renderização estão na documentação do Scrapeless, e o mesmo comportamento de js_render é abordado com mais profundidade no guia de renderização JS.
Solução de problemas
Seletores não correspondem a nada em uma página que você pode ver em um navegador. Imprima primeiro o comprimento da resposta, como no exemplo JS acima. Alguns milhares de bytes com zero correspondências geralmente significam que o conteúdo é renderizado no cliente, não que o seletor esteja errado. Veja o código-fonte da página em vez do inspetor — o inspetor mostra o DOM após a execução dos scripts, que não é o que reqwest recebeu.
Selector::parse causa pânico na inicialização. A string do seletor é CSS inválido. Pseudo-elementos e algumas extensões no estilo jQuery não fazem parte da especificação e não compilarão.
A construção falha no backend TLS. reqwest compila uma pilha TLS; em um contêiner mínimo, o sistema precisa de uma toolchain C e CMake disponíveis, ou você pode mudar para o backend rustls puro em Rust através de flags de recurso.
inner_html() retorna marcação em vez de texto. Esse método retorna tudo dentro do elemento, incluindo tags. Use text() e colete os fragmentos quando um campo pode conter elementos aninhados.
Antes de direcionar qualquer um disso para um alvo ao vivo, verifique os termos do site e suas diretivas /robots.txt, que seguem o padrão do Protocolo de Exclusão de Robôs. Mantenha a coleta a dados públicos e a um volume que o alvo possa atender confortavelmente.
Conclusão
Rust oferece a você um scraper que lida com falhas explicitamente e paraleliza sem muita cerimônia — reqwest para transporte, scraper para seletores, tokio para concorrência, e um compilador que não deixará você ignorar um Result. Essa pilha cobre páginas renderizadas no servidor completamente.
O que ela não faz é executar JavaScript, e o custo dessa lacuna é um zero silencioso em vez de um erro. Medir isso é o hábito útil: 10 registros na página renderizada no servidor, 0 na versão renderizada no cliente, 10 novamente uma vez que algo renderize a página antes que Rust a analise.
Comece com o plano gratuito do Scrapeless para executar a etapa de renderização contra seus próprios alvos, e revise a atual preço do Scrapeless quando você dimensionar um trabalho.
FAQ
P: Qual crate Rust é a melhor para web scraping?
reqwest emparelhado com scraper cobre a maior parte do trabalho. reqwest lida com HTTP com uma API baseada em assíncrona, e scraper fornece consultas com seletores CSS sobre uma árvore de análise html5ever. Use uma crate de navegador sem cabeça ou uma API de renderização apenas quando a página constrói seu conteúdo no lado do cliente.
P: Rust é bom para web scraping comparado ao Python?
Rust é uma boa opção quando o scraper é executado por longos períodos, é executado de forma concorrente ou é executado sem supervisão, porque não há pausa de coletor de lixo e o compilador força que cada caminho de erro seja tratado. O Python ainda vence em abrangência de ecossistema e velocidade de iteração para extrações únicas. Os conceitos de análise se transferem diretamente entre eles.
P: O reqwest pode executar JavaScript?
Não. reqwest é um cliente HTTP e retorna os bytes que o servidor enviou. Em uma página renderizada no cliente, isso significa HTML válido, sem nenhum conteúdo — o exemplo acima analisa 5.808 bytes para zero registros. A renderização deve acontecer em outro lugar, seja em um navegador sem cabeça ou através de uma API que retorna o DOM renderizado.
P: Eu preciso de async e tokio para um scraper simples?
Não estritamente — reqwest oferece um cliente bloqueante atrás de uma flag de recurso, o que é bom para uma única requisição sequencial. O cliente assíncrono vale a dependência do tokio assim que você busca mais de uma página, pois é aí que tokio::spawn transforma buscas sequenciais em concorrentes.
P: Como posso evitar que um scraper em Rust quebre quando o site mudar?
Verifique a estrutura em vez de confiar nela. Verifique se o seletor do contêiner corresponde a um número plausível de elementos antes de extrair campos e trate um súbito zero como uma falha em vez de um resultado vazio. Compilar seletores uma vez na inicialização também revela CSS inválido imediatamente em vez de durante a coleta.
Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.



