Qualsiasi parser che funziona per più di cinque minuti su siti reali, prima o poi, si scontra con il blocco per IP.
Rust con la libreria reqwest
offre ottime prestazioni, ma non risolve il problema della rotazione dei proxy "out of the box" — è necessario implementarlo da soli.
In questo articolo vedremo come collegare proxy SOCKS5 e HTTP(S), costruire un pool di client e organizzare la rotazione IP in modo che
il parser non si blocchi dopo le prime risposte 429 e 403.
Perché un parser ha bisogno di un proxy e quali sono i rischi della sua assenza
Siti come marketplace, aggregatori di prezzi e social media monitorano attività anomale da un singolo IP: decine di richieste al secondo, User-Agent simili, assenza di cookies tra le richieste. Non appena il contatore delle azioni sospette supera la soglia, l'IP finisce nella blacklist — temporanea o permanente. Per un parser questo significa una completa interruzione della raccolta dati, finché l'IP non viene cambiato manualmente.
I proxy risolvono questo problema, distribuendo le richieste tra decine o centinaia di indirizzi IP. È importante non solo "inserire un proxy", ma organizzare correttamente un pool di connessioni e la rotazione, altrimenti ci si scontra con i limiti di reqwest stesso (un client = un pool di connessioni), oppure si iniziano a inviare richieste dallo stesso proxy troppo frequentemente. Vedremo entrambe le problematiche a turno: prima il collegamento di base, poi l'architettura del pool.
Per un parser in produzione non è solo importante la rotazione IP, ma anche la qualità degli indirizzi stessi. Se stai facendo scraping su Wildberries, Ozon o Avito, gli IP dei data center vengono bloccati più rapidamente rispetto agli IP di utenti reali — di questo parleremo più dettagliatamente nella sezione sulla scelta del tipo di proxy.
Configurazione di base del proxy HTTP/HTTPS in reqwest
reqwest supporta i proxy tramite la struttura Proxy,
che deve essere passata a ClientBuilder.
Aggiungi le dipendenze in Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }
Un esempio minimo con un proxy HTTP che richiede autenticazione con nome utente e password:
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!("Risposta tramite proxy: {resp}");
Ok(())
}
Il metodo Proxy::all
instrada sia il traffico HTTP che HTTPS attraverso il proxy. Se è necessario separare i protocolli, utilizzare
Proxy::http() e
Proxy::https()
separatamente — questo è utile se il provider del proxy ascolta porte diverse per protocolli diversi.
Collegamento di un proxy SOCKS5
SOCKS5 funziona a un livello più basso rispetto ai proxy HTTP e supporta connessioni TCP senza analizzare
il contenuto della richiesta — questo è più veloce e stabile per il parsing, soprattutto se non ti limiti solo a HTTP(S),
ma interagisci anche con endpoint WebSocket. Per SOCKS5 è necessario abilitare la feature socks
in Cargo.toml (già indicato sopra). Ecco un esempio di collegamento:
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()
}
Fai attenzione allo schema socks5h
invece di socks5.
La lettera "h" alla fine indica che la risoluzione DNS del dominio avviene sul server proxy, non localmente.
Questo è critico per l'anonimato: senza di essa, il tuo vero risolutore DNS può "rivelare" i domini richiesti bypassando il proxy,
anche se il traffico stesso passa attraverso il tunnel SOCKS5.
Se il provider del proxy non richiede autenticazione con nome utente/password (whitelisting IP), semplicemente non chiamare
basic_auth —
specifica nel pannello del provider l'indirizzo IP del tuo server da cui provengono le richieste.
Pool di client reqwest per diversi proxy
Un errore comune è creare un nuovo Client
per ogni richiesta. Questo è costoso: reqwest mantiene internamente un pool di connessioni TCP e sessioni TLS, e ricreare il client
annulla tutti i vantaggi del keep-alive. L'approccio corretto è creare un client per ogni proxy in anticipo
e riutilizzarli da un pool comune.
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 })
}
}
Questo pool viene creato una sola volta all'avvio del parser. Il numero di client è solitamente pari al numero di indirizzi proxy unici nel tuo pool — se il provider offre accesso alla rotazione a livello di gateway, è sufficiente uno o due client, poiché la rotazione IP avviene automaticamente da parte del provider ad ogni nuova connessione TCP.
Per compiti con alta parallelizzazione, avvolgi il pool in Arc
e condividilo tra i task di tokio — i client reqwest sono thread-safe e progettati per l'uso concorrente.
Strategie di rotazione IP
Ci sono due approcci fondamentalmente diversi alla rotazione. Il primo è la rotazione lato provider: ti connetti a un unico indirizzo gateway, e il provider sostituisce automaticamente l'IP per ogni richiesta o ogni N minuti. Il secondo — rotazione lato tuo: mantieni un elenco di proxy statici e scegli tu stesso quale utilizzare per la prossima richiesta.
Se utilizzi proxy residenziali con rotazione automatica tramite gateway, il codice si semplifica al minimo — basta inviare richieste tramite lo stesso client, il provider cambia l'IP in uscita da solo. Ma se hai bisogno di controllo (ad esempio, mantenere l'IP per una sessione di 10 minuti per simulare il comportamento di un utente reale), implementa la rotazione manualmente:
use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RotatingPool {
clients: Vec<Client>,
counter: AtomicUsize,
}
impl RotatingPool {
// Round-robin: cicliamo i client uno dopo l'altro
fn next_round_robin(&self) -> &Client {
let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
&self.clients[idx]
}
// Scelta casuale — utile contro l'analisi dei pattern
fn next_random(&self) -> &Client {
self.clients.choose(&mut rand::thread_rng()).unwrap()
}
}
Il round-robin è adatto quando è importante un carico uniforme su tutti gli IP. La scelta casuale maschera meglio il pattern delle richieste, se il sito target analizza gli intervalli tra le richieste da un singolo indirizzo. Per compiti come il monitoraggio dei prezzi su Ozon o la raccolta di annunci da Avito, nella pratica è più efficace una combinazione: scelta casuale del proxy più un ritardo casuale di 1–3 secondi tra le richieste dello stesso client.
Vale la pena menzionare le sessioni sticky. Se il parser segue uno scenario multi-step (login → navigazione tra pagine → azione), non è possibile cambiare l'IP a metà sessione — il sito noterà il cambio di IP durante una sessione cookie attiva e potrebbe disconnettere o bloccare l'account. Per tali scenari, assegna un client (e di conseguenza un IP) a una specifica "sessione" del parser per tutta la sua durata.
Gestione degli errori e logica di retry
I proxy ogni tanto smettono di funzionare: timeout, 407 (Proxy Authentication Required), interruzione della connessione TCP. Il parser deve essere in grado di distinguere "proxy rotto — escludere dal pool" da "il sito ha restituito 429 — basta aspettare e cambiare IP". Ecco un pattern funzionante con retry ed esclusione dei proxy morti:
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 di frequenza, cambiando 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!("Stato inaspettato: {}", resp.status());
}
Err(e) if e.is_timeout() || e.is_connect() => {
last_error = format!("Problema con il proxy: {e}");
continue;
}
Err(e) => {
last_error = e.to_string();
}
}
}
Err(format!("Tutti i tentativi esauriti: {last_error}"))
}
Un punto importante: il ritardo esponenziale (500 * attempt)
riduce il carico sul sito target durante le risposte 429 di massa e allo stesso tempo diminuisce il rischio di un blocco totale
di tutto il pool di proxy. Per i parser in produzione, aggiungi un contatore di errori per ogni proxy nel pool e escludi automaticamente
l'indirizzo se genera 5+ errori consecutivi — questo proteggerà da tentativi inutili tramite un IP già bloccato.
Quale tipo di proxy scegliere per il parsing
La scelta del tipo di proxy influisce direttamente su quanti richieste di successo riceverai prima del blocco. Gli IP dei data center sono i più economici e veloci, ma appartengono a noti intervalli di provider di hosting — i sistemi anti-bot dei marketplace e dei social media li identificano quasi istantaneamente tramite database ASN. Gli IP residenziali e mobili sembrano utenti normali, quindi durano molto più a lungo.
| Tipo di proxy | Velocità | Rischio di blocco | Quando applicare |
|---|---|---|---|
| Data center | Alta | Alta | API aperti, siti senza anti-bot |
| Residenziali | Media | Basso | Wildberries, Ozon, Avito, social media |
| Mobili | Media | Minimo | Instagram, TikTok, severi anti-bot |
Per il parsing dei marketplace su larga scala, un equilibrio ragionevole è rappresentato dai proxy residenziali: non sono così costosi in relazione al traffico come i mobili, ma superano i controlli a livello di "utente reale", che bloccano gli intervalli dei data center. Se il parser lavora tramite API dei social media o simula un'app mobile, i proxy mobili offrono la percentuale più bassa di captcha e ban temporanei grazie agli IP assegnati agli operatori di rete.
Prestazioni e consigli sul carico
Rust consente di gestire migliaia di richieste concorrenti su un'unica istanza, ma il provider di proxy e il sito target limitano comunque la reale capacità di throughput. Alcune regole pratiche per un parser in produzione:
- Limita il parallelismo tramite
tokio::sync::Semaphore— non più di 20-50 richieste simultanee su un singolo pool di proxy, altrimenti alcune connessioni inizieranno a interrompersi per timeout. - Utilizza
pool_idle_timeoutin ClientBuilder, affinché reqwest non mantenga connessioni morte tramite proxy instabili. - Registra gli stati HTTP e i tempi di risposta per ogni proxy separatamente — in questo modo vedrai rapidamente quali indirizzi hanno iniziato a "degradarsi" e necessitano di sostituzione.
- La rotazione di User-Agent insieme alla rotazione IP riduce significativamente la probabilità di rilevamento: cambiali in modo sincrono, non in cicli separati.
- Per parser a lungo termine, aggiungi un controllo di salute: ogni pochi minuti invia una richiesta di test a https://httpbin.org/ip tramite ogni proxy ed escludi quelli che non rispondono.
Presta particolare attenzione alle impronte TLS (JA3/JA3S). reqwest utilizza per impostazione predefinita rustls o nativetls
a seconda delle funzionalità di compilazione, e entrambi hanno impronte diverse per il handshake TLS. Alcuni sistemi anti-bot (ad esempio,
nei grandi marketplace) analizzano proprio l'impronta TLS, non solo l'IP — quindi anche il proxy migliore non ti salverà se il client stesso si presenta con un handshake TLS non standard. Se ti trovi di fronte a blocchi con
proxy "puliti", controlla le funzionalità di reqwest e prova la funzionalità native-tls invece di rustls, o viceversa.
Conclusione
I proxy in Rust reqwest si configurano rapidamente — poche righe con Proxy::all
o lo schema SOCKS5 socks5h://.
Il lavoro principale inizia dopo: costruzione del pool di client, scelta della strategia di rotazione (round-robin, casuale
o sessioni sticky) e corretta gestione degli errori con esclusione degli indirizzi non funzionanti dal pool. Senza questo, anche
il parser più veloce in Rust si scontrerà con blocchi dopo pochi minuti di funzionamento.
Se il tuo parser lavora con marketplace o social media, dove la protezione anti-bot analizza non solo la frequenza delle richieste, ma anche l'origine dell'IP, inizia con proxy residenziali — offrono il miglior equilibrio tra prezzo e durata delle sessioni. Per obiettivi più severi come Instagram e TikTok, vale la pena testare IP mobili, mentre per compiti semplici senza anti-bot, bastano proxy veloci e economici dei data center.