In this Article
Le web scraping avec Rust consiste à récupérer et analyser des pages Web à l’aide du langage Rust et de son écosystème de crates. Rust attire les développeurs en quête de performances prévisibles et de sûreté mémoire sans garbage collector, ce qui en fait un excellent choix pour les crawlers exécutés en continu ou à grande échelle.
Ce guide pratique, axé sur le code, s’appuie sur le client HTTP reqwest et la crate d’analyse scraper. Il couvre la configuration du projet, la récupération minimale, les sélecteurs CSS, les modèles de données structurés, l’export JSON et CSV, la pagination, la gestion des erreurs avec tentatives et backoff, la concurrence asynchrone avec tokio, la configuration d’un proxy et le recours à un navigateur sans tête pour les sites riches en JavaScript. Chaque extrait est conçu pour être compilé et exécuté.
DataImpulse est un fournisseur de proxys éthiques proposant plus de 90 millions d’adresses IP résidentielles, mobiles et de datacenters dans 195 pays. Il propose un modèle de paiement à l’utilisation à partir de 1 dollar par GB, avec du trafic sans date d’expiration, pour le web scraping, la vérification des annonces, le suivi des prix, les études de marché et la gestion multi-comptes.
Faits clés
- Pile de base : Le web scraping avec Rust associe généralement le client HTTP reqwest à la crate scraper pour l’analyse HTML, ainsi que tokio pour la concurrence asynchrone et serde pour l’export de données structurées.
- Meilleur type de proxy : des proxys résidentiels rotatifs, qui s’appuient sur de véritables adresses IP de particuliers et aident à éviter la détection.
- Prix : à partir de 1 dollar par GB, paiement à l’utilisation, trafic sans expiration et sans abonnement.
- Couverture : plus de 90 millions d’IP d’origine éthique dans 195 pays.
- Fiabilité: Taux de réussite de 99,51 %, noté 4,8 sur 5 sur G2.
- Protocoles et ciblage : HTTP, HTTPS et SOCKS5, avec ciblage par pays inclus.

Pourquoi utiliser Rust pour le web scraping ?
Rust convient au web scraping lorsque la vitesse, une faible consommation mémoire et la fiabilité priment sur le prototypage rapide. Ses binaires compilés s’exécutent près du matériel, et son vérificateur d’emprunts détecte des catégories entières de bugs de concurrence à la compilation, plutôt qu’en production.
Ces propriétés sont rentables dans quelques scénarios courants :
- Débit : Un crawler Rust compilé traite de gros volumes de pages avec moins de surcharge CPU et mémoire qu’un langage interprété, ce qui réduit les coûts d’infrastructure des tâches de longue durée.
- Accès simultané sécurisé : Le runtime asynchrone tokio permet d’exécuter des milliers de requêtes simultanées, tandis que le système de types évite les data races entre ces tâches.
- Utilisation prévisible des ressources : L’absence de garbage collector apporte une latence plus stable, un atout pour les crawlers qui tournent des heures ou des jours.
Le compromis est une courbe d’apprentissage plus raide et des temps de compilation plus longs, c’est pourquoi Rust récompense les projets qui s’exécutent à grande échelle plutôt que les scripts ponctuels. Quel que soit le langage, les mêmes règles de base s’appliquent : respectez robots.txt, limitez votre fréquence de requêtes et suivez les Meilleures pratiques de web scraping pour que votre crawler reste respectueux.
Comment mettre en place un projet de scraping Rust ?
Configurez un projet de scraping Rust en créant un nouveau package Cargo et en déclarant les crates nécessaires dans Cargo.toml. Cargo est l’outil de construction et le gestionnaire de packages de Rust, il résout donc les dépendances et compile le projet pour vous.
Exécutez cargo new rust-scraper pour créer le squelette du projet, puis modifiez le fichier Cargo.toml généré. L’ensemble de dépendances ci-dessous couvre tout dans ce guide : HTTP, analyse, asynchrone, sérialisation, sortie CSV, flux et gestion ergonomique des erreurs.
[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"
Ici, reqwest gère les requêtes (la fonctionnalité socks active les proxys SOCKS5 et json ajoute des helpers JSON), scraper analyse le HTML avec des sélecteurs CSS, tokio fournit le runtime asynchrone, serde et serde_json sérialisent les données, csv écrit les fichiers CSV, futures fournit des combinateurs de flux pour la concurrence, et anyhow avec thiserror simplifie la gestion des erreurs. Exécutez cargo build une fois pour tout télécharger et compiler avant d’écrire votre première requête.
Comment récupérer et analyser une page avec reqwest et scraper ?
Récupérez une page en envoyant une requête GET avec reqwest, puis chargez le corps de la réponse dans scraper pour l’interroger avec les sélecteurs CSS. L’exemple ci-dessous s’exécute de manière asynchrone sur tokio, récupère une page et imprime le texte de chaque élément 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(())
}
Le déroulement est toujours le même : reqwest::get renvoie une réponse, .text() lit son corps comme une chaîne, et Html::parse_document transforme cette chaîne en un arbre interrogeable. L’opérateur ? propage toute erreur réseau ou de décodage jusqu’à main. Appeler .text() sur un élément correspondant récupère ses nœuds de texte, que vous nettoyez ensuite avec trim(). Cette approche basée sur le HTML brut est rapide, mais elle ne voit que le balisage renvoyé par le serveur, et non le contenu qu’un navigateur rendrait ensuite avec JavaScript.
Comment cibler les éléments avec les sélecteurs CSS ?
Ciblez les éléments en transmettant une chaîne de sélection CSS standard à Selector::parse, exactement comme vous le feriez dans une console de navigateur. La crate scraper prend en charge les balises, les classes, les identifiants, les correspondances d’attributs, les chemins descendants et les pseudo-classes structurelles.
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();
Compilez chaque sélecteur une fois et réutilisez-le plutôt que de l’analyser dans une boucle, car l’analyse a un faible coût. Lorsqu’un sélecteur risque de ne pas correspondre, préférez .next() avec .unwrap_or_default() afin qu’un champ manquant ne fasse pas échouer toute l’exécution. L’inspection de la page cible dans le sélecteur d’éléments de votre navigateur est le moyen le plus rapide de trouver des noms de classe stables et des attributs sur lesquels sélectionner.
Comment exporter des données récupérées vers JSON et CSV ?
Exportez les données récupérées en modélisant chaque enregistrement sous la forme d’une structure qui dérive Serialize de serde, puis écrivez une tranche de ces structures dans JSON avec serde_json ou dans CSV avec la crate csv. La définition d’un modèle typé maintient l’extraction et la sortie synchronisées et rend le code auto-documenté.
Commencez par modéliser l’enregistrement et extraire les champs de chaque élément conteneur :
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
}
Comme Product dérive Serialize, la même structure alimente les deux exportateurs sans travail supplémentaire :
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 produit un JSON lisible, et serialize de la crate csv écrit chaque structure sous forme de ligne, avec un en-tête dérivé des noms de champs. La réutilisation d’un modèle typé pour le scraping, JSON et CSV, est ce qui rend un pipeline Rust facile à entretenir à mesure que le site cible change.
Comment scraper plusieurs pages avec une boucle de pagination ?
Scrapez plusieurs pages en parcourant les numéros de page ou en suivant l’URL de la page suivante, tout en réutilisant un unique client reqwest afin de mutualiser les connexions entre les requêtes. Construisez chaque URL, récupérez-la, analysez-la avec la fonction de la section précédente, puis arrêtez-vous lorsqu’une page ne renvoie plus aucun résultat.
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(())
}
Créer le client une seule fois avec reqwest::Client::new(), hors de la boucle, lui permet de réutiliser les connexions TCP, ce qui est nettement plus rapide que de créer un client pour chaque requête. La vérification d’un résultat vide termine naturellement la boucle lorsque vous atteignez la dernière page, et de courtes pauses entre les requêtes évitent de surcharger le serveur. Pour les sites qui utilisent un lien suivant au lieu de pages numérotées, extrayez le href de l’ancre de la page suivante à chaque itération et suivez-le jusqu’à ce que le lien disparaisse.
Comment gérer les erreurs et les tentatives avec backoff ?
Gérez les erreurs en renvoyant une erreur tapée à partir de vos fonctions de récupération, puis réessayez les échecs transitoires avec un backoff exponentiel. Les réseaux expirent et les serveurs renvoient des codes d’état temporaires, de sorte qu’un scraper robuste traite une seule panne comme attendue plutôt que comme fatale.
La crate thiserror permet de définir une énumération d’erreur compacte qui se convertit automatiquement à partir d’une erreur 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?)
}
L’attribut #[from] permet de convertir automatiquement une erreur reqwest en ScrapeError::Request via l’opérateur ?. En complément des erreurs typées, placez la requête dans une boucle de tentatives dont l’attente augmente après chaque essai :
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")
}
Le backoff exponentiel (0,5 s, 1 s, 2 s, 4 s, 8 s ici) laisse à un serveur soumis à une limitation de débit ou brièvement surchargé le temps de récupérer au lieu de le marteler. La combinaison de tentatives avec des proxys rotatifs est encore plus efficace : une requête bloquée ou limitée sur une IP peut réussir à la tentative suivante depuis une IP de sortie différente.
Comment exécuter des requêtes simultanées avec tokio ?
Exécutez des requêtes simultanées en transformant votre liste d’URL en un flux asynchrone et en appliquant buffer_unordered, qui limite le nombre de requêtes en cours en même temps. C’est le principal levier de performance d’un scraper Rust, et tokio comme la crate futures le rendent sûr.
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(())
}
Ici stream::iter convertit la liste d’URL en flux, .map démarre une requête asynchrone par URL, et buffer_unordered(10) en maintient au plus dix en cours d’exécution simultanément, et renvoie les résultats dès qu’ils sont prêts. Le clonage du client par tâche est bon marché car un reqwest Client est un Arc autour d’un pool de connexions partagé. Ajustez la limite de concurrence pour équilibrer la vitesse par rapport à la charge que vous placez sur la cible : dix à vingt est un point de départ raisonnable, et l’association d’une concurrence plus élevée avec un pool de proxy répartit ces requêtes sur de nombreuses IP.
Comment acheminer le trafic de scraping Rust via un proxy ?
Acheminez le trafic via un proxy en créant un client reqwest avec reqwest::Proxy et en attachant votre passerelle et vos informations d’identification avec .basic_auth. L’envoi de requêtes via des IP rotatives répartit la charge, permet d’accéder à du contenu géolocalisé et réduit le risque de limitation de débit. La rotation des IP est l’une des techniques les plus efficaces pour gratter sans se bloquer.
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(())
}
Remplacez login et password par vos identifiants, puis ajustez le port selon le réseau souhaité. Si le proxy rejette votre demande avec un statut 407, la cause est presque toujours des informations d’identification manquantes ou erronées ; le guide sur Erreur HTTP 407 ainsi que le guide pas à pas sur authentification par proxy expliquent comment résoudre le problème. DataImpulse fournit proxys résidentiels, proxys de centre de données, et proxys mobiles sur HTTP, HTTPS et SOCKS5, avec des sessions rotatives et collantes. Le ciblage par pays est inclus ; le ciblage par État, ville, code postal et ASN est proposé en option payante.
Comment scraper les pages riches en JavaScript, et quand Rust est-il le bon choix ?
Scrapez les pages riches en JavaScript en pilotant un véritable navigateur sans tête, car reqwest et scraper ne voient que le HTML brut du serveur et n’exécutent jamais de scripts. Lorsque le contenu apparaît uniquement après le rendu côté client, utilisez une crate d’automatisation de navigateur telle que thirtyfour, qui parle le protocole WebDriver, ou chromiumoxide, qui contrôle Chrome via 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(())
}
L’exemple thirtyfour se connecte à un chromedriver en cours d’exécution, charge une page, attend un élément rendu et lit son texte. Cette approche est bien plus lourde qu’une simple récupération HTTP : utilisez-la donc uniquement pour les pages qui en ont vraiment besoin et conservez le chemin rapide reqwest pour tout le reste. Pour approfondir ce compromis, consultez les guides sur scraper des pages Web dynamiques et web scraping avec JavaScript. Le tableau ci-dessous résume la manière dont les caisses principales s’assemblent :
| Caisse | Rôle | Exécute JavaScript | Idéal pour |
|---|---|---|---|
| reqwest | Client HTTP | Non | Récupération des API brutes HTML et JSON |
| scraper | Analyseur HTML | Non | Extraction de données avec des sélecteurs CSS |
| thirtyfour | Client WebDriver | Oui | Automatisation complète du navigateur et formulaires |
| chromiumoxide | DevTools Protocol | Oui | Contrôle rapide de Chrome sans tête |
En ce qui concerne le choix du langage, choisissez Rust lorsque vous avez besoin de performances soutenues et d’une faible utilisation des ressources à grande échelle, et choisissez Python lorsque vous appréciez un développement rapide et un écosystème de scraping plus large tel que Scrapy et Playwright. Un modèle courant consiste à prototyper les règles d’extraction en Python, puis à porter les parties critiques vers Rust une fois que la logique est stable et que le volume augmente. Quel que soit votre choix, la couche réseau compte autant que l’analyseur, et les proxys d’origine éthique contribuent à la fiabilité des tâches de grande ampleur.

Questions fréquemment posées
Quelles crates Rust sont les plus adaptées au web scraping ?
L’association la plus courante est reqwest pour les requêtes HTTP et scraper pour l’analyse HTML avec des sélecteurs CSS. Ajoutez tokio pour la concurrence asynchrone, serde pour l’exportation de données et une crate d’automatisation de navigateur comme thirtyfour ou chromiumoxide lorsqu’un site nécessite un rendu JavaScript.
Rust peut-il scraper des pages rendues en JavaScript ?
Pas avec reqwest et scraper seuls, car ils ne voient que le HTML brut renvoyé par le serveur et ne peuvent pas exécuter de scripts. Pour récupérer le contenu rendu par le client, pilotez un navigateur sans tête via une crate WebDriver telle que thirtyfour ou une crate DevTools Protocol telle que chromiumoxide.
Comment exporter des données récupérées dans Rust ?
Modélisez chaque enregistrement comme une structure qui dérive Serialize de serde, puis écrivez une tranche de ces structures dans JSON avec serde_json ou dans CSV avec la crate csv. Un modèle typé peut alimenter les deux formats sans dupliquer la logique d’extraction.
Comment exécuter des requêtes simultanées sans surcharger un site ?
Transformez vos URL en flux asynchrone et appliquez buffer_unordered avec une limite fixe, qui limite le nombre de requêtes exécutées en même temps. Démarrez environ dix à vingt requêtes simultanées et associez une concurrence plus élevée à un pool de proxys rotatifs pour répartir la charge sur de nombreuses IP.
Ai-je besoin d’un proxy pour le web scraping Rust ?
Pas pour les petites tâches, mais les proxys deviennent importants à grande échelle pour distribuer les requêtes et accéder à du contenu géo-spécifique. Configurez-en un avec reqwest::Proxy et basic_auth, et utilisez la rotation d’IP résidentielles ou mobiles pour réduire les limitations de débit et les blocages.
Quand DataImpulse n’est-il pas la bonne solution ?
Si vous avez besoin de proxys FAI statiques, d’une API de scraping entièrement gérée ou d’un accès aux sites bancaires et gouvernementaux, DataImpulse n’est pas le bon outil. Il se concentre sur la rotation des proxys résidentiels, mobiles et des centres de données pour collecter des données publiques et accéder au contenu.
Scrapez de manière fiable avec des proxys éthiques
Une fois que votre scraper Rust récupère, analyse et exporte correctement les données, un réseau de proxys fiable lui permet de fonctionner à grande échelle. DataImpulse propose des IPs d’origine éthique, à partir de 1 dollar par GB, avec un trafic sans expiration, afin que vous puissiez créer un compte et acheminer vos premières requêtes en quelques minutes.
