Implementación de Comandos Personalizados y Ejecución Programática en Scrapy

Ejecución de Spiders mediante Scripts

Aunque la interfaz de línea de comandos (CLI) es el método estándar para interactuar con Scrapy, existen escenarios donde es necesario iniciar el proceso de rastreo directamente desde un script de Python. Esto es particularmente útil para integrar el scraping en aplicaciones más grandes o tareas automatizadas.

Uso de CrawlerProcess

La clase CrawlerProcess gestiona internamente el reactor de Twisted, lo que la hace ideal para scripts independientes donde Scrapy es el proceso principle. Permite configurar parámetros de ejecución y lanzar múltiples spiders de forma concurrente.

from scrapy.crawler import CrawlerProcess
from scrapy.spiders import Spider

class DataExtractor(Spider):
    name = "extractor"
    start_urls = ['http://example.com']
    # Lógica de parseo...

engine = CrawlerProcess(settings={
    'LOG_LEVEL': 'WARNING',
    'FEEDS': {
        'results.json': {'format': 'json', 'encoding': 'utf8'}
    }
})

# Es posible agregar múltiples spiders a la cola de ejecución
engine.crawl(DataExtractor)
engine.start()

Control Avanzado con CrawlerRunner

Cuando se necesita un control más granular sobre el ciclo de vida del reactor de Twisted (por ejemplo, si Twisted ya está en uso por otra parte de la aplicación), se utiliza CrawlerRunner. Este componente no inicia ni detiene el reactor por sí mismo.

from twisted.internet import reactor
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.spiders import Spider

class ProductSpider(Spider):
    name = "products"

configure_logging({'LOG_FORMAT': '%(asctime)s [%(name)s] %(levelname)s: %(message)s'})
execution_engine = CrawlerRunner()

# El método crawl devuelve un objeto Deferred
deferred_task = execution_engine.crawl(ProductSpider)
deferred_task.addBoth(lambda _: reactor.stop())

reactor.run()

Ejecución Secuencial

Para escenarios donde los spiders deben ejecutarse uno tras otro en lugar de concurrentemente, se pueden encadenar los objetos Deferred utilizando el decorador @defer.inlineCallbacks.

from twisted.internet import reactor, defer
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.spiders import Spider

class InitialPhaseSpider(Spider):
    name = "phase_one"

class FinalPhaseSpider(Spider):
    name = "phase_two"

configure_logging()
sequential_runner = CrawlerRunner()

@defer.inlineCallbacks
def execute_pipeline():
    yield sequential_runner.crawl(InitialPhaseSpider)
    yield sequential_runner.crawl(FinalPhaseSpider)
    reactor.stop()

execute_pipeline()
reactor.run()

Desarrollo de Comandos Personalizados

Scrapy permite extender su CLI definiendo nuevos comandos. El framework localiza estos comandos a través de la variable COMMANDS_MODULE en el archivo settings.py. Por ejemplo, si se establece COMMANDS_MODULE = 'myproject.custom_cli', Scrapy buscará módulos dentro del directorio custom_cli que hereden de la clase base correspondiente.

Comando para Ejecución Masiva

Un caso de uso común es la necesidad de lanzar todos los spiders registrados en un proyecto con una sola instrucción. A continuación, se muestra cómo implementar un comando runall:

from scrapy.commands import ScrapyCommand

class ExecuteAllCommand(ScrapyCommand):
    requires_project = True

    def syntax(self):
        return '[opciones]'

    def short_desc(self):
        return 'Inicia la ejecución de todos los spiders del proyecto simultáneamente'

    def run(self, args, opts):
        available_spiders = self.crawler_process.spiders.list()
        
        for spider_name in available_spiders:
            self.crawler_process.crawl(spider_name, **opts.__dict__)
            
        self.crawler_process.start()

Una vez ubicado este archivo dentro del directorio configurado, el comando estará disponible como scrapy runall.

Caso de Uso: Generación y Ejecución Dinámica Basada en Configuración

En entornos donde usuarios sin conocimientos de programación deben definir reglas de extracción (como expresiones XPath), se puede diseñar un comando personalizado que lea un archivo de configuración externo, genere el código del spider utilizando plantillas y lo ejecute al instante.

Esta lógica combina el comportamiento de los comandos nativos genspider (generación de código) y runspider (ejecución directa). Se utiliza el módulo string.Template para la interpolación de variables y importlib para la carga dinámica.

import os
import sys
import string
import logging
from importlib import import_module
from scrapy.commands import ScrapyCommand
from scrapy.exceptions import UsageError
from scrapy.utils.spider import iter_spider_classes

logger = logging.getLogger(__name__)

def generate_spider_code(config_data, target_name):
    template_str = """
import scrapy

class DynamicSpider(scrapy.Spider):
    name = "$spider_name"
    start_urls = ['$start_url']

    def parse(self, response):
        yield {'extracted_data': response.xpath('$xpath_rule').getall()}
"""
    template = string.Template(template_str)
    return template.substitute({
        'spider_name': target_name,
        'start_url': config_data.get('url', 'http://example.com'),
        'xpath_rule': config_data.get('xpath', '//title/text()')
    })

class DynamicRunCommand(ScrapyCommand):
    requires_project = True

    def syntax(self):
        return "<archivo_configuracion.py>"

    def short_desc(self):
        return "Genera y ejecuta un spider a partir de un diccionario de reglas"

    def run(self, args, opts):
        if len(args) != 1:
            raise UsageError("Debe proporcionar un archivo de configuración como argumento.")
        
        config_path = args[0]
        if not os.path.exists(config_path):
            raise UsageError(f"No se encontró el archivo: {config_path}")

        config_dir = os.path.dirname(os.path.abspath(config_path))
        sys.path.insert(0, config_dir)
        
        module_name = os.path.splitext(os.path.basename(config_path))[0]
        
        try:
            config_module = import_module(module_name)
            
            # Generación del código (en un caso real, esto se escribiría en un archivo temporal o en memoria)
            spider_code = generate_spider_code(config_module.RULES, module_name)
            
            # Simulación de carga del módulo generado
            # spider_module = import_module(f"{module_name}_generated")
            
            # Para este ejemplo, asumimos que el módulo importado contiene directamente la clase
            spider_classes = list(iter_spider_classes(config_module))
            
            if not spider_classes:
                raise UsageError("El módulo no contiene una definición de Spider válida.")
            
            target_spider_cls = spider_classes[0]
            
            self.crawler_process.crawl(target_spider_cls, **opts.__dict__)
            self.crawler_process.start()
            
            if self.crawler_process.bootstrap_failed:
                self.exitcode = 1
                
        except Exception as e:
            logger.error(f"Error al procesar la configuración dinámica: {e}")
            self.exitcode = 1
        finally:
            if config_dir in sys.path:
                sys.path.remove(config_dir)
</archivo_configuracion.py>

El núcleo de la ejecución dinámica radica en importar el módulo objetivo, extraer las clases que heredan de scrapy.Spider utilizando iter_spider_classes, y finalmente inyectar la clase resultante en el método crawl del crawler_process.

Etiquetas: scrapy Python twisted web-scraping metaprogramming

Publicado el 6-22 16:48