Volver al blog
Guías
Raluca PenciucLast updated on Apr 27, 202621 min read

Tutorial de Scrapy Playwright: Scrapear sitios con JavaScript a gran escala

En resumen: Scrapy-Playwright te permite renderizar páginas con mucho JavaScript directamente dentro de las arañas de Scrapy, controlando navegadores reales como Chromium, Firefox o WebKit a través de Playwright. Este tutorial te guía a través de la instalación, la configuración, las interacciones con las páginas, la interceptación de AJAX, las medidas antidetección y una estructura de proyecto lista para producción, para que puedas extraer datos de sitios dinámicos sin salir del ecosistema de Scrapy.

Scrapy es excelente para rastrear HTML estático a gran velocidad, pero en el momento en que un sitio de destino carga contenido mediante JavaScript, una solicitud estándar de Scrapy te devuelve un contenedor vacío. Ese es precisamente el problema que resuelve Scrapy Playwright. Se trata de un gestor de descargas de Scrapy que delega la renderización a Playwright, la biblioteca de automatización de navegadores de Microsoft, de modo que cada respuesta que recibe tu araña contiene el DOM completamente renderizado. Si has estado considerando la integración de Scrapy Playwright en tus propios proyectos pero no estabas seguro de cómo encajan todas las piezas, esta guía cubre cada paso: desde pip install hasta una araña lista para producción con elementos, pipelines y antidetección integrados. A lo largo del proceso, aprenderás estrategias de espera, interceptación de AJAX, gestión del desplazamiento infinito, configuración de proxies y los patrones de resolución de problemas que mantienen estables los rastreos largos.

¿Qué es Scrapy-Playwright y por qué utilizarlo?

Scrapy-Playwright (el paquete PyPI scrapy-playwright) es un gestor de descargas de Scrapy que sustituye el backend HTTP predeterminado por un navegador completo impulsado por Playwright. Cuando etiquetas una solicitud de Scrapy con "playwright": True en su meta diccionario, el gestor abre una página del navegador, navega a la URL, espera a que JavaScript termine y, a continuación, devuelve el HTML renderizado a tu parse callback como una solicitud normal de Scrapy Response.

¿Por qué es importante esto? Una parte cada vez mayor de la web renderiza el contenido del lado del cliente: paneles de React, escaparates de Vue, páginas protegidas por modales de consentimiento y sitios que cargan datos de productos a través de llamadas a la API en segundo plano. Scrapy estándar solo recupera el documento HTML inicial, que a menudo contiene <div> y un paquete de JavaScript, pero ninguno de los datos que realmente necesitas. Con la renderización de JavaScript de Scrapy Playwright, obtienes el mismo resultado que mostraría un navegador real, sin salir del familiar flujo de trabajo de solicitud/respuesta de Scrapy.

¿Cuándo debes habilitar Playwright en una solicitud? No todas las URL necesitan un navegador completo. Una regla general útil:

  • Utiliza una solicitud estándar de Scrapy cuando los datos que necesitas estén presentes en el HTML sin procesar o estén disponibles a través de un punto final de API directo que ya conozcas.
  • Utiliza una solicitud de Playwright cuando el contenido se inserte después de la carga de la página, cuando necesites hacer clic o desplazarte para revelar datos, o cuando la página dependa de cookies y redireccionamientos de JavaScript que sean difíciles de replicar con HTTP simple.

Mezclar ambos modos en una sola araña es fácil (y recomendable). Solo pagas la sobrecarga del navegador por las solicitudes que realmente lo necesitan, lo que mantiene tu rastreo rápido para las páginas que no lo requieren.

Scrapy-Playwright vs Scrapy-Splash vs Scrapy-Selenium

La elección entre los backends de renderizado del navegador para Scrapy se reduce a la carga de mantenimiento, la fidelidad del navegador y las herramientas existentes de tu equipo. Aquí tienes una comparación rápida:

Criterios

Scrapy-Playwright

Scrapy-Splash

Scrapy-Selenium

Motor del navegador

Chromium, Firefox o WebKit

Renderizador personalizado basado en Qt

Chrome o Firefox a través de WebDriver

Compatibilidad asíncrona

Nativo (asyncio)

Requiere un servidor Splash independiente

Sincronización por defecto; existen envoltorios asíncronos

Mantenimiento

Mantenimiento activo, comunidad en crecimiento

El desarrollo de Splash se ha ralentizado

Estable, pero depende del protocolo WebDriver

Fidelidad de JS

Navegador moderno completo

Buena, pero falla en algunos casos extremos

Navegador moderno completo

Facilidad de configuración

pip install + playwright install

Se requiere un contenedor Docker

Gestión de binarios de WebDriver

Interacciones con la página

Rico (click, fill, evaluate)

Scripting Lua limitado

API completa de WebDriver

Si vas a iniciar un nuevo proyecto hoy, Scrapy Playwright suele ser la mejor opción. Ofrece compatibilidad asíncrona moderna, métodos de interacción con páginas de primera clase y evita la sobrecarga operativa que supone ejecutar un servicio de renderizado independiente. Para profundizar en las ventajas e inconvenientes de Scrapy frente a Selenium, la guía comparativa sobre Scrapy vs. Selenium trata el tema en detalle.

Instalación y configuración del proyecto

Para poner en marcha un proyecto de Scrapy Playwright bastan unos pocos comandos de terminal. A continuación se detalla el proceso paso a paso.

Requisitos previos: Necesitas Python 3.8 o posterior y pip. Se recomienda encarecidamente utilizar un entorno virtual para mantener las dependencias aisladas.

# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install Scrapy and scrapy-playwright
pip install scrapy scrapy-playwright

# Install browser binaries (Chromium is the default)
playwright install chromium

El playwright install chromium comando descarga una versión específica de Chromium que Playwright gestiona internamente. También puedes instalar firefox o webkit si tu caso de uso requiere un motor diferente.

A continuación, crea un nuevo proyecto de Scrapy:

scrapy startproject myproject
cd myproject
scrapy genspider example example.com

Esto te proporciona la estructura de directorios estándar de Scrapy: settings.py, items.py, pipelines.py, middlewares.py, y una spiders/ carpeta. El único paso específico de Playwright que queda es actualizar settings.py, que veremos a continuación.

Una cosa que vale la pena señalar: scrapy-playwright depende de la API asíncrona de Playwright, que a su vez requiere el asyncioreactor Twisted basado en . Scrapy es compatible con esto, pero debes configurar explícitamente el reactor antes de que Scrapy intente utilizar su valor predeterminado. Olvidar este paso es el error de instalación más común que cometen los desarrolladores.

Configuración de Scrapy para Playwright

Abre el archivo settings.py y añade lo siguiente:

# settings.py

DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}

TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

# Optional: choose browser type (chromium, firefox, webkit)
PLAYWRIGHT_BROWSER_TYPE = "chromium"

# Optional: global navigation timeout in milliseconds
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000

El DOWNLOAD_HANDLERS dict le indica a Scrapy que redirija todas las solicitudes HTTP y HTTPS a través del controlador de Playwright. La TWISTED_REACTOR línea cambia el bucle de eventos de Scrapy a asyncio, tal y como requiere Playwright.

PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT establece el tiempo máximo (en milisegundos) que el navegador esperará a que se cargue una página. El valor predeterminado es de 30 segundos, lo cual es adecuado para la mayoría de los sitios. Si estás rastreando páginas especialmente lentas, auméntalo. Si quieres que se produzca un fallo rápido en las URL rotas, redúcelo.

Otros dos ajustes que conviene conocer:

  • PLAYWRIGHT_LAUNCH_OPTIONS: un diccionario pasado directamente a playwright.chromium.launch(). Úsalo para activar o desactivar el modo sin interfaz gráfica, rutas de ejecutables o configuración global de proxy.
  • PLAYWRIGHT_MAX_PAGES_PER_CONTEXT: limita el número de páginas que comparten un único contexto de navegador antes de que se cree uno nuevo. Esto puede ayudar con la gestión de la memoria en rastreos de gran tamaño.

Con estos ajustes configurados, cada solicitud de Scrapy que incluya "playwright": True en su meta será renderizada por Playwright. Las solicitudes sin ese indicador seguirán pasando por el descargador estándar de Scrapy, por lo que obtienes lo mejor de ambos mundos.

Representación de páginas con mucho JavaScript

Escribamos tu primera araña de Scrapy Playwright. El objetivo: visitar una página que carga su contenido con JavaScript y extraer datos del DOM completamente renderizado.

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = ["https://quotes.toscrape.com/js/"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url,
                meta={"playwright": True},
                callback=self.parse,
            )

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
            }

La línea clave es meta={"playwright": True}. Esa única bandera le indica al gestor de descargas que abra una página en el navegador, navegue hasta la URL, espere al load evento y devuelva el HTML renderizado como un TextResponse. Dentro de parse, utilizas los mismos selectores CSS (o XPath) que usarías con cualquier araña de Scrapy. No cambia nada en cuanto al análisis.

Ejecuta la araña con scrapy crawl quotes, y deberías ver las citas completamente extraídas, aunque la página utilice JavaScript para insertarlas en el DOM. Si probases la misma URL con una solicitud estándar de Scrapy (sin el indicador Playwright), response.css("div.quote") devolvería una lista vacía.

Este patrón es la base de todo lo demás en este tutorial de Scrapy Playwright. Todas las técnicas que siguen se basan en el mismo meta diccionario para pasar instrucciones adicionales al navegador.

Interacciones con la página: clics, desplazamiento y envíos de formularios

El scraping en el mundo real rara vez implica solo cargar una página. A menudo es necesario hacer clic en botones, rellenar formularios de búsqueda o desplazarse para activar contenido de carga diferida. Los métodos de página de Scrapy Playwright gestionan todo esto a través de la playwright_page_methods clave en la solicitud meta.

Un PageMethod es un contenedor que envuelve una acción de página de Playwright. Se pasa una lista de ellas y el controlador ejecuta cada una en orden tras la navegación inicial.

Hacer clic en un botón:

from scrapy_playwright.page import PageMethod

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod("click", selector="button#load-more"),
            PageMethod("wait_for_selector", selector="div.new-content"),
        ],
    },
    callback=self.parse,
)

Rellenar y enviar un formulario:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod("fill", selector="input#search", value="python scrapy"),
            PageMethod("click", selector="button[type=submit]"),
            PageMethod("wait_for_selector", selector="div.results"),
        ],
    },
    callback=self.parse,
)

Desplazarse hasta el final de una página:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod(
                "evaluate",
                "window.scrollTo(0, document.body.scrollHeight)",
            ),
            PageMethod("wait_for_timeout", 2000),
        ],
    },
    callback=self.parse,
)

Fíjate en el patrón: encadenas PageMethod las llamadas para simular una sesión de usuario real. El controlador las procesa secuencialmente, por lo que el orden importa. Incluye siempre una espera después de una acción que active contenido nuevo (un clic que dispare una llamada a la API, un desplazamiento que cargue más elementos) para dar tiempo a la página a actualizarse antes de que Scrapy capture el HTML final.

Un consejo práctico: mantén tu playwright_page_methods lista lo más corta posible. Cada llamada a un método añade latencia. Si puedes lograr el mismo resultado con menos pasos (por ejemplo, navegando directamente a una URL filtrada en lugar de rellenar un formulario), opta por el enfoque más sencillo.

Estrategias de espera para contenido dinámico

Elegir la estrategia de espera adecuada es fundamental para un rastreo fiable de contenido dinámico con Scrapy Playwright. Si esperas muy poco, obtendrás datos incompletos. Si esperas demasiado, tu rastreo se detendrá por completo.

Estos son los principales enfoques:

wait_for_selector es la opción más precisa. Pausa la ejecución hasta que aparece un selector CSS específico en el DOM.

PageMethod("wait_for_selector", selector="div.product-list")

Utilízala cuando sepas exactamente qué elemento indica que los datos se han cargado. Es rápida porque se resuelve en el momento en que el elemento existe, en lugar de esperar un tiempo arbitrario.

wait_for_load_state espera a que se produzca un evento concreto del ciclo de vida de la página:

  • "load": se activa cuando se han cargado el HTML inicial y todos los recursos (imágenes, hojas de estilo).
  • "domcontentloaded": se activa cuando se ha analizado el HTML, antes de que terminen las imágenes.
  • "networkidle": se activa cuando no ha habido conexiones de red durante al menos 500 ms.
PageMethod("wait_for_load_state", "networkidle")

networkidle es tentador porque captura la mayoría de las llamadas AJAX, pero puede resultar poco fiable en páginas con conexiones WebSocket persistentes, pings de análisis o rastreadores de anuncios que mantienen la red ocupada. También tiende a ser más lento que wait_for_selector.

wait_for_timeout es una pausa forzada, especificada en milisegundos.

PageMethod("wait_for_timeout", 3000)

Esta es la herramienta más burda. Úsala solo como último recurso, por ejemplo, en páginas donde no existe un selector estable y networkidle es inestable. Los tiempos de espera fijos hacen perder tiempo en páginas rápidas y aún así pueden no ser lo suficientemente largos en las lentas.

Recomendación: utilice wait_for_selector siempre que sea posible. Recurre a networkidle para páginas en las que no conozcas el selector exacto. Reserva wait_for_timeout para páginas realmente impredecibles, y mantén el valor lo más bajo posible.

Manejo del desplazamiento infinito y la paginación

Muchos sitios modernos utilizan patrones de desplazamiento infinito de Scrapy Playwright o navegación paginada para dividir el contenido en varias vistas. El manejo de ambos dentro de Scrapy requiere estrategias ligeramente diferentes.

El desplazamiento infinito suele funcionar desplazándose hasta la parte inferior de la página, esperando a que se carguen nuevos elementos y repitiendo el proceso hasta que no aparezcan más elementos. Dado que playwright_page_methods se ejecuta una vez antes de devolver la respuesta, es necesario gestionar el bucle de desplazamiento dentro de una page.evaluate llamada o accediendo directamente al objeto de página de Playwright.

El enfoque más limpio es utilizar la playwright_page meta para obtener la página Playwright sin procesar y programar el bucle tú mismo:

async def parse(self, response):
    page = response.meta["playwright_page"]
    previous_height = 0

    while True:
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        await page.wait_for_timeout(1500)
        current_height = await page.evaluate("document.body.scrollHeight")
        if current_height == previous_height:
            break
        previous_height = current_height

    # Re-read the fully scrolled page content
    content = await page.content()
    await page.close()

    sel = scrapy.Selector(text=content)
    for item in sel.css("div.item"):
        yield {
            "title": item.css("h3::text").get(),
        }

Fíjate en que cerramos la página explícitamente con await page.close(). Esto es esencial para la gestión de la memoria; de lo contrario, las páginas del navegador se acumulan y tu proceso se dispara en memoria.

La paginación (haciendo clic en «Siguiente» o basada en la URL) es más sencilla. Si el sitio utiliza parámetros de consulta (?page=2), basta con generar nuevas solicitudes de Scrapy con URL incrementadas. Si se basa en un botón «Siguiente», utiliza un PageMethod clic:

def parse(self, response):
    # Extract data from current page
    for product in response.css("div.product"):
        yield {"name": product.css("h2::text").get()}

    # Follow next page if it exists
    next_button = response.css("a.next-page::attr(href)").get()
    if next_button:
        yield response.follow(
            next_button,
            meta={"playwright": True},
            callback=self.parse,
        )

Para los sitios que utilizan botones «Cargar más» solo en JavaScript sin cambiar la URL, combina el patrón de clic de la sección de interacciones de página con un wait_for_selector para confirmar que han aparecido nuevos elementos antes de extraer los datos.

Interceptación de solicitudes AJAX

A veces, la fuente de datos más limpia no es el DOM renderizado, sino la llamada a la API en segundo plano que realiza la página para rellenarlo. La interceptación AJAX de Scrapy Playwright te permite capturar esas respuestas directamente, lo que a menudo te proporciona JSON estructurado sin necesidad de analizar el HTML.

Para interceptar respuestas, necesitas acceder al objeto de página de Playwright y a su response evento:

import json

class AjaxSpider(scrapy.Spider):
    name = "ajax_products"
    captured_data = []

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/products",
            meta={
                "playwright": True,
                "playwright_include_page": True,
            },
            callback=self.parse,
        )

    async def parse(self, response):
        page = response.meta["playwright_page"]

        async def handle_response(resp):
            if "/api/products" in resp.url:
                body = await resp.json()
                self.captured_data.extend(body.get("items", []))

        page.on("response", handle_response)

        # Trigger the AJAX call (e.g., scroll or click)
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        await page.wait_for_timeout(3000)
        await page.close()

        for product in self.captured_data:
            yield product

El page.on("response", ...) escucha se activa con cada respuesta de red. Se filtra por patrón de URL para capturar solo las llamadas a la API que te interesan. El cuerpo de la respuesta ya está analizado (.json() o .text()), por lo que te saltas por completo el recorrido del DOM.

Esta técnica resulta especialmente útil para aplicaciones de página única en las que el frontend realiza múltiples solicitudes API paginadas a medida que te desplazas. En lugar de analizar HTML complejo, obtienes datos limpios y estructurados directamente de la fuente.

Ejecución de JavaScript personalizado y captura de capturas de pantalla

Dos capacidades ligeras pero útiles de Scrapy Playwright son la ejecución de JavaScript personalizado y la captura de capturas de pantalla. Sirven para fines diferentes, pero comparten el mismo mecanismo: el acceso directo al objeto de página de Playwright.

La ejecución de JavaScript personalizado con page.evaluate te permite extraer datos que están ocultos en variables JavaScript o manipular el estado de la página antes de que Scrapy lea el HTML:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod(
                "evaluate",
                "document.querySelectorAll('.popup-overlay')"
                ".forEach(el => el.remove())",
            ),
        ],
    },
    callback=self.parse,
)

Esto elimina las superposiciones emergentes antes de que Scrapy analice la página, lo cual resulta útil para sitios que muestran ventanas modales en la primera visita.

Hacer una captura de pantalla con Scrapy Playwright es útil para depurar problemas de renderizado. Si tu araña extrae datos vacíos, una captura de pantalla te muestra exactamente lo que vio el navegador:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod("screenshot", path="debug.png", full_page=True),
        ],
    },
    callback=self.parse,
)

El full_page=True captura toda el área desplazable, no solo la ventana de visualización. Durante el desarrollo, puedes habilitar las capturas de pantalla de forma condicional (solo cuando una llamada de retorno de análisis no encuentra ningún elemento, por ejemplo) para evitar llenar el disco en los rastreos de producción.

Abortar solicitudes no deseadas para rastreos más rápidos

Todas las páginas del navegador cargan imágenes, fuentes, CSS, scripts de análisis y rastreadores de anuncios de forma predeterminada. Para el scraping, la mayoría de estos recursos son un lastre. Bloquearlos puede reducir drásticamente el uso de ancho de banda y acelerar la carga de las páginas.

Scrapy-Playwright admite la interceptación de solicitudes a través de la PLAYWRIGHT_ABORT_REQUEST . Se define una función asíncrona que inspecciona cada solicitud y devuelve True para abortarla:

# settings.py
PLAYWRIGHT_ABORT_REQUEST = "myproject.utils.should_abort"
# myproject/utils.py
from playwright.async_api import Request as PlaywrightRequest

async def should_abort(request: PlaywrightRequest) -> bool:
    blocked_types = {"image", "font", "stylesheet", "media"}
    if request.resource_type in blocked_types:
        return True
    blocked_domains = ["google-analytics.com", "doubleclick.net"]
    if any(domain in request.url for domain in blocked_domains):
        return True
    return False

El simple hecho de bloquear imágenes y fuentes puede reducir significativamente el tiempo de carga de la página, especialmente en sitios de comercio electrónico con gran cantidad de contenido multimedia. Solo hay que tener cuidado de no bloquear los archivos JavaScript que se encargan de renderizar el contenido que necesitas. Si tus datos desaparecen tras habilitar el bloqueo de solicitudes, vuelve a añadir "script" de nuevo a los tipos permitidos y restringe tu filtro a dominios específicos.

Uso de proxies con Scrapy-Playwright

Al realizar scraping a gran escala, los proxies rotativos son esenciales para evitar bloqueos de IP. La configuración de proxies de Scrapy Playwright funciona en dos niveles: global y por solicitud.

El proxy global se aplica a todas las solicitudes de Playwright. Configúralo en settings.py:

PLAYWRIGHT_LAUNCH_OPTIONS = {
    "proxy": {
        "server": "http://proxy-server:8080",
        "username": "user",
        "password": "pass",
    },
}

Esto pasa la configuración del proxy a la llamada de inicio del navegador, por lo que todas las páginas abiertas por esta instancia del navegador se enrutan a través de ese proxy.

El proxy por solicitud te ofrece un control más preciso. Utiliza playwright_context_kwargs en la solicitud meta para asignar diferentes proxies a solicitudes individuales:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_context_kwargs": {
            "proxy": {
                "server": "http://different-proxy:9090",
            },
        },
        "playwright_context": "proxy_context_1",
    },
    callback=self.parse,
)

Cada playwright_context crea un contexto de navegador independiente con su propio proxy, cookies y estado de almacenamiento. Así es como se aíslan las sesiones al rotar por un grupo de proxies.

Para rastreos en producción, considera servicios que gestionen la rotación de proxies y la resolución de CAPTCHA detrás de un único punto final, para que la lógica de tu araña se mantenga limpia. La clave es que la compatibilidad con proxies de Scrapy-Playwright es lo suficientemente flexible como para integrarse con cualquier estrategia de rotación que elijas.

Mejores prácticas de antidetección y sigilo

Los proxies por sí solos no son suficientes. Los sistemas antibots modernos comprueban las huellas del navegador, las cadenas de agente de usuario y los patrones de comportamiento. Estas son las capas de antidetección que deberías tener en cuenta para tus arañas de Scrapy Playwright.

Rotación del agente de usuario: Establece un agente de usuario realista y rotativo por contexto:

import random

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...",
    # Add more real browser UA strings
]

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_context_kwargs": {
            "user_agent": random.choice(USER_AGENTS),
        },
        "playwright_context": f"ctx_{random.randint(1, 100)}",
    },
    callback=self.parse,
)

Reducción de huellas digitales: El Chromium de Playwright tiene indicadores WebDriver predeterminados que los scripts antibots detectan. Puedes reducir tu huella digital:

  • Pasando "args": ["--disable-blink-features=AutomationControlled"] en PLAYWRIGHT_LAUNCH_OPTIONS.
  • Usando page.evaluate para eliminar la navigator.webdriver propiedad.
  • Establecer un tamaño de ventana gráfica realista en lugar de las dimensiones predeterminadas sin interfaz gráfica.

Retrasos aleatorios: añadir fluctuaciones entre las solicitudes evita que tu tráfico parezca un bot que bombardea el servidor a velocidad de máquina. Utiliza la DOWNLOAD_DELAY en combinación con RANDOMIZE_DOWNLOAD_DELAY:

DOWNLOAD_DELAY = 2
RANDOMIZE_DOWNLOAD_DELAY = True

la configuración del contexto Stealth: combina todo lo anterior en una configuración de contexto reutilizable. Para obtener una guía completa sobre cómo evitar bloqueos, el recurso «Consejos para evitar ser bloqueado o que te prohíban la IP al realizar web scraping» cubre estrategias adicionales que se aplican más allá de Scrapy-Playwright.

En resumen: considera la antidetección como múltiples capas en lugar de una única solución. Los proxies gestionan la reputación de la IP. La rotación del agente de usuario gestiona las comprobaciones a nivel de encabezado. La reducción de huellas digitales gestiona las comprobaciones a nivel de JavaScript. Los retrasos gestionan las comprobaciones de comportamiento. Necesitas que todos ellos funcionen juntos.

Contextos de navegador, sesiones y gestión de recursos

Un contexto de navegador en Playwright es una sesión de navegador aislada con sus propias cookies, almacenamiento local y caché. Scrapy-Playwright hace un uso intensivo de los contextos, y comprenderlos es clave para gestionar los recursos en rastreos de gran envergadura.

Por defecto, todas las solicitudes de Scrapy Playwright que no especifiquen un playwright_context nombre comparte un contexto predeterminado. Esto significa que las cookies persisten entre solicitudes, lo cual está bien para sitios en los que necesitas mantenerte conectado, pero resulta problemático si deseas sesiones limpias por solicitud.

Los contextos con nombre permiten aislar las sesiones:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_context": "session_a",
    },
    callback=self.parse,
)

Todas las solicitudes etiquetadas "session_a" comparten cookies y estado. Las solicitudes etiquetadas "session_b" obtienen una sesión completamente independiente. Esto resulta útil para flujos de trabajo de scraping en paralelo en los que es necesario simular múltiples usuarios independientes.

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT controla cuántas páginas se pueden abrir simultáneamente dentro de un mismo contexto. Cuando se alcanza el límite, se crea un nuevo contexto. Ajustar esta configuración ayuda a evitar el consumo excesivo de memoria:

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT = 4

Consejos para la gestión de la memoria:

  • Cierra siempre las páginas cuando utilices playwright_include_page. Si se olvida await page.close() en tu parse , las páginas se acumulan y el uso de memoria crece linealmente con el número de solicitudes.
  • Utiliza CONCURRENT_REQUESTS para limitar el paralelismo. Los navegadores consumen muchos recursos; entre 8 y 16 solicitudes simultáneas de Playwright es un punto de partida razonable en una máquina con 8 GB de RAM.
  • Supervisa la memoria RSS de tu araña durante las ejecuciones de prueba. Si aumenta de forma constante, comprueba si hay páginas sin cerrar o una creación excesiva de contexto.

En cuanto a los flujos de trabajo de scraping con navegadores sin interfaz gráfica en general, la guía sobre cómo ejecutar un navegador sin interfaz gráfica con Python aborda patrones de recursos que complementan lo que tratamos aquí.

Solución de problemas y gestión de errores

Incluso las arañas de Scrapy Playwright bien configuradas pueden fallar a gran escala. A continuación se presentan los problemas más comunes y las soluciones prácticas.

TimeoutError: Este es el error que verás con más frecuencia. Significa que el navegador no pudo completar la navegación o una espera dentro del tiempo permitido.

  • Aumenta PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT para sitios lentos.
  • Cambia de networkidle a wait_for_selector para evitar que se cuelgue en conexiones persistentes.
  • Comprueba si el sitio de destino te está bloqueando (una captura de pantalla de la página de tiempo de espera suele revelar un CAPTCHA o una página de bloqueo).

Desconexiones del navegador: si el proceso del navegador se bloquea durante el rastreo, verás BrowserError o Connection closed excepciones.

  • Reduzca CONCURRENT_REQUESTS. Demasiadas páginas en paralelo pueden agotar la memoria del sistema y bloquear el navegador.
  • Establece PLAYWRIGHT_MAX_PAGES_PER_CONTEXT un valor más bajo.
  • Añadir "args": ["--disable-dev-shm-usage"] a PLAYWRIGHT_LAUNCH_OPTIONS cuando se ejecute en Docker, donde /dev/shm suele ser demasiado pequeño.

Fugas de memoria: el uso de memoria de tu araña aumenta gradualmente durante los rastreos largos.

  • Comprueba que estás cerrando todas las páginas obtenidas mediante playwright_include_page. Cada página sin cerrar mantiene un DOM completo en la memoria.
  • Limita PLAYWRIGHT_MAX_PAGES_PER_CONTEXT y reinicia periódicamente los contextos.
  • Utiliza CLOSESPIDER_PAGECOUNT o una extensión personalizada para reiniciar la araña tras un umbral.

Patrones de error: Utiliza errback para gestionar los fallos con elegancia en lugar de dejar que provoquen el bloqueo de la araña:

yield scrapy.Request(
    url,
    meta={"playwright": True, "playwright_include_page": True},
    callback=self.parse,
    errback=self.handle_error,
)

async def handle_error(self, failure):
    page = failure.request.meta.get("playwright_page")
    if page:
        await page.close()
    self.logger.error(f"Request failed: {failure.request.url}")

El detalle clave: si has solicitado playwright_include_page, debes cerrar la página tanto en la llamada de retorno como en el errback. De lo contrario, una solicitud fallida provoca una fuga del objeto de página. Combina los errbacks con la configuración integrada de Scrapy RETRY_TIMES para reintentar automáticamente los fallos transitorios antes de darse por vencido.

Depuración con trazas: Playwright admite la grabación de trazas, que captura una cronología completa de las solicitudes de red, instantáneas del DOM y acciones. Actívala mediante PLAYWRIGHT_LAUNCH_OPTIONS durante el desarrollo para reproducir exactamente lo que hizo el navegador en una página problemática.

Creación de una araña lista para producción

Los tutoriales suelen terminar después de mostrarte cómo extraer datos. En producción, necesitas una estructura de proyecto completa con elementos, pipelines, middlewares y configuraciones bien ajustadas. A continuación te explicamos cómo conectar todo para un proyecto de Scrapy Playwright.

Define tus elementos:

# items.py
import scrapy

class ProductItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    url = scrapy.Field()

Usando Item clases (o dataclass items en las versiones más recientes de Scrapy) te permite validar el esquema y hace que el código de tu pipeline sea más limpio que si pasas diccionarios sin procesar.

Escribe un pipeline de elementos para la validación y el almacenamiento:

# pipelines.py
class ValidateProductPipeline:
    def process_item(self, item, spider):
        if not item.get("name"):
            raise scrapy.exceptions.DropItem("Missing name")
        item["price"] = float(item["price"].replace("$", "").strip())
        return item

class JsonWriterPipeline:
    def open_spider(self, spider):
        import json
        self.file = open("products.jsonl", "w")

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        import json
        self.file.write(json.dumps(dict(item)) + "\n")
        return item

Lista de verificación de la configuración de producción:

# settings.py (additions for production)
ITEM_PIPELINES = {
    "myproject.pipelines.ValidateProductPipeline": 100,
    "myproject.pipelines.JsonWriterPipeline": 200,
}

CONCURRENT_REQUESTS = 8
DOWNLOAD_DELAY = 1.5
RANDOMIZE_DOWNLOAD_DELAY = True
RETRY_TIMES = 3
LOG_LEVEL = "INFO"

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT = 4
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000

El patrón listo para producción es: los elementos estructurados fluyen a través de pipelines de validación, la configuración limita la concurrencia a un nivel que tu máquina y el sitio de destino puedan manejar, y la lógica de reintentos más los errbacks detectan los fallos transitorios. El recopilador de estadísticas integrado de Scrapy te proporciona métricas por rastreo (elementos extraídos, errores, reintentos) sin código adicional.

Para los equipos que desean aprender los fundamentos del web scraping con Scrapy antes de incorporar Playwright, la guía sobre web scraping con Scrapy proporciona una base sólida.

Puntos clave

  • Habilita Playwright de forma selectiva. Solo etiqueta las solicitudes con "playwright": True cuando la página realmente requiera renderización con JavaScript; combina solicitudes estándar de Scrapy para todo lo demás a fin de mantener la velocidad de los rastreos.
  • Utiliza wait_for_selector en lugar de networkidle o las pausas forzadas. La espera basada en selectores es más rápida y fiable para la mayoría de los escenarios de contenido dinámico.
  • Intercepta las llamadas AJAX siempre que sea posible. Capturar las respuestas de la API en segundo plano te proporciona JSON limpio y evita selectores DOM frágiles.
  • Aplica medidas de antidetección en capas: los proxies, la rotación de user-agent, la reducción de huellas digitales y los retrasos aleatorios deben funcionar conjuntamente, no sustituirse entre sí.
  • Cierra todas las páginas que abras. Las fugas de memoria de las páginas de Playwright sin cerrar son la causa más común de inestabilidad en los rastreos de Scrapy Playwright de larga duración.

Preguntas frecuentes

¿Scrapy-Playwright es compatible con Firefox y WebKit, o solo con Chromium?

Sí, son compatibles los tres motores. Establece PLAYWRIGHT_BROWSER_TYPE a "firefox" o "webkit" en la configuración de Scrapy y ejecuta playwright install firefox (o webkit) para descargar el binario del navegador correspondiente. Chromium es el predeterminado y el más probado, pero Firefox puede ser útil para sitios que detectan específicamente Chromium.

¿Cómo soluciono las excepciones TimeoutError en Scrapy-Playwright?

Empieza por aumentar PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT más allá de los 30 segundos predeterminados. Si el tiempo de espera persiste, cambia tu estrategia de espera de networkidle a wait_for_selector apuntando a un elemento específico. Haz también una captura de pantalla de la página que falla para comprobar si el sitio está mostrando un CAPTCHA o una página de bloqueo en lugar del contenido esperado.

¿Puedo ejecutar Scrapy-Playwright en modo headful (navegador visible) para la depuración?

Sí. Añade "headless": False a PLAYWRIGHT_LAUNCH_OPTIONS en settings.py. La ventana del navegador se abrirá de forma visible, lo que te permitirá ver cada navegación e interacción en tiempo real. Esto es muy valioso para depurar secuencias de métodos de página. Recuerda volver al modo sin interfaz gráfica antes de ejecutar rastreos de producción.

¿Cuánta memoria consume Scrapy-Playwright y cómo puedo reducir el consumo?

Cada página de Chromium consume aproximadamente entre 50 y 150 MB de RAM, dependiendo de la complejidad de la página. Para reducir la memoria, baja CONCURRENT_REQUESTS, establece PLAYWRIGHT_MAX_PAGES_PER_CONTEXT a un número pequeño (de 3 a 5), omite los tipos de recursos innecesarios (imágenes, fuentes, hojas de estilo) y cierra siempre las páginas explícitamente tanto en tus métodos callback como errback.

¿Cuál es la diferencia entre Scrapy-Playwright, Scrapy-Splash y Scrapy-Selenium?

Scrapy-Playwright utiliza la moderna API asíncrona de Playwright con Chromium, Firefox o WebKit. Scrapy-Splash se basa en un servicio de renderizado independiente basado en Docker con interactividad limitada. Scrapy-Selenium envuelve el protocolo WebDriver más antiguo. Para nuevos proyectos, Scrapy-Playwright suele ofrecer la mejor combinación de fidelidad del navegador, rendimiento asíncrono y mantenimiento activo.

Conclusión

Scrapy Playwright tiende un puente entre el potente motor de rastreo de Scrapy y la realidad de la web actual impulsada por JavaScript. Al añadir un único indicador meta a tus solicitudes, obtienes una renderización completa del navegador sin abandonar los flujos de trabajo, el middleware y el modelo de concurrencia de Scrapy. Este tutorial ha cubierto todo el espectro: desde la instalación y configuración iniciales hasta las interacciones con la página, la interceptación de AJAX, la antidetección y el refuerzo para producción.

Las técnicas aquí expuestas deberían cubrir la gran mayoría de los escenarios de scraping dinámico. Para proyectos en los que la gestión de la infraestructura del navegador, la rotación de proxies y la antidetección a gran escala se convierten en el cuello de botella, en lugar de la propia lógica de scraping, nuestra API Scraper gestiona esas capas detrás de un único punto final para que puedas centrarte en los datos en lugar de en la infraestructura.

Sea cual sea el enfoque que elijas, el principio básico sigue siendo el mismo: utiliza el renderizado del navegador solo cuando sea necesario, mantén tus arañas bien estructuradas y cierra todas las páginas que abras.

Acerca del autor
Raluca Penciuc, Desarrollador full-stack @ WebScrapingAPI
Raluca PenciucDesarrollador full-stack

Raluca Penciuc es desarrolladora full stack en WebScrapingAPI, donde se dedica a crear rastreadores, mejorar las técnicas de evasión y buscar formas fiables de reducir la detección en los sitios web de destino.

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.