Tout parseur qui fonctionne plus de cinq minutes sur des sites réels finit par rencontrer un blocage par IP.
Rust avec la bibliothèque reqwest
offre d'excellentes performances, mais ne résout pas le problème de la rotation des proxies par défaut — il faut le mettre en œuvre soi-même.
Dans cet article, nous allons voir comment connecter des proxies SOCKS5 et HTTP(S), constituer un pool de clients et organiser la rotation des IP de manière à ce que le parseur ne tombe pas après les premières réponses 429 et 403.
Pourquoi un proxy est-il nécessaire pour un parseur et quels sont les risques de son absence
Des sites comme les marketplaces, les agrégateurs de prix et les réseaux sociaux surveillent l'activité anormale d'une seule IP : des dizaines de requêtes par seconde, des User-Agent similaires, l'absence de cookies entre les requêtes. Dès que le compteur d'actions suspectes dépasse un certain seuil, l'IP est ajoutée à une liste de blocage — temporaire ou permanente. Pour un parseur, cela signifie un arrêt complet de la collecte de données, jusqu'à ce que l'IP soit changée manuellement.
Les proxies résolvent ce problème en répartissant les requêtes entre des dizaines ou des centaines d'adresses IP. Il est important de ne pas simplement "brancher un proxy", mais d'organiser correctement le pool de connexions et la rotation, sinon vous vous heurterez soit aux limites de reqwest lui-même (un client = un pool de connexions), soit vous commencerez à envoyer des requêtes depuis le même proxy trop souvent. Nous allons examiner ces deux problèmes un par un : d'abord la connexion de base, puis l'architecture du pool.
Pour un parseur en production, non seulement la rotation des IP est importante, mais aussi la qualité des adresses elles-mêmes. Si vous parsez Wildberries, Ozon ou Avito, les IP de centres de données sont bloquées plus rapidement que les IP d'utilisateurs réels — nous en parlerons plus en détail dans la section sur le choix du type de proxy.
Configuration de base des proxies HTTP/HTTPS dans reqwest
reqwest prend en charge les proxies via la structure Proxy,
qui doit être transmise à ClientBuilder.
Ajoutez les dépendances dans Cargo.toml :
[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }
Un exemple minimal avec un proxy HTTP nécessitant une authentification par nom d'utilisateur et mot de passe :
use reqwest::{Client, Proxy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let proxy = Proxy::all("http://gate.proxycove.com:8000")?
.basic_auth("user_login", "user_password");
let client = Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(15))
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?
.text()
.await?;
println!("Réponse via le proxy : {resp}");
Ok(())
}
La méthode Proxy::all
dirige le trafic HTTP et HTTPS via le proxy. Si vous devez séparer les protocoles, utilisez
Proxy::http() et
Proxy::https()
séparément — c'est utile si le fournisseur de proxy écoute différents ports pour différents protocoles.
Connexion d'un proxy SOCKS5
SOCKS5 fonctionne à un niveau plus bas que les proxies HTTP et prend en charge les connexions TCP sans examiner
le contenu de la requête — c'est plus rapide et plus stable pour le parsing, surtout si vous accédez non seulement à HTTP(S),
mais aussi à des points de terminaison WebSocket. Pour SOCKS5, assurez-vous d'activer la fonctionnalité socks
dans Cargo.toml (déjà mentionné ci-dessus). Exemple de connexion :
use reqwest::{Client, Proxy};
async fn build_socks5_client(
host: &str,
login: &str,
password: &str,
) -> reqwest::Result<Client> {
let proxy_url = format!("socks5h://{host}");
let proxy = Proxy::all(&proxy_url)?.basic_auth(login, password);
Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()
}
Notez le schéma socks5h
au lieu de socks5.
La lettre "h" à la fin signifie que la résolution DNS du domaine se fait du côté du serveur proxy, et non localement.
Cela est critique pour l'anonymat : sans cela, votre véritable résolveur DNS peut "fuir" les domaines demandés en dehors du proxy,
même si le trafic lui-même passe par le tunnel SOCKS5.
Si le fournisseur de proxy ne nécessite pas d'authentification par nom d'utilisateur/mot de passe (whitelisting IP), il suffit de ne pas appeler
basic_auth —
indiquez dans le panneau du fournisseur l'adresse IP de votre serveur à partir de laquelle les requêtes sont envoyées.
Pool de clients reqwest pour différents proxies
Une erreur fréquente consiste à créer un nouveau Client
pour chaque requête. C'est coûteux : reqwest maintient un pool de connexions TCP et de sessions TLS, et la recréation d'un client
annule tous les avantages du keep-alive. L'approche correcte consiste à créer un client pour chaque proxy à l'avance
et à les réutiliser à partir d'un pool commun.
use reqwest::{Client, Proxy};
use std::sync::Arc;
#[derive(Clone)]
struct ProxyEndpoint {
url: String,
login: String,
password: String,
}
struct ClientPool {
clients: Vec<Client>,
}
impl ClientPool {
fn build(endpoints: &[ProxyEndpoint]) -> reqwest::Result<Self> {
let mut clients = Vec::with_capacity(endpoints.len());
for ep in endpoints {
let proxy = Proxy::all(&ep.url)?
.basic_auth(&ep.login, &ep.password);
let client = Client::builder()
.proxy(proxy)
.pool_max_idle_per_host(4)
.timeout(std::time::Duration::from_secs(15))
.build()?;
clients.push(client);
}
Ok(Self { clients })
}
}
Ce pool est créé une seule fois au démarrage du parseur. Le nombre de clients est généralement égal au nombre d'adresses proxy uniques dans votre pool — si le fournisseur donne accès à la rotation au niveau de la passerelle (gateway), un ou deux clients suffisent, car la rotation des IP se fait automatiquement du côté du fournisseur à chaque nouvelle connexion TCP.
Pour des tâches à haute parallélisme, enveloppez le pool dans Arc
et partagez-le entre les tâches tokio — les clients reqwest sont thread-safe et conçus pour une utilisation concurrente.
Stratégies de rotation IP
Il existe deux approches fondamentalement différentes pour la rotation. La première est la rotation du côté du fournisseur : vous vous connectez à une seule adresse de passerelle, et le fournisseur remplace automatiquement l'IP à chaque requête ou toutes les N minutes. La seconde — la rotation de votre côté : vous maintenez une liste de proxies statiques et choisissez vous-même lequel utiliser pour la prochaine requête.
Si vous utilisez des proxies résidentiels avec rotation automatique via la passerelle, le code se simplifie au minimum — vous envoyez simplement des requêtes via le même client, le fournisseur change l'IP sortante lui-même. Mais si vous avez besoin de contrôle (par exemple, lier une IP à une session pendant 10 minutes pour simuler le comportement d'un utilisateur réel), mettez en œuvre la rotation manuellement :
use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RotatingPool {
clients: Vec<Client>,
counter: AtomicUsize,
}
impl RotatingPool {
// Round-robin : nous parcourons les clients à tour de rôle
fn next_round_robin(&self) -> &Client {
let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
&self.clients[idx]
}
// Choix aléatoire — utile contre l'analyse de motifs
fn next_random(&self) -> &Client {
self.clients.choose(&mut rand::thread_rng()).unwrap()
}
}
Le round-robin convient lorsque la charge doit être uniformément répartie sur toutes les IP. Le choix aléatoire masque mieux le motif des requêtes, si le site cible analyse les intervalles entre les appels d'une même adresse. Pour des tâches comme la surveillance des prix sur Ozon ou la collecte d'annonces sur Avito, une combinaison est souvent plus efficace : choix aléatoire de proxy plus un délai aléatoire de 1 à 3 secondes entre les requêtes d'un même client.
Il convient de mentionner séparément les sessions sticky. Si le parseur suit un scénario multi-étapes (connexion → navigation entre les pages → action), l'IP ne doit pas changer au milieu de la session — le site remarquera le changement d'IP lors d'une session de cookie active et soit déconnectera, soit bloquera le compte. Pour de tels scénarios, associez un client (et donc une IP) à une "session" spécifique du parseur pendant toute sa durée de vie.
Gestion des erreurs et logique de retry
Les proxies tombent parfois : délais d'attente, 407 (Proxy Authentication Required), rupture de la connexion TCP. Le parseur doit être capable de distinguer "le proxy est cassé — exclure du pool" de "le site a renvoyé 429 — il suffit d'attendre et de changer d'IP". Voici un modèle de travail avec retry et exclusion des proxies morts :
use reqwest::StatusCode;
use std::time::Duration;
async fn fetch_with_retry(
pool: &RotatingPool,
url: &str,
max_attempts: u32,
) -> Result<String, String> {
let mut last_error = String::new();
for attempt in 0..max_attempts {
let client = pool.next_random();
match client.get(url).send().await {
Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
last_error = "429 : limite de taux, changement d'IP".into();
tokio::time::sleep(Duration::from_millis(500 * (attempt as u64 + 1))).await;
continue;
}
Ok(resp) if resp.status().is_success() => {
return resp.text().await.map_err(|e| e.to_string());
}
Ok(resp) => {
last_error = format!("Statut inattendu : {}", resp.status());
}
Err(e) if e.is_timeout() || e.is_connect() => {
last_error = format!("Problème avec le proxy : {e}");
continue;
}
Err(e) => {
last_error = e.to_string();
}
}
}
Err(format!("Toutes les tentatives épuisées : {last_error}"))
}
Un détail important : le délai exponentiel (500 * attempt)
réduit la charge sur le site cible lors de réponses 429 massives et diminue simultanément le risque de blocage complet
de tout le pool de proxies. Pour les parseurs en production, ajoutez un compteur d'erreurs pour chaque proxy dans le pool et excluez automatiquement
l'adresse si elle génère 5+ erreurs consécutives — cela évitera des tentatives inutiles via une IP déjà bloquée.
Quel type de proxy choisir pour le parsing
Le choix du type de proxy influence directement le nombre de requêtes réussies que vous obtiendrez avant le blocage. Les IP de centres de données sont les moins chères et les plus rapides, mais appartiennent à des plages connues de fournisseurs d'hébergement — les systèmes anti-bot des marketplaces et des réseaux sociaux les identifient presque instantanément via les bases ASN. Les IP résidentielles et mobiles ressemblent à des utilisateurs ordinaires, donc elles durent beaucoup plus longtemps.
| Type de proxy | Vitesse | Risque de blocage | Quand appliquer |
|---|---|---|---|
| Centre de données | Élevée | Élevé | APIs ouvertes, sites sans anti-bot |
| Résidentiel | Moyenne | Faible | Wildberries, Ozon, Avito, réseaux sociaux |
| Mobile | Moyenne | Minime | Instagram, TikTok, anti-bots strictes |
Pour le parsing de marketplaces à grande échelle, un équilibre raisonnable est d'utiliser des proxies résidentiels : ils ne sont pas aussi chers en termes de trafic que les mobiles, mais passent les vérifications au niveau "utilisateur réel", qui bloquent les plages de centres de données. Si le parseur fonctionne via les APIs des réseaux sociaux ou simule une application mobile, les proxies mobiles offrent le plus bas pourcentage de captchas et de blocages temporaires grâce aux IP, qui sont liées aux opérateurs de télécommunications.
Performance et conseils de charge
Rust permet de maintenir des milliers de requêtes concurrentes sur une seule instance, mais le fournisseur de proxy et le site cible limitent tout de même la bande passante réelle. Voici quelques règles pratiques pour un parseur en production :
- Limitez le parallélisme via
tokio::sync::Semaphore— pas plus de 20-50 requêtes simultanées sur un seul pool de proxies, sinon certaines connexions commenceront à se rompre par timeout. - Utilisez
pool_idle_timeoutdans ClientBuilder, pour que reqwest ne garde pas de connexions mortes via des proxies instables. - Enregistrez les statuts HTTP et le temps de réponse pour chaque proxy séparément — ainsi vous verrez rapidement quelles adresses commencent à "pourrir" et doivent être remplacées.
- La rotation des User-Agent avec la rotation des IP réduit considérablement la probabilité de détection : changez-les de manière synchrone, et non en cycles séparés.
- Pour les parseurs à long terme, ajoutez un contrôle de santé : toutes les quelques minutes, envoyez une requête de test à https://httpbin.org/ip via chaque proxy et excluez ceux qui ne répondent pas.
Accordez une attention particulière aux empreintes TLS (JA3/JA3S). reqwest utilise par défaut rustls ou nativetls
selon les fonctionnalités de compilation, et les deux ont des empreintes différentes pour la poignée de main TLS. Certains systèmes anti-bot (par exemple,
sur de grandes marketplaces) analysent précisément l'empreinte TLS, et non seulement l'IP — donc même un proxy idéal
ne sauvera pas si le client lui-même se présente avec une poignée de main TLS non standard. Si vous rencontrez des blocages avec
des proxies "propres", vérifiez les fonctionnalités de reqwest et essayez la fonctionnalité native-tls au lieu de rustls, ou vice versa.
Conclusion
Les proxies dans Rust reqwest se configurent rapidement — quelques lignes avec Proxy::all
ou le schéma SOCKS5 socks5h://.
Le véritable travail commence ensuite : construire un pool de clients, choisir une stratégie de rotation (round-robin, aléatoire
ou sessions sticky) et gérer correctement les erreurs en excluant les adresses non fonctionnelles du pool. Sans cela, même
le parseur le plus rapide sur Rust se heurtera à des blocages après quelques minutes de fonctionnement.
Si votre parseur travaille avec des marketplaces ou des réseaux sociaux, où la protection anti-bot analyse non seulement la fréquence des requêtes, mais aussi l'origine des IP, commencez par des proxies résidentiels — ils offrent le meilleur équilibre entre prix et durabilité des sessions. Pour des objectifs plus stricts comme Instagram et TikTok, il vaut la peine de tester des IP mobiles, tandis que pour des tâches simples sans anti-bot, des proxies de centres de données rapides et peu coûteux suffisent largement.