1) Introducción detallada
Escalar scraping no consiste en “tirar más hilos” o “meter más proxies”. A gran volumen, la mayoría de bloqueos no ocurren por el contenido solicitado, sino por el patrón de comportamiento y por la huella técnica (fingerprint) que dejas en red, TLS, HTTP, navegador y tiempos. Cuando pasas de cientos a millones de requests/día, el objetivo deja de ser “descargar HTML” y pasa a ser “simular tráfico plausible” bajo restricciones de coste, latencia y estabilidad, mientras mantienes trazabilidad y control del riesgo. Un sistema de scraping escalable se diseña como un sistema distribuido con gestión de identidad (IP, sesión, cookies, fingerprint), control adaptativo de carga y un pipeline de extracción que se degrada con gracia ante cambios del sitio.
Los bloqueos típicos vienen de varias capas: limitación por IP/ASN (rate limiting), reputación de salida (datacenter vs residencial), detección de automatización por encabezados incoherentes, orden de headers, JA3/JA4 y huellas TLS, análisis de comportamiento (timing, navegación, interacción), detección de headless (propiedades de WebDriver, canvas, audio), y WAF/bot managers (Cloudflare, Akamai, Imperva, PerimeterX, DataDome) que correlacionan señales. En paralelo, el propio servidor objetivo aplica defensas: CAPTCHAs, respuestas 403/429, desafíos JS, honeypots, contenido falso, redirects, tarpit (respuestas lentas), o degradación selectiva de endpoints.
La estrategia correcta se apoya en tres pilares: (1) Arquitectura: colas, workers, límites por dominio, orquestación de sesiones y un “scheduler” que decide con datos; (2) Red y huella: egress controlado, perfiles de cliente realistas, rotación con afinidad, y navegadores/herramientas con fingerprints coherentes; (3) Observabilidad y adaptación: medir bloqueos, detectar cambios, reintentar con backoff, y ajustar automáticamente. Esta guía va al grano: cómo diseñar el sistema, qué parámetros ajustar, qué señales vigilar y cómo implementar ejemplos de código para scraping HTTP y scraping con navegador, siempre priorizando estabilidad, coste y reducción de bloqueos.
2) Contexto histórico o teórico
El scraping “clásico” (años 2000–2010) se basaba en requests HTTP simples con cookies mínimas y parsers HTML. Las defensas eran principalmente rate limiting, bloqueos por IP y cambios de DOM. A partir de 2013–2016, con la explosión de SPAs, la renderización client-side y el auge de bot managers, la detección pasó a ser estadística y multi-señal: se combinan huellas TLS, comportamiento de navegación, coherencia de headers y telemetría JS (propiedades del navegador, WebGL, canvas, tiempos). El estándar de facto dejó de ser “ser rápido” y pasó a ser “ser indistinguible dentro de un cluster de tráfico normal”.
Teóricamente, la detección de bots puede modelarse como un clasificador que estima la probabilidad de automatización con señales observables: capa 3/4 (IP/ASN), capa 7 (headers, orden, compresión), TLS handshake (ciphers, extensiones, ALPN), y señales de cliente (JS). Tu objetivo al escalar no es eliminar señales (imposible), sino mantenerlas coherentes y dentro de rangos plausibles, evitando correlaciones raras (p.ej., miles de requests con el mismo fingerprint desde IPs distintas, o cambios de idioma/zonahoraria inconsistentes). En scraping a escala, la unidad de control no es la request, sino la identidad: una identidad = (IP/egress, fingerprint, sesión/cookies, patrón temporal). Y el segundo concepto clave es el presupuesto de riesgo: cada dominio/endpoint tiene una tolerancia distinta; tu scheduler debe gastar ese presupuesto de manera inteligente (concurrencia, cadencia, reintentos, rutas alternativas como APIs internas o feeds).
3) Sección técnica 1: Arquitectura escalable (colas, scheduler, límites por dominio y afinidad de sesión)
Escalar sin bloqueos empieza con una arquitectura que imponga disciplina. El error típico es lanzar N workers sin coordinación: generan picos, repiten errores y amplifican bloqueos. El patrón robusto es scheduler central + cola + workers idempotentes, con límites por dominio/host/endpoint. Componentes recomendados:
a) Cola de trabajos: Redis Streams, RabbitMQ, SQS, Kafka. El job debe incluir: URL, método, prioridad, dominio, tipo (HTML/API/render), metadatos (categoría, profundidad), y política de reintento. La idempotencia es obligatoria: si el job se reejecuta, no debe duplicar resultados o debe deduplicar por clave.
b) Scheduler consciente del dominio: no basta con “rate global”. Implementa token buckets por dominio y por endpoint crítico (login, búsqueda, producto). Cada bucket define QPS máximo, concurrencia máxima y “burst”. También define ventanas horarias y jitter para evitar periodicidad. Un scheduler avanzado adapta estos límites según métricas (403/429, latencia, porcentaje de JS challenge).
c) Gestión de identidad y afinidad: muchas defensas correlacionan sesión/cookies con IP. Si rotas IP en cada request, empeoras: cada request parece “nuevo usuario”, dispara desafíos y reduce cache. Necesitas sticky sessions: asignar un conjunto de URLs a una identidad por un tiempo (p.ej., 10–30 minutos) y rotar cuando detectas degradación. Para sitios con carritos o paginaciones, la afinidad es crítica: misma IP + mismas cookies + mismo fingerprint + misma zona horaria.
d) Control de reintentos: el reintento agresivo es un multiplicador de bloqueos. Usa exponential backoff con jitter y cambia estrategia: si 429, reduce ritmo y mantén identidad; si 403 con challenge, cambia a navegador; si timeout repetido, cambia egress/ASN. Define un máximo de reintentos y “circuit breaker” por dominio: si la tasa de bloqueo supera umbral, pausar automáticamente y alertar.
e) Almacenamiento y deduplicación: el scraping a escala produce duplicados por redirects, parámetros, UTM, paginaciones. Normaliza URLs (canonización), guarda huellas (hash del contenido/ETag) y deduplica a nivel de entidad. Esto reduce carga y, por tanto, bloqueos.
Una referencia práctica: divide en tres pipelines: (1) descubrimiento (sitemaps, categorías, búsqueda), (2) extracción (detalle), (3) verificación (recrawls). Cada pipeline tiene límites distintos. La extracción de detalle suele tolerar menos QPS pero más estabilidad; el descubrimiento tolera más fallos pero puede usar fuentes alternativas (RSS, APIs). Diseña con la idea de que cada dominio es un “tenant” con su propia configuración de riesgo y rendimiento.
4) Sección técnica 2: Red, proxies y egress (rotación con criterio, reputación, ASN, geolocalización)
La red es donde la mayoría de equipos se equivocan por simplismo. “Más proxies” no es igual a “menos bloqueos”. Las defensas modernas miran reputación de IP, ASN (datacenter vs ISP), coherencia geográfica y patrones de conexión. Reglas prácticas:
a) Tipos de egress: Datacenter (barato, rápido, más bloqueable), ISP/residencial (más caro, mejor aceptación), móvil (caro, buena evasión), y “unblockers” (servicios que resuelven desafíos). Para volumen masivo, combina: datacenter para endpoints poco protegidos; ISP/residencial para páginas sensibles; navegador/unblocker para “última milla” (páginas con challenge).
b) Rotación con afinidad: rotar por request rompe coherencia. Usa rotación por sesión o por “batch” de tareas relacionadas. Mantén la IP durante un conjunto de requests y rota cuando la tasa de éxito cae. Esto minimiza señales de “spray” y hace que cookies y cache funcionen.
c) Pools segmentados: separa pools por dominio y por función. Un pool “sucio” (IPs quemadas) no debe contaminar un dominio sensible. Implementa un “IP health score” basado en: ratio 2xx, ratio 403/429, latencia, frecuencia de CAPTCHA. Retira IPs degradadas automáticamente y reintrodúcelas tras enfriamiento.
d) Geolocalización y coherencia: si un sitio sirve contenido distinto por país, la IP debe coincidir con Accept-Language, timezone y moneda. Cambiar de país en la misma sesión es una señal fuerte. Define perfiles regionales (ES, FR, US) y mantén consistencia por identidad.
e) Conexiones y HTTP/2: la forma de abrir conexiones también importa. Reutiliza conexiones (keep-alive) de forma realista. Algunos clientes bots abren demasiadas conexiones cortas. En HTTP/2, cuida el número de streams concurrentes. El “shape” del tráfico (conexiones por minuto, duración) debe parecer humano o, al menos, parecido a un crawler legítimo (p.ej. Googlebot tiene patrones conocidos, pero no intentes imitarlo).
f) DNS y egress control: usa resolvers consistentes por región; cambios erráticos de DNS pueden correlacionarse con automatización. Si controlas infraestructura, usa NAT gateways por región y limita salidas. Si usas proveedores, pide rotación por ASN y sesiones stickies.
Coste: a escala, el egress es el principal. Optimiza reduciendo requests (cache, condicionales, evitar render cuando no hace falta) antes que comprar más IPs. Y recuerda que “evitar bloqueos” no es 100%: el objetivo es mantener una tasa estable y predecible con degradación controlada.
5) Sección técnica 3: Fingerprinting en HTTP/TLS y consistencia del cliente
Muchos bloqueos ocurren aunque uses proxies “buenos” porque tu cliente HTTP delata automatización. Señales típicas: orden de headers no estándar, valores incoherentes, JA3/JA4 raro, falta de soporte de compresión real, User-Agent sin correspondencia con TLS, o patrones de navegación imposibles. A nivel técnico:
a) Headers coherentes: No basta con poner User-Agent. Debes alinear Accept, Accept-Language, Accept-Encoding, Connection, Upgrade-Insecure-Requests, Sec-Fetch-*, Sec-CH-UA*, DNT, etc. Para clientes no-browser (requests, aiohttp) es difícil replicar exactamente Chrome. Si el target es estricto, usa un stack que pueda emular tráfico real (Playwright con Chromium real) o un cliente que imite fingerprints (librerías especializadas). Si el target es moderado, un cliente HTTP bien configurado puede bastar.
b) Orden y casing: algunos bot managers miran el orden de headers, el uso de mayúsculas/minúsculas y la presencia de headers “extraños”. Muchos clientes HTTP envían headers en un orden fijo que no coincide con navegadores. Solución: usar navegadores reales o stacks que permitan controlar orden (no trivial). En Python, httpx/aiohttp no te lo ponen fácil; en Go puedes ajustar más, pero sigue habiendo huellas TLS.
c) TLS fingerprint (JA3/JA4): el handshake TLS incluye lista de ciphers, extensiones, curvas, ALPN. Navegadores tienen perfiles concretos; librerías como OpenSSL/Go crypto/tls generan perfiles distintos. Si tu HTTP client no coincide con el User-Agent, es señal. Para endpoints duros, usa Playwright/Chromium (que “trae” su TLS), o herramientas que soporten uTLS (en Go) para clonar fingerprints de Chrome/Firefox. Esto es especialmente relevante en Cloudflare/Akamai.
d) Cookies y almacenamiento: muchos sitios establecen cookies anti-bot (por ejemplo, basadas en JS). Si haces requests sin ejecutar JS, no las obtendrás. Puedes: (1) pasar por un navegador para bootstrap de cookies y luego usar HTTP client, (2) mantener un navegador por identidad, o (3) usar un “unblocker” para resolver el challenge. El enfoque híbrido suele ser el más eficiente: navegador sólo para obtener tokens, HTTP para descargar HTML/API.
e) Control de caché y condicionales: usa If-Modified-Since/If-None-Match cuando sea coherente. Reduce carga y te hace parecer un cliente bien comportado. Pero cuidado: algunos sitios cambian ETags por request o los vinculan a sesión; si ves inconsistencias, desactívalo por dominio.
f) Patrón temporal: Jitter realista, pausas, y evitar periodicidad exacta. Un scheduler debe introducir variabilidad (distribución log-normal para tiempos, no uniforme) y respetar límites. Si haces 10 RPS constantes durante horas desde la misma IP, aunque sea “bajo”, es sospechoso.
En resumen: la evasión no es “hackear”, es alinear tu tráfico con los invariantes de un cliente real. A mayor protección, más se impone usar navegador real o perfiles TLS realistas, y mantener coherencia estricta entre identidad, región y comportamiento.
6) Sección técnica 4: Scraping con navegador (Playwright) a escala: sesiones, stealth, interacción mínima y resolución de desafíos
Cuando el contenido depende de JS o hay desafíos, el navegador es inevitable. Pero escalar Playwright/Puppeteer mal configurado es un festival de bloqueos y costes. Claves:
a) Contextos y almacenamiento: usa browser contexts para aislar identidades. Cada identidad mantiene cookies/localStorage. Persiste almacenamiento (storageState) para reutilizar sesiones sin relogin/rehacer challenge. Rota contextos con TTL.
b) Concurrencia controlada: no abras 200 Chromes. Usa un pool de navegadores con múltiples contextos (limitados). Mide CPU/RAM. A menudo, 1 browser con 5–10 contextos es más estable que 10 browsers con 1 contexto cada uno, dependiendo del sitio. Ajusta según consumo y tasa de éxito.
c) Interacción mínima pero plausible: no hagas “random mouse moves” sin sentido; muchos sitios detectan patrones de automatización ridículos. Lo que sí ayuda: esperar a eventos correctos (networkidle, DOMContentLoaded), respetar tiempos de carga, scroll sólo si desbloquea lazy-load real, y no disparar 30 navigations por segundo.
d) Headless vs headful: hoy, headless moderno está mejor, pero algunos sitios siguen bloqueando headless por señales JS. Prueba ambos. En casos duros, headful con xvfb o contenedores con GPU puede mejorar, pero aumenta coste. Lo importante: coherencia de fingerprint (timezone, locale, viewport, device scale factor).
e) Bloqueo de recursos: para escalar, bloquea imágenes, fuentes y trackers (siempre que no rompas la lógica anti-bot). Muchos desafíos cargan scripts específicos: si los bloqueas, no obtendrás cookies. Solución: listas allow/deny por dominio, no un bloqueo global. Primero “aprende” qué recursos son necesarios.
f) Estrategia híbrida: usa Playwright para: (1) obtener HTML renderizado cuando hace falta, (2) capturar tokens/cookies, (3) descubrir endpoints XHR internos y luego llamar a la API con HTTP client. Esto reduce el uso de navegador y baja bloqueos.
g) Manejo de challenges y CAPTCHAs: idealmente, evita llegar a CAPTCHA controlando ritmo y reputación. Si aparece, decide: abortar (para no quemar IP), reintentar con otra identidad, o resolver vía servicios (con implicaciones legales y éticas). En términos de escalabilidad, un CAPTCHA recurrente es señal de mala configuración: revisa fingerprint, egress y comportamiento.
h) Instrumentación: guarda artefactos cuando hay fallo: screenshot, HAR, HTML, consola, y response headers. Sin esto, “optimizar” es adivinar. A escala, necesitas muestreo (no guardes todo) y correlación por identidad y por IP.
El navegador es una herramienta quirúrgica. Si lo usas como martillo para todo, pagarás en coste y en bloqueos. La disciplina es: navegador para bootstrap/JS; HTTP para volumen; scheduler para controlar.
7) Sección técnica 5: Observabilidad, detección de bloqueos, autoadaptación y resiliencia ante cambios
El scraping a escala falla por dos razones: bloqueos y cambios del target. Ambos se gestionan con observabilidad y sistemas adaptativos.
a) Taxonomía de fallos: clasifica respuestas en categorías accionables: 2xx OK; 3xx redirects (a login/challenge); 4xx (403 forbidden, 401 auth, 404); 429 rate limit; 5xx server; timeouts. Para HTML, además detecta “soft blocks”: página de verificación 200 OK con contenido de challenge, HTML vacío, contenido falso. Implementa detectores por firma (título, patrones en DOM, scripts de challenge) y por heurística (tamaño anómalo, presencia de palabras clave).
b) Métricas por dominio: al menos: tasa de éxito (2xx reales), tasa de soft-block, 403/429, latencia p50/p95, bytes descargados, ratio de reintentos, CAPTCHAs por 1000 requests, y “cost per success” (proxy + compute). Segmenta por identidad/ASN/geo. Sin segmentación, no sabrás qué pool te está quemando.
c) Ajuste automático (control loop): implementa un lazo de control: si 429 sube, reduce QPS y aumenta backoff; si 403 sube en un pool, rota a otro ASN; si soft-block aparece, eleva a navegador o cambia fingerprint; si latencia sube, reduce concurrencia para evitar colas internas del target. Usa límites para no oscilar (histeresis).
d) Circuit breakers: cuando el sitio entra en “modo defensa” (picos de 403), insistir quema IPs. Pausa el dominio, enfría el pool, y reanuda gradualmente. El circuito debe ser por dominio y por endpoint.
e) Resiliencia a cambios del DOM: separa “fetch” de “parse”. Guarda HTML crudo (o una muestra) para reproducir. Usa parsers robustos: selectores tolerantes, extracción por atributos semánticos (schema.org, JSON-LD), o combinaciones. Implementa tests de extracción con fixtures reales. Cuando el parsing falla, marca versión y activa “fallback” (otra estrategia de selector) antes de declarar error.
f) Anti-detección de scraping interno: no sólo te bloquea el WAF. A veces te bloquea la aplicación (p.ej. límite por cuenta, detección de scraping en búsquedas). Controla el número de búsquedas, paginaciones, y evita endpoints “caros”. Prefiere sitemaps, feeds, endpoints de catálogo o APIs internas cuando existan y sean estables.
g) Compliance y auditoría: registra qué se pidió, cuándo y con qué identidad. Esto no es burocracia: te permite demostrar buenas prácticas (rate limiting, respeto a exclusiones) y depurar incidentes. Además, limita la recolección de PII y aplica minimización: menos datos = menos riesgo y menos fricción.
Sin observabilidad no hay escalado: sólo hay suerte. A gran escala, el scraping es un sistema de control adaptativo con telemetría y políticas automatizadas.
8) Ejemplos de código detallados
8.1. Scheduler con límites por dominio (Python + Redis) y backoff con jitter
Ejemplo simplificado: un scheduler que consume jobs de Redis, aplica un token bucket por dominio y ejecuta fetchers. En producción separarías scheduler/worker, añadirías persistencia y métricas.
import time
import random
import json
import redis
from collections import defaultdict
from dataclasses import dataclass
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
@dataclass
class DomainPolicy:
qps: float # tokens per second
burst: int # max tokens
max_concurrency: int
base_backoff: float
max_backoff: float
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.updated = time.time()
def take(self, n=1):
now = time.time()
elapsed = now - self.updated
self.updated = now
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
if self.tokens >= n:
self.tokens -= n
return True
return False
def jittered_backoff(attempt, base=1.0, cap=60.0):
# exponential backoff with full jitter
exp = min(cap, base * (2 ** attempt))
return random.random() * exp
policies = {
"example.com": DomainPolicy(qps=0.5, burst=2, max_concurrency=2, base_backoff=2, max_backoff=120),
"api.example.com": DomainPolicy(qps=2.0, burst=10, max_concurrency=10, base_backoff=1, max_backoff=30),
}
buckets = {d: TokenBucket(p.qps, p.burst) for d, p in policies.items()}
concurrency = defaultdict(int)
def run_once():
raw = r.lpop("jobs")
if not raw:
time.sleep(0.2)
return
job = json.loads(raw)
domain = job["domain"]
attempt = job.get("attempt", 0)
policy = policies.get(domain, DomainPolicy(qps=0.2, burst=1, max_concurrency=1, base_backoff=2, max_backoff=120))
bucket = buckets.setdefault(domain, TokenBucket(policy.qps, policy.burst))
if concurrency[domain] >= policy.max_concurrency or not bucket.take(1):
# requeue with small delay (simulado)
r.rpush("jobs", raw)
time.sleep(0.05)
return
concurrency[domain] += 1
try:
# Aquí iría tu fetch real
status = fake_fetch(job)
if status in (429,):
delay = jittered_backoff(attempt, base=policy.base_backoff, cap=policy.max_backoff)
job["attempt"] = attempt + 1
job["not_before"] = time.time() + delay
r.rpush("jobs_delayed", json.dumps(job))
elif status in (403,):
# Escala a navegador o cambia identidad/pool
job["mode"] = "browser"
job["attempt"] = attempt + 1
r.rpush("jobs", json.dumps(job))
else:
r.rpush("results", json.dumps({"url": job["url"], "status": status}))
finally:
concurrency[domain] -= 1
def fake_fetch(job):
# Simulación de respuestas según modo
url = job["url"]
mode = job.get("mode", "http")
# ejemplo simple: algunas URLs fallan si no es browser
if "challenge" in url and mode == "http":
return 403
# rate limiting aleatorio
if random.random() < 0.02:
return 429
return 200
def promote_delayed():
# mover jobs cuyo not_before ya pasó
now = time.time()
# simplificado: barrido naive
for _ in range(200):
raw = r.lpop("jobs_delayed")
if not raw:
break
job = json.loads(raw)
if job.get("not_before", 0) <= now:
r.rpush("jobs", json.dumps(job))
else:
r.rpush("jobs_delayed", raw)
break
while True:
promote_delayed()
run_once()
8.2. Cliente HTTP con sesiones y coherencia básica (httpx) + detección de soft-block
import httpx
import re
SOFT_BLOCK_PATTERNS = [
re.compile(r"checking your browser", re.I),
re.compile(r"verify you are human", re.I),
re.compile(r"attention required", re.I),
]
def is_soft_block(html: str) -> bool:
if not html or len(html) < 500:
return True
return any(p.search(html) for p in SOFT_BLOCK_PATTERNS)
class ScrapeClient:
def __init__(self, proxy_url: str | None = None, locale="es-ES,es;q=0.9", ua=None):
self.headers = {
"User-Agent": ua or "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": locale,
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
}
self.client = httpx.Client(
headers=self.headers,
proxies=proxy_url,
timeout=30,
follow_redirects=True,
http2=True,
)
def get_html(self, url: str):
r = self.client.get(url)
text = r.text
if r.status_code == 200 and is_soft_block(text):
return {"status": 200, "soft_block": True, "html": text, "headers": dict(r.headers)}
return {"status": r.status_code, "soft_block": False, "html": text, "headers": dict(r.headers)}
# Uso
# sc = ScrapeClient(proxy_url="http://user:pass@ip:port")
# out = sc.get_html("https://example.com")
8.3. Playwright: identidad persistente, bloqueo selectivo de recursos y extracción híbrida
from playwright.sync_api import sync_playwright
import json
BLOCK_TYPES = {"image", "font", "media"}
def run_browser_job(url: str, storage_state_path: str, proxy: dict | None = None):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
locale="es-ES",
timezone_id="Europe/Madrid",
viewport={"width": 1366, "height": 768},
proxy=proxy, # {"server": "http://ip:port", "username": "u", "password": "p"}
)
# Carga sesión previa si existe
try:
with open(storage_state_path, "r", encoding="utf-8") as f:
state = json.load(f)
context.add_cookies(state.get("cookies", []))
except FileNotFoundError:
pass
page = context.new_page()
def route_handler(route, request):
if request.resource_type in BLOCK_TYPES:
return route.abort()
return route.continue_()
page.route("**/*", route_handler)
page.goto(url, wait_until="domcontentloaded", timeout=60000)
page.wait_for_timeout(800) # pequeña pausa, evita patrones demasiado perfectos
html = page.content()
# Persistir estado para reutilizar cookies en siguientes jobs
state = context.storage_state()
with open(storage_state_path, "w", encoding="utf-8") as f:
json.dump(state, f)
browser.close()
return html
# html = run_browser_job("https://sitio-con-challenge.com", "./state_es.json")
8.4. Go + uTLS (concepto) para alinear fingerprint TLS
En targets muy estrictos, puedes necesitar un cliente con perfil TLS similar a Chrome. En Go, se usa frecuentemente utls para imitar ClientHello. Ejemplo conceptual (puede requerir ajustes según versión):
package main
import (
"fmt"
"io"
"net/http"
utls "github.com/refraction-networking/utls"
)
// Nota: para producción, encapsula transporte, proxy, cookies y retries.
func main() {
// Transporte con uTLS suele requerir un dialer TLS custom.
// Aquí se muestra la idea: usar un ClientHelloID tipo Chrome.
_ = utls.HelloChrome_Auto
req, _ := http.NewRequest("GET", "https://example.com", nil)
req.Header.Set("User-Agent", "Mozilla/5.0 ... Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, len(b))
}
La implementación completa implica un RoundTripper custom con dial TLS usando uTLS, más gestión de proxies y keep-alive. La idea clave: no mezclar un User-Agent de Chrome con un handshake TLS que no se parece a Chrome.
9) Comparativa de pros y contras (enfoques y decisiones)
1) Scraping HTTP “puro” (sin navegador)
Pros: barato, rápido, fácil de paralelizar, menos consumo CPU/RAM, ideal para endpoints estables o APIs internas accesibles.
Contras: fingerprint TLS/headers suele delatar, no ejecuta JS (no obtiene tokens), sufre con challenges, mayor probabilidad de soft-block si el sitio usa bot manager agresivo.
2) Navegador headless (Playwright/Puppeteer)
Pros: alta compatibilidad con sitios modernos, ejecuta JS, obtiene cookies/tokens, fingerprint más cercano al real (si se configura bien).
Contras: caro y pesado, más complejo de operar (crashes, fugas de memoria, timeouts), más sensible a cambios, difícil de escalar sin un pool y límites estrictos.
3) Estrategia híbrida (browser bootstrap + HTTP volumen)
Pros: mejor relación coste/éxito, reduce uso de navegador, mantiene compatibilidad con desafíos, escalado más eficiente.
Contras: complejidad: sincronizar cookies/tokens, caducidad, invalidaciones; requiere buen diseño de identidad y almacenamiento.
4) Proxies datacenter vs ISP/residencial
Datacenter Pros: coste bajo, latencia baja, ancho de banda alto, fácil de automatizar.
Datacenter Contras: reputación peor, bloqueos por ASN, útil sólo en targets permisivos.
ISP/residencial Pros: aceptación alta, menos challenges, mejor para sitios sensibles.
ISP/residencial Contras: coste alto, latencia variable, rotación limitada, gestión de sesiones más delicada.
5) Rotación agresiva vs sticky sessions
Rotación agresiva Pros: dispersa carga, útil ante rate limits estrictos por IP.
Rotación agresiva Contras: rompe coherencia, dispara challenges, invalida cookies y reduce cache.
Sticky Pros: coherencia alta, menos detección, mejor para flujos con estado.
Sticky Contras: si la identidad se “quema”, hay que rotarla y limpiar; requiere scoring y circuit breakers.
10) Conclusión
Escalar scraping sin ser bloqueado es un problema de ingeniería de sistemas, no un truco aislado. La solución robusta combina: arquitectura con scheduler y límites por dominio, control de identidad (IP+sesión+fingerprint), egress con reputación y rotación con afinidad, clientes coherentes a nivel HTTP/TLS, uso estratégico de navegador sólo donde aporta valor, y un sistema de observabilidad que detecta soft-blocks, clasifica fallos y adapta la carga automáticamente. Si intentas resolverlo sólo con proxies o sólo con headless, pagarás en bloqueos o en coste.
En la práctica, la hoja de ruta que mejor funciona es: (1) reducir requests con descubrimiento inteligente, caché y deduplicación; (2) implementar límites por dominio y reintentos con backoff; (3) segmentar pools de egress y medir salud de IPs; (4) alinear fingerprints y mantener coherencia regional; (5) añadir Playwright como capa de escalado para desafíos/JS y usar un enfoque híbrido para el volumen; (6) cerrar el ciclo con métricas, circuit breakers y tests de parsing. Con esto, pasas de scraping “que funciona a ratos” a un sistema industrial con rendimiento estable y bloqueos controlados.