Cualquier parser que funcione más de cinco minutos en sitios reales, tarde o temprano se enfrenta a bloqueos por IP.
Rust con la biblioteca reqwest
ofrece un excelente rendimiento, pero no resuelve el problema de la rotación de proxies de forma predeterminada: esto debe implementarse manualmente.
En este artículo, analizaremos cómo conectar proxies SOCKS5 y HTTP(S), construir un grupo de clientes y organizar la rotación de IP de tal manera
que el parser no se detenga después de las primeras respuestas 429 y 403.
¿Por qué necesita un parser un proxy y cuáles son los riesgos de no tenerlo?
Sitios como marketplaces, agregadores de precios y redes sociales rastrean actividades anormales desde una sola IP: decenas de solicitudes por segundo, User-Agent idénticos, ausencia de cookies entre solicitudes. Tan pronto como el contador de acciones sospechosas supera un umbral, la IP entra en una lista negra: temporal o permanente. Para el parser, esto significa una parada completa en la recolección de datos, hasta que la IP se cambie manualmente.
Los proxies resuelven este problema, distribuyendo solicitudes entre decenas o cientos de direcciones IP. Es importante no solo "sustituir el proxy", sino organizar correctamente el grupo de conexiones y la rotación, de lo contrario, o bien se encontrará con los límites de reqwest (un cliente = un grupo de conexiones), o comenzará a enviar solicitudes desde el mismo proxy demasiado a menudo. A continuación, analizaremos ambos problemas por separado: primero la conexión básica, luego la arquitectura del grupo.
Para un parser de producción, no solo es importante la rotación de IP, sino también la calidad de las direcciones mismas. Si está parseando Wildberries, Ozon o Avito, las IP de centros de datos se bloquean más rápido que las IP de usuarios reales: más sobre esto en la sección sobre la elección del tipo de proxy.
Configuración básica de proxy HTTP/HTTPS en reqwest
reqwest admite proxies a través de la estructura Proxy,
que debe pasarse a ClientBuilder.
Agregue las dependencias en Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }
Un ejemplo mínimo con un proxy HTTP que requiere autenticación por nombre de usuario y contraseña:
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!("Respuesta a través del proxy: {resp}");
Ok(())
}
El método Proxy::all
dirige tanto el tráfico HTTP como el HTTPS a través del proxy. Si necesita separar los protocolos, use
Proxy::http() y
Proxy::https()
por separado: esto es útil si el proveedor de proxy escucha diferentes puertos para diferentes protocolos.
Conexión de proxy SOCKS5
SOCKS5 opera a un nivel más bajo que los proxies HTTP y admite conexiones TCP sin analizar el contenido de la solicitud: esto es más rápido y estable para el parsing, especialmente si no solo accede a HTTP(S),
sino que también llama a puntos finales de WebSocket. Para SOCKS5, asegúrese de habilitar la característica socks
en Cargo.toml (ya se mencionó anteriormente). Ejemplo de conexión:
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()
}
Tenga en cuenta el esquema socks5h
en lugar de socks5.
La letra "h" al final significa que la resolución DNS del dominio ocurre en el lado del servidor proxy, no localmente.
Esto es crítico para la anonimidad: sin ella, su verdadero resolvedor DNS puede "filtrar" los dominios solicitados fuera del proxy,
incluso si el tráfico en sí pasa a través del túnel SOCKS5.
Si el proveedor de proxy no requiere autenticación por nombre de usuario/contraseña (IP-whitelisting), simplemente no llame a
basic_auth —
indique en el panel del proveedor la dirección IP de su servidor desde el cual se envían las solicitudes.
Grupo de clientes reqwest para diferentes proxies
Un error común es crear un nuevo Client
para cada solicitud. Esto es costoso: reqwest mantiene un grupo de conexiones TCP y sesiones TLS, y recrear el cliente
elimina todas las ventajas de keep-alive. El enfoque correcto es crear un cliente por cada proxy de antemano
y reutilizarlos desde un grupo común.
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 })
}
}
Este grupo se crea una vez al iniciar el parser. La cantidad de clientes suele ser igual al número de direcciones proxy únicas en su grupo: si el proveedor ofrece acceso a la rotación a nivel de gateway, basta con uno o dos clientes, ya que la rotación de IP ocurre automáticamente en el lado del proveedor con cada nueva conexión TCP.
Para tareas con alta paralelismo, envuelva el grupo en Arc
y compártalo entre las tareas de tokio: los clientes reqwest son seguros para hilos y están diseñados para su uso concurrente.
Estrategias de rotación de IP
Hay dos enfoques fundamentalmente diferentes para la rotación. El primero es la rotación del lado del proveedor: se conecta a una dirección de gateway, y el proveedor mismo sustituye una nueva IP en cada solicitud o cada N minutos. El segundo es la rotación de su lado: mantiene una lista de proxies estáticos y elige cuál usar para la siguiente solicitud.
Si utiliza proxies residenciales con rotación automática a través de gateway, el código se simplifica al mínimo: simplemente envía solicitudes a través del mismo cliente, el proveedor cambia la IP de salida por sí mismo. Pero si necesita control (por ejemplo, fijar la IP a una sesión durante 10 minutos para simular el comportamiento de un usuario real), implemente la rotación manualmente:
use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RotatingPool {
clients: Vec<Client>,
counter: AtomicUsize,
}
impl RotatingPool {
// Round-robin: iteramos los clientes uno a uno
fn next_round_robin(&self) -> &Client {
let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
&self.clients[idx]
}
// Selección aleatoria — útil contra el análisis de patrones
fn next_random(&self) -> &Client {
self.clients.choose(&mut rand::thread_rng()).unwrap()
}
}
Round-robin es adecuado cuando es importante una carga uniforme en todas las IP. La selección aleatoria oculta mejor el patrón de solicitudes, si el sitio objetivo analiza los intervalos entre las solicitudes desde una misma dirección. Para tareas como el monitoreo de precios en Ozon o la recolección de anuncios de Avito, en la práctica, es más efectiva una combinación: selección aleatoria de proxy más un retraso aleatorio de 1 a 3 segundos entre las solicitudes del mismo cliente.
Vale la pena mencionar las sesiones pegajosas. Si el parser sigue un escenario de múltiples pasos (inicio de sesión → navegación por páginas → acción), no se puede cambiar la IP a mitad de sesión: el sitio notará el cambio de IP durante una sesión de cookie activa y o bien cerrará la sesión o bloqueará la cuenta. Para tales escenarios, fije un cliente (y, por lo tanto, una IP) a una "sesión" específica del parser durante toda su vida útil.
Manejo de errores y lógica de reintento
Los proxies a veces fallan: tiempos de espera, 407 (Proxy Authentication Required), interrupciones de la conexión TCP. El parser debe ser capaz de distinguir entre "proxy roto — excluir del grupo" y "el sitio devolvió 429 — solo esperar y cambiar IP". Aquí hay un patrón funcional con reintentos y exclusión de proxies muertos:
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: límite de tasa, 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!("Estado inesperado: {}", resp.status());
}
Err(e) if e.is_timeout() || e.is_connect() => {
last_error = format!("Problema con el proxy: {e}");
continue;
}
Err(e) => {
last_error = e.to_string();
}
}
}
Err(format!("Todos los intentos agotados: {last_error}"))
}
Un matiz importante: el retraso exponencial (500 * attempt)
reduce la carga en el sitio objetivo durante respuestas masivas 429 y al mismo tiempo disminuye el riesgo de un bloqueo total
de todo el grupo de proxies a la vez. Para parsers de producción, agregue un contador de errores en cada proxy del grupo y excluya automáticamente
la dirección si da 5 o más errores consecutivos: esto protegerá de intentos inútiles a través de una IP ya bloqueada.
Qué tipo de proxy elegir para el parsing
La elección del tipo de proxy influye directamente en cuántas solicitudes exitosas obtendrá antes del bloqueo. Las IP de centros de datos son las más baratas y rápidas, pero pertenecen a rangos conocidos de proveedores de hosting: los sistemas anti-bot de marketplaces y redes sociales las detectan casi instantáneamente a través de bases de datos ASN. Las IP residenciales y móviles parecen usuarios normales, por lo que viven mucho más tiempo.
| Tipo de proxy | Velocidad | Riesgo de bloqueo | Cuándo aplicar |
|---|---|---|---|
| Centro de datos | Alta | Alta | APIs abiertas, sitios sin anti-bot |
| Residenciales | Media | Bajo | Wildberries, Ozon, Avito, redes sociales |
| Móviles | Media | Mínimo | Instagram, TikTok, estrictos anti-bots |
Para el parsing de marketplaces en grandes volúmenes, un equilibrio razonable son los proxies residenciales: no son tan caros en comparación con el tráfico como los móviles, pero pasan las verificaciones a nivel de "usuario real", que eliminan los rangos de centros de datos. Si el parser trabaja a través de la API de redes sociales o simula una aplicación móvil, los proxies móviles ofrecen el menor porcentaje de captchas y bloqueos temporales gracias a las IP asignadas a los operadores de telecomunicaciones.
Rendimiento y consejos sobre carga
Rust permite mantener miles de solicitudes concurrentes en una sola instancia, pero el proveedor de proxy y el sitio objetivo aún limitan el ancho de banda real. Algunas reglas prácticas para un parser de producción:
- Limite el paralelismo a través de
tokio::sync::Semaphore— no más de 20-50 solicitudes concurrentes por grupo de proxies, de lo contrario, algunas conexiones comenzarán a interrumpirse por tiempo de espera. - Utilice
pool_idle_timeouten ClientBuilder, para que reqwest no mantenga conexiones muertas a través de proxies inestables. - Registre los estados HTTP y el tiempo de respuesta por cada proxy por separado: así podrá ver rápidamente qué direcciones han comenzado a "descomponerse" y deben ser reemplazadas.
- La rotación de User-Agent junto con la rotación de IP reduce significativamente la probabilidad de detección: cámbielos de manera sincrónica, no en ciclos separados.
- Para parsers de larga duración, agregue un chequeo de salud: cada pocos minutos envíe una solicitud de prueba a https://httpbin.org/ip a través de cada proxy y excluya los que no respondan.
Preste especial atención a las huellas TLS (JA3/JA3S). reqwest utiliza por defecto rustls o nativetls
dependiendo de las características de la compilación, y ambos tienen diferentes huellas de apretón de manos TLS. Algunos sistemas anti-bots (por ejemplo,
en grandes marketplaces) analizan precisamente la huella TLS, no solo la IP — por lo tanto, incluso un proxy ideal
no ayudará si el cliente se presenta con un apretón de manos TLS no estándar. Si se enfrenta a bloqueos con
proxies "limpios", verifique las características de reqwest y pruebe la característica native-tls en lugar de rustls, o viceversa.
Conclusión
Los proxies en Rust reqwest se configuran rápidamente: unas pocas líneas con Proxy::all
o el esquema SOCKS5 socks5h://.
El trabajo principal comienza después: construir un grupo de clientes, elegir una estrategia de rotación (round-robin, aleatoria
o sesiones pegajosas) y un manejo adecuado de errores con la exclusión de direcciones no operativas del grupo. Sin esto, incluso
el parser más rápido en Rust se encontrará con bloqueos después de unos minutos de funcionamiento.
Si su parser trabaja con marketplaces o redes sociales, donde la protección anti-bots analiza no solo la frecuencia de solicitudes, sino también el origen de la IP, comience con proxies residenciales — ofrecen el mejor equilibrio entre precio y durabilidad de las sesiones. Para objetivos más estrictos como Instagram y TikTok, vale la pena probar IP móviles, y para tareas simples sin anti-bots, son suficientes proxies de centros de datos rápidos y económicos.