Desarrollamos tu web presencial SPA DESDE 300€. Si, es una locura. Web SPA DESDE 300€ — Si, es una locura. Hablemos →

Cómo evitar que te bloqueen la IP cuando varios procesos hacen scraping a la vez

Cómo evitar que te bloqueen la IP cuando varios procesos hacen scraping a la vez

Todo el que ha automatizado la consulta de un sitio externo ha puesto una pausa entre peticiones y ha dado el problema por resuelto. Funciona mientras solo hay un proceso pidiendo. Cuando tu aplicación crece y consultan a la vez la cola, la petición web y una tarea programada, esa pausa deja de servir y llega el bloqueo. Aquí está por qué falla y cómo se coordina de verdad, con un caso real que nos costó un 403. Los ejemplos van en Laravel, una de nuestras especialidades, y comparamos con spatie/crawler, que es la referencia del ecosistema, pero la coordinación entre procesos se plantea igual con cualquier stack.

El 403 no llega por hacer muchas peticiones, llega por hacerlas a la vez

Casi todo el mundo empieza igual: un sleep(1) entre peticiones y a correr. Funciona en local, funciona en preproducción, y funciona en producción hasta el día que el mismo sitio se consulta desde tres sitios distintos de tu aplicación.

Nos pasó con el buscador de jurisprudencia del Consejo General del Poder Judicial. El comentario que dejamos en la configuración de producción lo cuenta tal cual:

El chat, el MCP y los tres workers de la cola le disparan a poderjudicial.es sin saber unos de otros. En diecisiete minutos se llegaron a hacer del orden de veinte sesiones de navegador, porque el modelo lanza búsquedas en paralelo por ángulos distintos y cada una reintenta tres veces. CENDOJ acabó devolviendo 403.

Ninguno de esos procesos se estaba portando mal. Cada uno respetaba su pausa. El problema es que la pausa era local a cada proceso y el sitio remoto ve una sola dirección IP.

Un sleep entre peticiones no es un límite de velocidad

Esta es la parte que cuesta ver. Si tu forma de ir despacio es dormir un segundo después de cada petición, lo que tienes es un límite por proceso. Con cinco procesos vas a cinco veces esa velocidad y no te has enterado.

La solución intuitiva tampoco funciona: guardar en algún sitio cuándo salió la última petición y esperar a que pase el intervalo. Suena bien y es una condición de carrera de manual. Tres procesos que despiertan a la vez leen la misma respuesta, esperan los mismos segundos y disparan en el mismo instante. Has convertido tres peticiones seguidas en una ráfaga de tres, disfrazada de petición espaciada.

Lo que sí funciona es invertir el orden: primero reservas el turno, después duermes. La reserva es lo que hay que proteger con un cerrojo, no la espera. Cada proceso pide su hueco, recibe una marca de tiempo distinta y duerme hasta la suya. Tres procesos simultáneos salen espaciados sin haberse hablado nunca.

Ese calendario compartido vive en la caché de la aplicación, con una entrada por objetivo. Si usas Redis o Memcached, la coordinación cruza máquinas; con la caché de fichero se queda en una sola.

Cuando ya te han bloqueado, reintentar igual empeora las cosas

Un fallo de red y un 403 no son la misma clase de problema, aunque los dos sean "no ha salido bien".

Un 500 o un 504 son transitorios: esperas un poco y vuelves a intentarlo por el mismo sitio. Un 403 o un 429 significan que esa salida está quemada. Reintentar por ahí después de esperar quince segundos no arregla nada y prolonga el castigo.

El tratamiento correcto tiene dos partes. Primero, anotar esa salida como bloqueada durante un tiempo que crece si vuelve a pasar, empezando en un par de minutos y doblando hasta un tope. Segundo, no esperar antes de reintentar, porque el reintento va a salir por otra dirección, y en la nueva ya se aplica el calendario general.

Y un detalle que evita insistir en vano: si no queda ninguna salida sana, el fallo se acepta. Reintentar contra la dirección que acaba de rechazarte solo alarga el bloqueo.

Andie recomienda

Configura la clave del calendario por objetivo lógico, no por dominio. En nuestro caso hay dos, el buscador y la descarga de documentos, contra el mismo sitio y con intervalos distintos: diez segundos para buscar y tres para descargar. Si lo agrupas por dominio, la parte lenta le impone su ritmo a la rápida sin motivo.

Mentir con el user agent te delata más que ir de frente

Falsear el user agent es de las primeras cosas que se hacen al automatizar peticiones, y se hace por una intuición razonable: si pareces un navegador normal, te tratan como a uno. Lo que casi nadie comprueba es cuántas cosas más mira el servidor antes de decidir si se lo cree.

La costumbre es inventarse una cadena de user agent de un Chrome normal para que no se note que es un navegador automatizado. El problema es que un Chrome de verdad no solo manda esa cadena: manda también los Client Hints, unas cabeceras que declaran marca y versión del navegador, y expone lo mismo en JavaScript.

Esos datos los rellena el navegador real, y no se falsean escribiendo una cadena. Así que cuando inventas el user agent lo que consigues es una contradicción: una cabecera que dice ser Chrome 148 junto a unos Client Hints que dicen otra cosa. Eso no es pasar desapercibido, es levantar la mano.

Sale mejor preguntarle al navegador cuál es su cadena real y quitarle solo la palabra que delata el modo sin ventana. Todo lo demás queda coherente porque es coherente.

Separar la petición del parseo cambia cómo se depura

Hay un beneficio menos vistoso que se agradece a los seis meses: distinguir dos tipos de fallo que se confunden siempre.

Que la petición no salga (la red, un tiempo agotado, un status de error tras los reintentos) es un fallo de transporte, y lo natural es que lance una excepción.

Que la página no contenga lo que esperabas (un captcha, cero resultados, el diseño cambiado) no es un error del programa, es un dato. Y merece volver como resultado, no como excepción.

Confundir los dos tiene consecuencias reales. En nuestra plataforma legal, "esta búsqueda no ha devuelto sentencias" y "no hemos podido preguntar" acababan en el mismo sitio, y el copiloto le decía a un abogado que ciertas resoluciones no existían cuando lo cierto es que el buscador estaba bloqueado. Son respuestas distintas y el usuario merece saber cuál es.

Qué hay en el mercado y en qué se diferencia

En Laravel la referencia es spatie/crawler, y es un paquete excelente para lo que hace. La diferencia es que resuelve otro problema.

Ese paquete es un rastreador de descubrimiento: le das una URL, extrae los enlaces, los encola y recorre el sitio entero, respetando robots.txt y con un pool de peticiones concurrentes. Si lo que necesitas es recorrer una web, es la herramienta.

Larascraper, que es el que mantenemos, no descubre nada: parte de rutas que ya conoces y va a por datos concretos. A cambio trae tres cosas que el otro no plantea porque no las necesita: el calendario compartido entre procesos del que va este artículo, el bloqueo por salida con retroceso exponencial, y una cadena de acciones para operar la página viva, con clicks, formularios, condiciones y bucles acotados.

Ese último punto es el que decide en la práctica. Renderizar JavaScript y devolver el HTML final lo hacen los dos. Rellenar un formulario, resolver un captcha de texto, enviar y capturar el PDF que devuelve el servidor, repitiendo hasta ocho veces si el captcha falla, es otro problema. Nació de eso: boletines oficiales con visores de PDF detrás de captcha y documentos escaneados sin capa de texto.

Tres cosas que hay que dejar montadas

Ninguna es complicada, pero conviene resolverlas antes de la primera ejecución en producción.

Ajusta la concurrencia según el modo. Cuando la extracción no necesita navegador, las peticiones se solapan de verdad. Con navegador, cada ejecución es un Chromium aislado y van de una en una. Configurar veinte en paralelo con navegador no acelera nada.

Hay que instalar cosas fuera de Composer. Node, los paquetes de Puppeteer y un binario de Chrome, en el mismo entorno donde se ejecuta. Si trabajas con contenedores, dentro del contenedor. Y para extraer texto de PDF hacen falta utilidades del sistema.

Y la trampa que nos costó una tarde: lanzar el navegador necesita ejecutar un proceso externo, y en muchos despliegues esa función está desactivada en el PHP que sirve las peticiones web por seguridad. En la línea de comandos no lo está. La solución no es abrirla en el servidor web, es mover la extracción a una cola, que es donde debería estar de todos modos.

Y si no trabajas con Laravel

El diseño se traslada entero, porque el problema no es del lenguaje.

Necesitas tres cosas: un calendario compartido en un almacén que vean todos los procesos, con reserva de turno antes de esperar; un registro de salidas bloqueadas con retroceso exponencial; y separar el fallo de transporte del fallo de contenido para no mentirle a quien consume los datos.

Si tu extracción vive en un solo proceso, nada de esto hace falta y un sleep te vale. En cuanto haya un segundo proceso, tu límite de velocidad ha dejado de existir aunque el código no haya cambiado.

Dónde está el código

El paquete está publicado en Packagist, el código en GitHub y la documentación del paquete recoge todas las opciones. Es la capa de extracción que hay detrás de Crowd Legal, donde recorre boletines oficiales de varios países.

Si tienes que integrar datos de fuentes que no ofrecen API, es de las cosas que más veces hemos resuelto. Lo trabajamos en aplicaciones web a medida y desarrollo Laravel, y si nos cuentas el caso te decimos por dónde iría.

Escrito por
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desarrollador full-stack con más de 15 años de experiencia en Laravel, React, Node.js y arquitecturas cloud. Ayudo a empresas en Andorra a construir su presencia digital.

Partner de diseño · ionospace.
Necesitas ayuda? ×
Andie by AndorraDev
Asistente IA + equipo humano
Asistente IA de AndorraDev
Andie
Hola! Soy Andie, el asistente IA de AndorraDev. ¿En qué puedo ayudarte? Si necesitas hablar con Edu, solo pídelo.
17:27