El fallo de scraping para el que todo el mundo se prepara es el obvio: un 403, un 429, una conexión que hace timeout. Lo puedes ver, contar y reintentar. El fallo que de verdad envenena tu dataset es el contrario: un 200 OK que parece completamente exitoso y no contiene nada de lo que querías. Una página de bloqueo disfrazada de respuesta normal. Una pantalla de desafío. Una cáscara vacía. Una página de datos fabricada específicamente para dar a un bot mala información.
Un scraper que da error es molesto. Un scraper que “tiene éxito” con basura es peligroso, porque no te enteras hasta que los datos malos ya están en tu almacén, en un informe o en un modelo. Esta es la otra mitad de por qué se bloquean los scrapers: los sistemas anti-bot modernos prefieren cada vez más engañar en silencio en lugar de rechazar a gritos, precisamente porque un fallo silencioso vale más para ellos que uno visible. Aquí está cómo pillarlo.
El código de estado no es la verdad
El primer hábito que romper es fiarte de los códigos de estado HTTP como señal de éxito. Un 200 significa que el servidor envió una respuesta, no que envió la respuesta que pediste. Los sitios devuelven de forma rutinaria páginas de bloqueo, CAPTCHAs y muros de consentimiento con un 200, porque hacerlo mantiene a los scrapers ingenuos contentos y callados mientras no les sirve nada. Trata el código de estado como una señal débil y valida el cuerpo en cada petición.
Vale la pena nombrar las categorías de 200 engañoso, porque cada una tiene un indicio distinto:
- Bloqueos suaves e interstitials. Una página de desafío o de “verifica que eres humano” devuelta con un
200, a menudo mucho más pequeña que una página real. - Pantallas de desafío y CAPTCHA. Servidas en línea donde debería estar el contenido. Relacionado con cómo evolucionaron las defensas anti-bot.
- Contenido degradado o recortado. Un muro de login, un stub de “activa JavaScript”, o un esqueleto vacío donde se suponía que renderizaban los datos.
- Soft-fails de límite de ritmo. Resultados obsoletos, cacheados o vacíos devueltos en lugar de un error una vez que cruzas un umbral.
- Geo o personalización equivocada. La página correcta para el país, moneda o estado no-logueado equivocado, técnicamente válida, silenciosamente inútil.
- Honeypots y datos envenenados. Contenido servido deliberadamente a bots: precios falsos, enlaces trampa, o registros de aspecto plausible con valores imposibles.
Valida el contenido, no solo la entrega
La defensa más eficaz es una aserción de contenido en cada respuesta: una comprobación barata de que la página contiene lo que una página real debe contener. Elige un invariante que un resultado genuino siempre tiene y una página de bloqueo nunca: un elemento específico, un campo requerido, una longitud mínima plausible, y falla la petición si falta.
def is_valid_product_page(html, parsed): # Una página de producto real siempre tiene esto. Una de bloqueo, ninguno. if len(html) < 2000: # las páginas de bloqueo suelen ser diminutas return False if parsed.select_one("h1.product-title") is None: return False # el elemento ancla ya no está if parsed.select_one('[data-price]') is None: return False # falta el campo que veníamos a buscar return TrueEl cambio clave es que un elemento ancla ausente es un fallo, no un resultado vacío. Si el selector del que dependes no está, no registres una fila en blanco y sigas, trata la respuesta como un bloqueo suave y reintenta con una identidad fresca igual que harías con un 403. Registrar blancos es como un bloqueo se convierte en silencio en miles de filas vacías que nadie nota hasta mucho después.
Vigila la forma de tus respuestas, no solo las individuales
Las comprobaciones individuales pillan la basura obvia. Las comprobaciones distribucionales pillan la deriva sutil, y son las que separan un pipeline robusto de uno frágil.
- Tamaño de respuesta. Las páginas de bloqueo y desafío tienden a ser pequeñas y uniformes. Un colapso repentino en el tamaño medio de página a lo largo de un lote, o un pico de respuestas agrupadas en un recuento de bytes exacto, es una firma de bloqueo aunque cada una devuelva
200. - Hashing de respuestas. Hashea una versión normalizada de cada cuerpo de respuesta. Cuando el mismo hash se repite de repente por muchas URLs distintas, te están sirviendo una página de bloqueo una y otra vez, no contenido real y variado.
- Tasa de relleno de campos. Sigue el porcentaje de registros donde cada campo de verdad se pobló. Un campo que estaba al 98 por ciento ayer y al 4 por ciento hoy no se volvió menos común, empezaste a recibir páginas recortadas.
- Tasa de éxito por host. Una caída en un destino específico, mientras otros se mantienen estables, es ese destino cambiando su postura hacia ti, digna de pillar antes de que malgaste una corrida entera. Esta es la misma señal por destino que importa al monitorizar un pipeline de scraping a escala.
Nada de esto necesita machine learning. Son contadores y líneas base simples, y son la diferencia entre notar un bloqueo en las primeras cien peticiones y notarlo tras un millón.
Firmas de páginas de bloqueo conocidas
Mantén una pequeña biblioteca de frases y marcadores que solo aparecen en páginas de bloqueo, desafío o error, y marca cualquier respuesta que los contenga sin importar el código de estado.
BLOCK_MARKERS = ( "verify you are human", "unusual traffic", "access denied", "enable javascript to continue", "request blocked",)
def looks_blocked(text): low = text.lower() return any(marker in low for marker in BLOCK_MARKERS)Mantenla corta y específica para que no dé falsos positivos con contenido real que casualmente hable de esos temas, y hazla crecer a medida que conozcas nuevas defensas. Una página de desafío que puedes reconocer es un desafío que puedes reintentar en lugar de almacenar.
Honeypots y datos envenenados
La categoría más desagradable es el contenido diseñado para parecer real. Aquí importan dos defensas.
Primero, no sigas enlaces trampa. Los enlaces honeypot suelen estar ocultos a los humanos con display:none, visibility:hidden, tamaño cero, posicionamiento fuera de pantalla, o aria-hidden, y existen solo para atrapar a un bot que sigue cada ancla. Un crawler que respeta la visibilidad, e ignora enlaces que un humano nunca podría clicar, esquiva la mayoría de ellos.
Segundo, haz un chequeo de sensatez de los datos en sí. Los registros envenenados están construidos para pasar un parser ingenuo pero tienden a fallar reglas de dominio básicas: precios que son cero o absurdamente altos, fechas en el futuro, cantidades que no pueden existir, valores fuera de cualquier rango plausible. Valida contra los rangos que tu dominio de verdad permite, y pon en cuarentena los registros que los violen en lugar de fiarte de ellos porque el HTML parseó limpiamente.
Peticiones canario
El aviso temprano más fiable es un canario: obtén periódicamente una página cuyo contenido correcto ya conoces, y afirma que sigue coincidiendo. Cuando tu canario empieza a devolver una página de bloqueo o los datos equivocados, sabes que el destino se volvió contra ti, inmediatamente y sin ambigüedad, sin tener que inferirlo de un lento declive de la calidad de los datos. Corre canarios por destino y por geo, ya que un sitio puede bloquear las IP de salida de un país mientras deja otro intacto.
Dónde encajan los proxies
Las IP limpias reducen con qué frecuencia te bloquean suavemente de entrada, un pool con buena reputación pasa sin problema donde las direcciones marcadas reciben en silencio una página de desafío. Eso baja la tasa de respuestas engañosas que tienes que pillar, pero no elimina la necesidad de pillarlas, porque los bloqueos son cada vez más invisibles por diseño y ninguna IP es inmune. Los dos funcionan juntos: la detección te dice que una respuesta fue un bloqueo suave, y un pool residencial rotativo te da una identidad fresca con la que reintentarla, exactamente como tratarías un fallo duro. Realimenta cada bloqueo suave detectado en tu lógica de reintento y rotación (sticky vs rotativo), y trata un pico persistente de tasa de bloqueo por destino como una señal para retroceder en lugar de martillear (evitar bloqueos y scrapear responsablemente ambos aplican).
En resumen
Asume que la respuesta miente hasta que demuestre lo contrario. Valida el contenido en cada petición contra un invariante que una página real siempre tiene, y trata un ancla ausente como un fallo a reintentar, no una fila vacía a almacenar. Vigila la distribución de tamaños de respuesta, hashes y tasas de relleno para que un bloqueo que devuelve 200 igual aparezca como una anomalía. Mantén una lista corta de firmas de páginas de bloqueo conocidas, niégate a seguir enlaces trampa ocultos, haz chequeo de sensatez de los datos contra reglas de dominio, y corre canarios para enterarte en el instante en que un destino se vuelve contra ti.
Haz eso, y el fallo silencioso, el que corrompe en silencio un dataset durante semanas, deja de ser silencioso. Combínalo con un pool residencial limpio para que te bloqueen suavemente menos de entrada, y los planes por GB te dejan validar esto contra tus propios destinos sin comprometerte por adelantado.