Screaming Frog es el crawler preferido para la mayoría de los SEOs, pero probablemente ya has encontrado sus límites: el tope de 500 URLs en la versión gratuita, la RAM al máximo en sitios grandes, o querer automatizar rastreos sin vigilar una GUI. Scrapy es el framework de Python de código abierto que elimina esos límites.

Si puedes ejecutar npm install o git clone, puedes ejecutar Scrapy. La curva de aprendizaje es real pero manejable, especialmente si ya te estás familiarizando con herramientas CLI a través de flujos de trabajo de codificación agéntica.

¿Por qué Scrapy?

Beneficios Clave

Screaming Frog funciona bien para auditorías rápidas. Pero tiene límites:

Limitación Impacto
Límite gratuito de 500 URLs Requiere licencia de $259/año para sitios más grandes
Consume mucha memoria Rastreos grandes pueden consumir 8GB+ de RAM
Dependiente de GUI Difícil de automatizar o programar
Personalización limitada Las opciones de configuración son fijas

Scrapy resuelve estos problemas:

Scrapy Lo que obtienes
Gratuito y de código abierto Sin límites de URLs, sin tarifas de licencia
Menor huella de memoria Colas respaldadas en disco mantienen la RAM controlada
Nativo de CLI Scripteable, programable con cron, listo para CI/CD
Personalización completa con Python Extrae lo que necesitas, filtra como quieras
Pausar/Reanudar Detén y continúa rastreos grandes en cualquier momento
Scrapy no reemplazará a Screaming Frog para todo. Las auditorías rápidas siguen siendo más rápidas en una GUI. Pero para rastreos a gran escala, automatización y extracción personalizada, vale la pena tenerlo en tu kit de herramientas.

Instalación

Configuración

Scrapy funciona con Python. Usa un entorno virtual para mantener todo limpio:

Debian/Ubuntu:

sudo apt install python3.11-venv
python3 -m venv venv
source venv/bin/activate
pip install scrapy

macOS:

python3 -m venv venv
source venv/bin/activate
pip install scrapy

Windows:

python -m venv venv
venv\Scripts\activate
pip install scrapy
Los Entornos Virtuales Importan
Siempre usa un venv. Instalar globalmente causa conflictos de dependencias y rompe la reproducibilidad.

Crear un Proyecto

Con Scrapy instalado:

scrapy startproject myproject
cd myproject
scrapy genspider sitename example.com

Esto crea:

myproject/
    scrapy.cfg
    myproject/
        __init__.py
        items.py
        middlewares.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            sitename.py

El código del spider va en spiders/sitename.py. La configuración está en settings.py.

Configuración para Rastreo Educado

Crítico

Configura settings.py antes de ejecutar cualquier cosa. Ser bloqueado desperdicia más tiempo que rastrear lentamente.

# Rastreo educado
CONCURRENT_REQUESTS_PER_DOMAIN = 5
DOWNLOAD_DELAY = 1
ROBOTSTXT_OBEY = True

# AutoThrottle - ajusta velocidad según respuesta del servidor
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = True

# Límites de seguridad
CLOSESPIDER_PAGECOUNT = 10000

# Salida
FEED_EXPORT_ENCODING = "utf-8"
Con AutoThrottle habilitado, 5 solicitudes concurrentes es un punto de partida razonable. AutoThrottle reducirá automáticamente si el servidor tiene problemas. Sin AutoThrottle, comienza más bajo con 1-3.

AutoThrottle

AutoThrottle monitorea los tiempos de respuesta del servidor y ajusta la velocidad de rastreo automáticamente:

  • Respuestas rápidas → acelera
  • Respuestas lentas → reduce velocidad
  • Errores/timeouts → reduce significativamente

A diferencia de los retrasos fijos de Screaming Frog, se adapta a las condiciones reales del servidor.

Manejo de Códigos de Estado

Por defecto, el HttpErrorMiddleware de Scrapy descarta silenciosamente las respuestas no-2xx. Esto significa que 404s, 301s, 500s son descartados antes de llegar a tu callback. Tu rastreo podría mostrar 100% códigos de estado 200, no porque el sitio sea perfecto, sino porque los errores están siendo filtrados.

Añade esto a tu clase spider para capturar todos los códigos de estado:

handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]

Screaming Frog captura todos los códigos de estado por defecto. Esta configuración alinea Scrapy con ese comportamiento.

Rendimiento en el Mundo Real

Benchmarks

Números reales de un rastreo de prueba con 5 solicitudes concurrentes y AutoThrottle habilitado:

Progreso del Rastreo Páginas/Minuto Notas
0-200 páginas 14-22 Arranque
200-500 páginas 10-12 Estabilizando
500-1,000 páginas 7-10 AutoThrottle ajustando
1,000+ páginas 5-7 Estado estable
Velocidad vs. Fiabilidad
Estas velocidades parecen lentas. Ese es el punto. AutoThrottle prioriza la salud del servidor sobre la velocidad cruda. Ser bloqueado y reiniciar desperdicia más tiempo que un rastreo metódico.

Comparación de Características

Característica Screaming Frog Scrapy
Costo Gratis <500 URLs, ~$259/año Gratis, código abierto
Tamaño máx. de rastreo Limitado por memoria Colas respaldadas en disco
Personalización Opciones de config limitadas Código Python completo
Programación Manual o terceros CLI nativo, programable con cron
Pausar/Reanudar Sí (con JOBDIR)
Curva de aprendizaje Baja (GUI) Media (código)
Limitación de velocidad Retrasos fijos básicos AutoThrottle (adaptativo)
Renderizado JavaScript Opcional (Chrome) Opcional (playwright/splash)
Códigos de estado Todos por defecto Requiere configuración
Filtrado de subdominios Casillas GUI Código (regex flexible)
Formatos de exportación CSV, Excel, etc. JSON, CSV, XML, personalizado
Integración CI/CD Difícil Nativo

Filtrado de URLs

Control Preciso

Screaming Frog usa casillas. Scrapy usa código. El intercambio es curva de aprendizaje por precisión.

Excluyendo rutas internacionales:

import re
from urllib.parse import urlparse

class MySiteSpider(scrapy.Spider):
    name = "mysite"
    allowed_domains = ["example.com", "www.example.com"]
    start_urls = ["https://www.example.com/"]

    # Omitir rutas internacionales como /uk/, /fr/, /de/
    EXCLUDED_PATTERNS = re.compile(
        r"/(in|au|th|es|hk|sg|ph|my|ca|cn|uk|kr|id|fr|vn|de|jp|nl|it|tw)/"
    )

    def filter_links(self, links):
        filtered = []
        for link in links:
            hostname = urlparse(link.url).hostname or ""
            if hostname not in ("example.com", "www.example.com"):
                continue
            if self.EXCLUDED_PATTERNS.search(link.url):
                continue
            filtered.append(link)
        return filtered

Puedes filtrar por patrones de URL, parámetros de consulta, encabezados de respuesta, contenido de página o cualquier combinación.

Pausar y Reanudar

Esencial

Para rastreos de más de 1,000 páginas, habilita pausar/reanudar con JOBDIR:

scrapy crawl myspider -o output.json -s JOBDIR=crawl_state

Scrapy guarda el estado en crawl_state/. Presiona Ctrl+C para pausar. Ejecuta el mismo comando para reanudar.

Siempre usa JOBDIR para rastreos de producción. Protege contra problemas de red, reinicios del sistema, o simplemente necesitar parar por el día.

El estado incluye URLs pendientes, URLs vistas y la cola de solicitudes. Esto es más robusto que la función de guardar/cargar de Screaming Frog porque está basado en archivos y sobrevive a reinicios del sistema.

Renderizado JavaScript

Scrapy obtiene solo HTML crudo. No renderiza JavaScript. Esto es lo mismo que devuelve curl.

Para la mayoría de los rastreos SEO, esto está bien:

  • Las meta tags, canonicals y h1s suelen estar en el HTML inicial
  • Los motores de búsqueda indexan principalmente contenido renderizado del servidor
  • La mayoría de los sitios de e-commerce y contenido son renderizados del servidor

Si tu sitio objetivo renderiza contenido del lado del cliente, tienes opciones:

Paquete Notas
scrapy-playwright Usa Chromium/Firefox/WebKit. Recomendado para sitios JS modernos
scrapy-splash Ligero, renderizador basado en Docker
scrapy-selenium Enfoque antiguo, aún funciona

El renderizado JS es significativamente más lento y consume más recursos. Solo añádelo si el sitio lo requiere.

Screaming Frog tiene un compromiso similar. Habilitar el renderizado JavaScript usa Chrome bajo el capó y ralentiza considerablemente los rastreos.

Gestión de Memoria

A ~1,300 páginas con extracción completa de campos:

  • Memoria: ~265 MB
  • CPU: ~4%

Usar JOBDIR mueve las colas de solicitudes a disco, manteniendo la memoria baja. Para rastreos muy grandes (100k+ URLs), añade estas configuraciones:

MEMUSAGE_LIMIT_MB = 1024
MEMUSAGE_WARNING_MB = 800
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'

Esto limita el uso de memoria y fuerza colas respaldadas en disco para el scheduler.

Datos de Salida

Personalizable

Salida básica del spider:

{
    "url": "https://www.example.com/page/",
    "title": "Título de la Página Aquí",
    "status": 200
}

Para rastreos SEO, querrás campos similares a lo que exporta Screaming Frog:

def parse_page(self, response):
    yield {
        "url": response.url,
        "status": response.status,
        "title": response.css("title::text").get(),
        "meta_description": response.css("meta[name='description']::attr(content)").get(),
        "meta_robots": response.css("meta[name='robots']::attr(content)").get(),
        "h1": response.css("h1::text").get(),
        "canonical": response.css("link[rel='canonical']::attr(href)").get(),
        "og_title": response.css("meta[property='og:title']::attr(content)").get(),
        "og_description": response.css("meta[property='og:description']::attr(content)").get(),
        "word_count": len(response.text.split()) if response.status == 200 else None,
        "content_type": response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore"),
    }

Añade o elimina campos según lo que necesites. Los selectores CSS funcionan para cualquier elemento en la página.

Formatos de exportación: JSON (-o output.json), JSON Lines (-o output.jsonl), CSV (-o output.csv), XML (-o output.xml).

JSON Lines es mejor para rastreos grandes. Los archivos son válidos línea por línea durante el rastreo, así que puedes monitorear con tail -f. El JSON estándar no es válido hasta que el rastreo se completa.

Screaming Frog → Scrapy

Guía de Traducción

Mapeo de flujos de trabajo de SF a Scrapy:

Acción de Screaming Frog Equivalente en Scrapy
Iniciar nuevo rastreo scrapy crawl spidername
Establecer retraso de rastreo DOWNLOAD_DELAY en configuración
Limitar hilos concurrentes CONCURRENT_REQUESTS_PER_DOMAIN
Respetar robots.txt ROBOTSTXT_OBEY = True
Exportar a CSV -o output.csv
Guardar/Cargar rastreo -s JOBDIR=crawl_state
Filtrar subdominios Código en spider (regex)
Extracción personalizada Selectores CSS/XPath en parse()

Cambios de mentalidad:

  1. La configuración es código. Edita settings.py en lugar de hacer clic en casillas.
  2. La extracción es explícita. Tú escribes qué datos capturar.
  3. La programación es nativa. Añade comandos a cron o CI/CD.
  4. El debugging son logs. Habilita AUTOTHROTTLE_DEBUG para ver qué está pasando.

Flujo de Trabajo Completo

Con la configuración estándar anterior, puedes tener Scrapy instalado y rastreando en menos de 15 minutos:

python3 -m venv venv
source venv/bin/activate  # venv\Scripts\activate en Windows
pip install scrapy
scrapy startproject urlcrawler
cd urlcrawler
scrapy genspider mysite example.com
# Edita settings.py con configuración de rastreo educado
# Edita spiders/mysite.py con tu lógica de parse
scrapy crawl mysite -o urls.jsonl -s JOBDIR=crawl_state

Scrapy Shell

Mientras construyes configuraciones personalizadas, usa Scrapy Shell para probar tus selectores y configuraciones interactivamente:

scrapy shell "https://example.com"

Esto abre una consola interactiva de Python con la respuesta ya cargada. Prueba selectores CSS y XPath en tiempo real antes de añadirlos a tu spider:

>>> response.css('title::text').get()
'Example Domain'
>>> response.xpath('//h1/text()').get()
'Example Domain'

Scrapy Shell reduce significativamente el tiempo de iteración. Valida la lógica de extracción sin ejecutar rastreos completos.

Plantilla Completa de Spider

Un spider listo para producción con filtrado de URLs, manejo de códigos de estado y extracción completa de campos SEO:

import re
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from urllib.parse import urlparse


class SEOSpider(CrawlSpider):
    name = "seospider"
    allowed_domains = ["example.com"]
    start_urls = ["https://www.example.com"]

    # Capturar todos los códigos de estado HTTP, no solo 2xx
    handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]

    # Patrones de URL a excluir
    EXCLUDED_PATTERNS = re.compile(
        r"/(in|au|th|es|hk|sg|ph|my|ca|cn|uk|kr|id|fr|vn|de|jp|nl|it|tw)/"
    )

    rules = (
        Rule(
            LinkExtractor(allow=()),
            callback="parse_page",
            follow=True,
            process_links="filter_links",
        ),
    )

    def filter_links(self, links):
        filtered = []
        for link in links:
            parsed = urlparse(link.url)
            hostname = parsed.hostname or ""

            if hostname not in ("example.com", "www.example.com"):
                continue

            if self.EXCLUDED_PATTERNS.search(link.url):
                continue

            filtered.append(link)
        return filtered

    def parse_page(self, response):
        yield {
            "url": response.url,
            "status": response.status,
            "title": response.css("title::text").get(),
            "meta_description": response.css("meta[name='description']::attr(content)").get(),
            "meta_robots": response.css("meta[name='robots']::attr(content)").get(),
            "h1": response.css("h1::text").get(),
            "canonical": response.css("link[rel='canonical']::attr(href)").get(),
            "og_title": response.css("meta[property='og:title']::attr(content)").get(),
            "og_description": response.css("meta[property='og:description']::attr(content)").get(),
            "word_count": len(response.text.split()) if response.status == 200 else None,
            "content_type": response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore"),
        }

Reemplaza example.com con tu dominio objetivo. Ajusta EXCLUDED_PATTERNS para la estructura de URLs de tu sitio.

Cuándo Usar Cuál

Screaming Frog:

Scrapy:

  • Sitios de más de 10,000 URLs
  • Rastreos automatizados y programados
  • Necesidades de extracción personalizada
  • Integración CI/CD
  • Restricciones de memoria
  • Configuraciones con control de versiones

La Conclusión

Scrapy tiene una curva de configuración más pronunciada que Screaming Frog, pero elimina los límites prácticos que imponen los crawlers GUI. Sin límites de URLs, sin tarifas de licencia, menor uso de memoria y automatización nativa.

Empieza pequeño. Rastrea un sitio que conozcas. Usa configuraciones conservadoras. Compara la salida con Screaming Frog. Los datos coincidirán, pero tendrás una herramienta que escala.