Volver al blog
Guías
Mihai MaximLast updated on May 13, 202618 min read

Web Scraping con Scrapy: Playbook 2026

Web Scraping con Scrapy: Playbook 2026
En resumen: Esta es una guía completa y con un enfoque muy concreto sobre el web scraping con Scrapy en 2026. Instalarás Scrapy, crearás prototipos de selectores en la terminal, desarrollarás una araña de comercio electrónico de varias páginas, limpiarás los elementos con Item Loaders, los guardarás en una base de datos, reforzarás la configuración para evitar bloqueos e integrarás Scrapy-Playwright para páginas renderizadas en JavaScript.

Scrapy ha sido la columna vertebral del rastreo serio con Python durante más de una década y, a pesar de la oleada de nuevas bibliotecas asíncronas, sigue demostrando su valía. Si hoy en día realizas web scraping con Scrapy, dispones de un marco de trabajo con una visión propia que resuelve las partes aburridas (programación de solicitudes, deduplicación, reintentos, flujos de elementos) para que puedas centrarte en las partes que realmente fallan: selectores, antibots y almacenamiento.

Esta guía se estructura en torno al ciclo de vida de las solicitudes y respuestas, en lugar de seguir una progresión cronológica. Cada sección se corresponde con un componente de Scrapy con el que trabajarás en producción, desde el motor y los middlewares de descarga hasta los cargadores de elementos y las exportaciones de feeds. Utilizamos un único objetivo a lo largo de todo el proceso, el sitio de prácticas público books.toscrape.com, de modo que cada bloque de código encaja en un único modelo mental.

Al final tendrás una araña ejecutable que pagina un catálogo, valida y limpia elementos, escribe tanto en JSON Lines como en SQLite, realiza reintentos en 429 casos de error y recurre a un navegador real cuando una página necesita JavaScript. También señalaremos las partes del marco que los principiantes suelen utilizar incorrectamente, con soluciones que se pueden copiar.

Por qué Scrapy sigue dominando el scraping de producción en 2026

Es tentador recurrir a httpx más selectolax y dar el tema por zanjado. Para un script puntual, esa es la decisión correcta. Para un rastreador que tiene que ejecutarse cada noche, deduplicar URL, sobrevivir a una interrupción parcial del servicio y escribir en dos destinos, lo que necesitas es un marco de trabajo. Scrapy sigue siendo el estándar del sector para la extracción de datos a gran escala en el momento de escribir este artículo, y la razón es sencilla: incluye el programador, el filtro de duplicados, el middleware de reintentos, el limitador de velocidad, las señales y las exportaciones de feeds ya integrados.

En comparación con la combinación de requests y BeautifulSoup, Scrapy tiene una postura definida que resulta útil. Se ejecuta en el bucle de eventos de Twisted, por lo que un único proceso puede distribuir cientos de solicitudes simultáneas sin la sobrecarga cognitiva de async/await. No tienes que escribir el bucle de rastreo. Declaras las URL de entrada y la lógica de análisis, y el motor se encarga de la cola. Ese acuerdo es lo que hace que Scrapy merezca la pena a pesar de su curva de aprendizaje más pronunciada.

Cómo funciona el web scraping con Scrapy: el ciclo de vida de la solicitud y la respuesta

Antes de escribir una araña, interioriza el ciclo de vida. Una ejecución de Scrapy tiene este aspecto:

  1. El motor retira una Request del programador.
  2. La solicitud pasa por los middlewares del descargador (por orden de prioridad). Aquí es donde se configuran los encabezados, se añaden las cookies, se rotan los proxies y se activan los reintentos.
  3. El descargador emite la llamada HTTP y devuelve un Response.
  4. La respuesta vuelve a pasar por los middlewares del descargador al entrar, luego por los middlewares de la araña y, finalmente, llega a la llamada de retorno de tu araña (normalmente parse).
  5. Tu callback yielddevuelve más Request objetos (que vuelven al programador) o Item(que fluyen hacia los pipelines de elementos).
  6. Los pipelines validan, transforman, descartan o persisten cada elemento.
  7. Todo lo que sobreviva se entrega al exportador de feeds, que escribe en disco, S3 o stdout.

Dos términos que verás en las devoluciones de llamada: callback es la función que ejecuta Scrapy cuando una solicitud tiene éxito, y errback es la función que ejecuta cuando una solicitud falla. Las arañas suelen escribirse como generadores de Python, generando solicitudes y elementos de forma diferida para que el motor pueda intercalar el trabajo.

Conocer este ciclo marca la diferencia entre «mi araña funciona» y «mi araña escala». Cuando las páginas vuelven vacías, la respuesta casi siempre está en la capa de middleware del descargador. Cuando los elementos desaparecen, la respuesta está en un canal. Cuando falla la paginación, es tu callback. Relaciona el síntoma con la etapa y, a continuación, corrige el componente adecuado.

En la documentación oficial de la arquitectura de Scrapy hay una guía más detallada que vale la pena marcar como favorita.

Instalación de Scrapy e inicio de un proyecto

Scrapy está pensado para Python 3 moderno (consulta la guía de instalación oficial para ver la versión mínima en el momento de la instalación). La documentación recomienda encarecidamente un entorno virtual dedicado para que las dependencias fijas de Scrapy no entren en conflicto con los paquetes del sistema.

python -m venv .venv
source .venv/bin/activate     # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install scrapy
scrapy version

Una vez que scrapy version imprima una cadena de versión, crea un esqueleto de proyecto:

scrapy startproject bookstore
cd bookstore

Ahora tienes un árbol de proyecto que tiene el mismo aspecto en todos los repositorios de Scrapy del mundo, que es precisamente la idea. Cada vez que te incorporas a un nuevo repositorio de Scrapy, ya sabes dónde están las arañas, dónde se encuentran los ajustes y qué archivo contiene los flujos de trabajo. Esa repetibilidad es la mitad del valor de usar un marco de trabajo en primer lugar. Resiste la tentación de simplificar la estructura: herramientas posteriores como scrapyd y scrapy crawl dependen de él.

Dentro de un proyecto de Scrapy: qué hace cada archivo

scrapy startproject genera cinco archivos y una carpeta con los que trabajarás a diario.

  • scrapy.cfg es la configuración de nivel superior del proyecto. Nombra el proyecto e indica scrapyd dónde se encuentra el módulo de configuración.
  • items.py es la capa de esquema. Aquí defines Product, Article, o las clases que desees, cada una de las cuales hereda de scrapy.Item. Trátala como una clase de datos para la salida extraída.
  • pipelines.py Es donde los elementos extraídos se limpian, validan, descartan o escriben en una base de datos. Cada canalización es una clase simple con un process_item método.
  • middlewares.py Contiene middlewares de descarga y rastreo. Este es el archivo donde se rotan los agentes de usuario, se añaden proxies o se enrutan las solicitudes a través de una API de rastreo gestionada.
  • settings.py es el objeto de configuración central: concurrencia, limitación, reintentos, pipelines, middlewares y exportaciones de feeds se encuentran todos aquí.
  • spiders/ es la carpeta donde se encuentran los archivos de araña individuales. Una araña por sitio de destino es un valor predeterminado adecuado.

Creación de prototipos de selectores en el shell de Scrapy

El shell de Scrapy es el arma secreta de la que nadie habla lo suficiente. Antes de escribir una sola línea de código de araña, abre el shell con una URL real y prueba los selectores de forma interactiva. Te ahorrará horas.

scrapy shell "https://books.toscrape.com/catalogue/page-1.html"

Dentro del shell obtienes un response objeto en tiempo real precargado con la página. Hay tres comandos importantes:

  • fetch("https://example.com") cambia a una nueva respuesta sin salir del shell.
  • view(response) abre el HTML descargado en tu navegador predeterminado, lo que te permite confirmar que estás trabajando con el mismo DOM que ve la araña, no con el renderizado que tu navegador mostraría normalmente.
  • response.css(...) y response.xpath(...) te permite probar selectores con la respuesta en tiempo real.

Prueba esto en el sitio de práctica:

>>> response.css("article.product_pod h3 a::attr(title)").getall()[:3]
['A Light in the Attic', 'Tipping the Velvet', 'Soumission']
>>> response.xpath("//article[@class='product_pod']//p[@class='price_color']/text()").get()
'£51.77'

Repite el proceso hasta que ambos selectores devuelvan datos limpios. Solo entonces traslada la expresión a tu araña. El coste de depurar un XPath defectuoso dentro de un rastreo de 5 minutos es mucho mayor que el coste de una sesión de shell.

Escribir tu primera araña para el web scraping con Scrapy

Genera un esbozo de araña para tu dominio de destino:

scrapy genspider books books.toscrape.com

Esto crea spiders/books.py. Reemplaza su contenido con la araña que aparece a continuación. Esta araña rastrea la página de inicio del catálogo, extrae el título, el precio y la valoración de cada libro, y luego genera un diccionario de Python por libro. Lo actualizaremos a objetos reales en una sección posterior.

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]

    def parse(self, response):
        for card in response.css("article.product_pod"):
            yield {
                "title": card.css("h3 a::attr(title)").get(),
                "price": card.css("p.price_color::text").get(),
                "rating": card.css("p.star-rating::attr(class)").get(),
                "url": response.urljoin(card.css("h3 a::attr(href)").get()),
            }

Ejecútalo desde la raíz del proyecto:

scrapy crawl books -o books.jsonl

Deberías ver que Scrapy registra una solicitud a la página 1, veintiocho elementos extraídos y, a continuación, un cierre limpio. Abre books.jsonl y comprueba que hay un objeto JSON por línea.

Hay algunas cosas que debes tener en cuenta. start_urls es el punto de entrada; el motor programa cada URL automáticamente. parse es la llamada de retorno predeterminada. response.urljoin resuelve una href con respecto a la página actual para que no te encuentres con enlaces rotos. El rating campo sigue conteniendo ruido como "star-rating Three", que es exactamente el tipo de limpieza que los cargadores de elementos se encargarán de realizar más adelante.

Nota de producción: ejecutarlo con -o está bien para una prueba rápida, pero nunca confíes en ello en un trabajo programado. Configura el FEEDS configuración en settings.py para que el destino de salida, el formato y el comportamiento de sobrescritura estén controlados por versiones. Lo conectaremos junto con un canal de datos de la base de datos en la sección de persistencia. Considera el indicador de la CLI como un atajo de desarrollo, no como un artefacto de implementación.

CSS frente a XPath: elegir selectores que no fallen

Ambos motores de selección vienen incluidos con Scrapy y ambos se ejecutan sobre el mismo árbol analizado. Utiliza el que sea más breve y claro para la tarea. Como regla general, CSS es mejor para consultas basadas en clases y estructurales, mientras que XPath es mejor cuando necesitas recorrer el árbol por contenido de texto, por hermanos o por antecesores.

# CSS: short, idiomatic, fast to write
response.css("article.product_pod p.price_color::text").get()

# XPath equivalent
response.xpath("//article[@class='product_pod']//p[@class='price_color']/text()").get()

XPath se gana su lugar cuando CSS no puede expresar lo que necesitas:

# "Find the <td> that follows the <th> whose text is 'Stock'"
response.xpath("//th[normalize-space()='Stock']/following-sibling::td/text()").get()

# "Find all links whose visible text contains 'Next'"
response.xpath("//a[contains(., 'Next')]/@href").getall()

Algunos hábitos que mantienen la estabilidad de los selectores: da preferencia a los selectores de atributos frente a los posicionales, que son frágiles (nth-child(3) que acabarán fallando), normaliza los espacios en blanco al comparar texto (normalize-space()), y combina .get() para una única coincidencia con .getall() para una lista, nunca indexes el resultado de .getall() a ciegas. Para una comparación más detallada de cuándo es adecuado cada motor, nuestra guía «Selectores XPath frente a CSS» es una buena lectura complementaria.

Nota de producción: cuando un selector devuelve None en producción pero funciona en el shell, es probable que la página se haya renderizado con JavaScript. Confírmalo con view(response) antes de culpar al selector.

Elementos y cargadores de elementos: patrones de limpieza reutilizables

Generar diccionarios simples está bien para diez líneas de código. A gran escala, se necesita un esquema tipado para que un error tipográfico en el nombre de un campo falle rápidamente en lugar de producir silenciosamente filas basura. Define un elemento en items.py:

import scrapy
from itemloaders.processors import MapCompose, TakeFirst, Join

def to_float(value):
    return float(value.replace("£", "").replace("$", "").strip())

def normalize_rating(value):
    # "star-rating Three" -> "Three"
    parts = value.split()
    return parts[1] if len(parts) > 1 else value

class ProductItem(scrapy.Item):
    title = scrapy.Field(input_processor=MapCompose(str.strip), output_processor=TakeFirst())
    price = scrapy.Field(input_processor=MapCompose(str.strip, to_float), output_processor=TakeFirst())
    rating = scrapy.Field(input_processor=MapCompose(normalize_rating), output_processor=TakeFirst())
    description = scrapy.Field(input_processor=MapCompose(str.strip), output_processor=Join(" "))

MapCompose transformadores de cadenas, TakeFirst colapsa una lista de coincidencias en un único valor y Join une varios párrafos en uno solo. Usa un cargador en la araña para que esta siga siendo legible:

from scrapy.loader import ItemLoader
from bookstore.items import ProductItem

def parse(self, response):
    for card in response.css("article.product_pod"):
        loader = ItemLoader(item=ProductItem(), selector=card)
        loader.add_css("title", "h3 a::attr(title)")
        loader.add_css("price", "p.price_color::text")
        loader.add_css("rating", "p.star-rating::attr(class)")
        yield loader.load_item()

La ventaja es la reutilización. Una vez to_float reside en items.py, cualquier elemento que contenga un precio en cualquier araña puede llamarlo. La lógica de limpieza deja de copiarse y pegarse en las llamadas de retorno.

Seguir enlaces: paginación manual frente a CrawlSpider

Hay dos formas habituales de rastrear varias páginas en Scrapy. Elige en función de lo predecible que sea la estructura de enlaces.

La paginación manual es la opción adecuada cuando hay un único enlace «siguiente» que seguir. Añade esto al final de parse:

next_page = response.css("li.next a::attr(href)").get()
if next_page:
    yield response.follow(next_page, callback=self.parse)

response.follow gestiona las URL relativas y reutiliza la misma llamada de retorno, que es exactamente lo que necesita la paginación de estilo catálogo. El rastreo se detiene de forma natural cuando el enlace «siguiente» desaparece en la página final.

CrawlSpider es la opción adecuada cuando se desea rastrear un sitio completo haciendo coincidir patrones de URL. Utiliza Rule y LinkExtractor para descubrir y seguir enlaces automáticamente:

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class BooksCrawl(CrawlSpider):
    name = "books_crawl"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/"]
    rules = (
        Rule(LinkExtractor(restrict_css=".pager a")),  # follow pagination
        Rule(LinkExtractor(restrict_css="h3 a"), callback="parse_book"),
    )

    def parse_book(self, response):
        yield {
            "title": response.css("h1::text").get(),
            "price": response.css("p.price_color::text").get(),
        }

La función integrada de Scrapy RFPDupeFilter garantiza que la misma URL no se añada dos veces a la cola, por lo que no es necesario que realices tú mismo un seguimiento de los enlaces visitados. Establece DEPTH_LIMIT en settings.py cuando rastrees un sitio con muchas subpáginas y quieras detener el rastreo de forma definitiva.

Nota de producción: para sitios compatibles con mapas de sitio, SitemapSpider es aún más sencillo. Lee /sitemap.xml directamente y te permite filtrar patrones de URL con sitemap_rules.

Persistencia de resultados: FEEDS y un pipeline de base de datos

El web scraping con Scrapy te ofrece dos capas de persistencia, y normalmente querrás ambas. La configuración de FEEDS gestiona las exportaciones estructuradas de forma gratuita, mientras que un pipeline se encarga de destinos personalizados como una base de datos relacional.

Configura los feeds en settings.py. Consulta la documentación de exportaciones de feeds de Scrapy para ver la sintaxis actual, pero una configuración moderna se parece más o menos a esto:

FEEDS = {
    "data/books.jsonl": {
        "format": "jsonlines",
        "encoding": "utf-8",
        "overwrite": True,
    },
    "data/books.csv.gz": {
        "format": "csv",
        "postprocessing": ["scrapy.extensions.postprocessing.GzipPlugin"],
    },
}

JSON Lines es la opción predeterminada adecuada: se puede transmitir, es fácil de añadir y se carga fácilmente en Pandas o en un almacén de datos. El CSV con gzip es adecuado para el traspaso a los analistas. Ambos fallan en las consultas relacionales, y ahí es donde entran en juego los pipelines.

Un pipeline SQLite que se ejecuta después de un validador:

# pipelines.py
import sqlite3
from itemadapter import ItemAdapter

class SqlitePipeline:
    def open_spider(self, spider):
        self.conn = sqlite3.connect("data/books.db")
        self.conn.execute(
            "CREATE TABLE IF NOT EXISTS products (title TEXT, price REAL, rating TEXT)"
        )

    def close_spider(self, spider):
        self.conn.commit()
        self.conn.close()

    def process_item(self, item, spider):
        a = ItemAdapter(item)
        self.conn.execute(
            "INSERT INTO products(title, price, rating) VALUES (?, ?, ?)",
            (a["title"], a["price"], a["rating"]),
        )
        return item

Regístrela con una prioridad. Los números más bajos se ejecutan antes, por lo que un validador con prioridad 100 se activa antes que el escritor de la base de datos con prioridad 200:

ITEM_PIPELINES = {
    "bookstore.pipelines.PriceRangeValidator": 100,
    "bookstore.pipelines.SqlitePipeline": 200,
}

Ahora los precios no válidos se descartan antes de llegar a la base de datos.

Reforzando settings.py: AutoThrottle, reintentos y almacenamiento en caché

La configuración predeterminada funciona en desarrollo, pero te bloqueará en producción. Las siguientes son las más importantes. Comprueba los valores predeterminados exactos con tu versión instalada de Scrapy.

# settings.py
ROBOTSTXT_OBEY = True            # respect the site's policy unless you have a contract
CONCURRENT_REQUESTS = 8          # global cap; lower for fragile sites
CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 0.5             # base delay; AutoThrottle adjusts dynamically

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 30.0

RETRY_ENABLED = True
RETRY_TIMES = 5
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 408, 522, 524]

HTTPCACHE_ENABLED = True         # huge time-saver during development
HTTPCACHE_EXPIRATION_SECS = 3600
HTTPCACHE_IGNORE_HTTP_CODES = [429, 500, 502, 503, 504]

AutoThrottle es la característica estrella aquí. En lugar de adivinar un DOWNLOAD_DELAY, le indicas una concurrencia objetivo y Scrapy reduce la velocidad cuando aumenta la latencia. Eso por sí solo evita la mayoría de las situaciones de DDoS accidentales en sitios lentos.

HTTPCACHE_ENABLED es una configuración que facilita el desarrollo: mientras iteras sobre selectores, las solicitudes idénticas se recuperan del disco, por lo que dejas de saturar el objetivo. Desactívala en producción.

Para una protección real contra los bots, la configuración por sí sola no es suficiente, y nuestra guía sobre por qué se bloquean los rastreadores aborda los patrones más profundos. En cualquier caso, la siguiente capa son los middlewares.

Middlewares de descarga: encabezados, proxies y API gestionadas

Cuando un sitio empieza a devolver 403s, la solución casi siempre está en un middleware de descarga. La estructura es sencilla:

# middlewares.py
import random

class RandomUserAgentMiddleware:
    UAS = [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/605.1.15 ...",
    ]
    def process_request(self, request, spider):
        request.headers["User-Agent"] = random.choice(self.UAS)

Regístralo en settings.py. Scrapy incluye una pila de middleware predeterminada ya configurada (más de diez habilitados de serie), y los números de prioridad del middleware suelen expresarse en un rango de enteros documentado. Las recomendaciones de la comunidad sitúan el middleware antibots personalizado antes del RetryMiddleware, cuya prioridad predeterminada es 550, para que los reintentos vean tu identidad rotada.

DOWNLOADER_MIDDLEWARES = {
    "bookstore.middlewares.RandomUserAgentMiddleware": 400,
    "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": None,  # disable default
}

Para la rotación de proxies, configura request.meta["proxy"] en process_request. Existen plugins de la comunidad tanto para la rotación de proxies como para los agentes de usuario aleatorios (y para el rastreo distribuido, el almacenamiento en caché persistente y la monitorización), pero comprueba el estado de mantenimiento actual de cada proyecto antes de depender de él en producción para el web scraping con Scrapy a cualquier escala seria.

La verdad a cambio: en algún momento, gestionar tus propios encabezados, direcciones IP residenciales y resolución de CAPTCHA se convierte en un proyecto paralelo. Ahí es donde una API de Scrapy gestionada encaja a la perfección. Implementa un middleware que reescriba request.url para que apunte al punto final de la API y añada tu clave de API como encabezado, y el resto de tu araña no cambiará.

Scrapy-Playwright: la vía de escape de JavaScript

Scrapy no ejecuta JavaScript por sí mismo, por lo que los sitios creados con Angular, React o cualquier marco de trabajo del lado del cliente devuelven el HTML del shell y no los datos que se ven en el navegador. La solución más limpia en 2026 para el web scraping con Scrapy en páginas dinámicas es scrapy-playwright, que cambia el descargador predeterminado por un Chromium sin interfaz gráfica real cuando se activa por solicitud.

Instálalo y comprueba la sintaxis actual de registro del controlador con el README de scrapy-playwright en el momento de la instalación:

# settings.py
DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

Activa las solicitudes configurando meta:

def start_requests(self):
    yield scrapy.Request(
        "https://example-spa.com/products",
        meta={
            "playwright": True,
            "playwright_page_methods": [
                ("wait_for_selector", "article.product"),
            ],
        },
    )

Marca solo las URL que realmente necesitan un navegador. Cada solicitud de Playwright es considerablemente más costosa que una simple recuperación de Scrapy, tanto en CPU como en latencia, por lo que una araña híbrida (HTML para listados, Playwright para detalles de productos) suele ser la opción adecuada. Si deseas una guía más detallada o una comparación con el backend Splash anterior, nuestro tutorial de Scrapy-Playwright cubre los patrones en detalle.

Registro, contratos y despliegue

El web scraping de nivel de producción con Scrapy necesita tres cosas que los tutoriales suelen omitir.

Registro. Configura LOG_LEVEL = "INFO" en settings.py para ejecuciones normales y "DEBUG" solo cuando haya algún problema. Canaliza los registros a un archivo con LOG_FILE o envíalos a un backend estructurado.

Contratos de rastreo. Añade contratos docstring a las devoluciones de llamada y ejecútalos scrapy check en CI. Un contrato típico fija la URL, los campos esperados y el recuento mínimo de elementos, de modo que un cambio silencioso en el sitio rompa la compilación en lugar del conjunto de datos.

def parse(self, response):
    """
    @url https://books.toscrape.com/
    @returns items 20 20
    @scrapes title price rating
    """

Programación e implementación. scrapyd ejecuta tu proyecto como un daemon de larga duración que puedes implementar a través de scrapyd-client. Para pilas basadas en contenedores, crea una imagen Docker ligera con tu proyecto y ejecútala scrapy crawl en una programación cron (o un CronJob de Kubernetes). En cualquier caso, guarda los resultados en un almacenamiento duradero, no en el sistema de archivos del contenedor.

Errores comunes y cómo depurarlos

  • Selectores vacíos. El selector funcionaba en el shell, pero devuelve None en la araña. Casi siempre se renderiza en JavaScript. Confírmalo con view(response) y cambia a scrapy-playwright para esa URL.
  • 403 y 429 tormentas. Tu huella digital es obvia. Añade un middleware de User-Agent aleatorio, reduce CONCURRENT_REQUESTS_PER_DOMAIN, sube AUTOTHROTTLE_START_DELAYy confirma RETRY_HTTP_CODES incluye 429.
  • Bucles de paginación infinitos. El selector «siguiente» también coincide en la última página. Ancla el selector a una clase CSS que desaparezca al final, o configura DEPTH_LIMIT.
  • Elementos eliminados silenciosamente. Se ha producido un error en el proceso DropItem y nunca te diste cuenta. Actualiza LOG_LEVEL a DEBUG, busca en el registro Dropped:y valida tus comprobaciones de rango.
  • Se cuelan URL duplicadas. RFPDupeFilter Las coincidencias se basan en la huella digital, por lo que pueden colarse URL que solo difieran en el orden de la cadena de consulta. Normaliza las URL antes de enviar las solicitudes.

Conclusiones clave

  • El web scraping con Scrapy vale la pena cuando necesitas programación, deduplicación, reintentos, limitación de velocidad y pipelines integrados de forma nativa, no cuando basta con un script de 20 líneas.
  • Asigna cada síntoma a una etapa del ciclo de vida: los bloqueos se producen en los middlewares del descargador, los elementos que faltan se encuentran en los pipelines y los errores del selector suelen apuntar al renderizado de JavaScript.
  • Cargadores de elementos con MapCompose, TakeFirst, y Join mantienen la lógica de limpieza reutilizable en todas las arañas, en lugar de copiarla y pegarla en las llamadas de retorno.
  • Persiste con FEEDS para formatos portátiles y un pipeline personalizado para el almacenamiento relacional. Utiliza ambos, con prioridades de pipeline que ordenen la validación antes del escritor de la base de datos.
  • Trata AutoThrottle, los códigos de reintento y una API de scraping gestionada como una defensa por niveles contra las prohibiciones. Recurre a scrapy-playwright solo cuando el HTML esté realmente vacío.

Preguntas frecuentes

¿Sigue mereciendo la pena aprender Scrapy en 2026 en comparación con las bibliotecas asíncronas más recientes?

Sí, para rastreos de más de unos pocos cientos de páginas. Las pilas asíncronas más recientes como httpx plus selectolax son estupendas para scripts puntuales, pero Scrapy incluye el programador, el filtro de duplicados, el middleware de reintentos, las señales y las exportaciones de feeds que, de otro modo, tendrías que escribir tú mismo. Para un rastreador de producción recurrente, ese diseño «todo incluido» sigue ganando en cuanto a costes de mantenimiento.

¿Puede Scrapy rastrear páginas renderizadas en JavaScript por sí solo, o necesito Playwright o Splash?

Por sí solo, no. Scrapy obtiene el HTML sin procesar y no ejecuta JavaScript, por lo que las aplicaciones de una sola página devuelven el marcado del shell. La mejor opción actual es scrapy-playwright, que sustituye el descargador por un Chromium sin interfaz gráfica real por cada solicitud. scrapy-splash sigue funcionando para algunos equipos, pero Playwright tiene una compatibilidad más amplia con navegadores y un mantenimiento activo.

¿Cómo se compara Scrapy con Beautiful Soup y Selenium para proyectos de diferentes tamaños?

Beautiful Soup es un analizador, no un rastreador, y combina bien con requests para pequeños rastreos estáticos. Selenium controla un navegador completo y es ideal para flujos interactivos con estado, como paneles de control con sesión iniciada. Scrapy se sitúa entre ambos: un marco de rastreo de alto rendimiento para cientos o millones de páginas, con renderizado del navegador integrado mediante scrapy-playwright cuando sea necesario.

¿Cómo implemento una araña de Scrapy para que se ejecute de forma programada en producción?

Tres patrones habituales. Ejecuta scrapyd como un daemon y activa los trabajos a través de su API HTTP. Crea una imagen de Docker con tu proyecto y programa scrapy crawl <name> a través de cron o un CronJob de Kubernetes. O utiliza una plataforma de scraping gestionada que aloje las arañas por ti. En todos los casos, guarda los resultados en un almacenamiento duradero como S3 o una base de datos, nunca en el sistema de archivos de un contenedor.

¿Cómo evito que mi araña de Scrapy sea bloqueada o que su IP sea prohibida?

Aplica varias capas de defensa. Habilita AutoThrottley aleatoriza User-Agent los encabezados mediante un middleware de descarga, incluye 429 en RETRY_HTTP_CODESy reduce CONCURRENT_REQUESTS_PER_DOMAIN. Para sitios más difíciles, redirige las solicitudes a través de proxies residenciales o una API de scraper gestionada que se encargue de la rotación y la resolución de CAPTCHAs detrás de un único punto de acceso. Respeta robots.txt y los límites de velocidad siempre que sea posible.

Conclusión

El objetivo del web scraping con Scrapy no es que escribas menos código que con requests más BeautifulSoup. Normalmente escribes más el primer día. El objetivo es que el código que escribes el primer día siga funcionando el nonagésimo día, porque el motor, el programador, el filtro de duplicados, el limitador, la capa de reintentos y el contrato de canalización no cambian por debajo de ti. Te haces con una base estable y, a continuación, especializas las arañas, los elementos y los middlewares para cada sitio de destino.

Si hay algo que debes interiorizar de esta guía, que sea el ciclo de vida de la solicitud y la respuesta. Todos los errores de Scrapy con los que te encontrarás se producen en una etapa específica de ese bucle, y nombrar la etapa es la mitad de la solución. Los selectores fallan en la llamada de retorno. Los elementos desaparecen en el pipeline. Las prohibiciones se producen en el descargador. La paginación se repite indefinidamente en tu lógica de llamada de retorno. Relaciona el síntoma con la etapa y la solución se hará evidente.

Cuando la presión anti-bot supere lo que puedes construir en middlewares.py, ese es el momento adecuado para descargar la capa de solicitudes. En WebScrapingAPI hemos creado Scraper API precisamente para ese traspaso: mantén tus arañas de Scrapy, tus elementos y tus pipelines, y deja que un punto final gestionado se ocupe de los proxies, la resolución de CAPTCHA y la renderización de JavaScript. Tu araña sigue siendo Scrapy. Los obstáculos pasan a ser problema de otra persona.

Acerca del autor
Mihai Maxim, Desarrollador Full Stack @ WebScrapingAPI
Mihai MaximDesarrollador Full Stack

Mihai Maxim es desarrollador full stack en WebScrapingAPI, donde colabora en todas las áreas del producto y ayuda a crear herramientas y funciones fiables para la plataforma.

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.