Tabla de Contenidos
Proyecto Scrapy para extraer las conferencias europython
Módulo perteneciente al curso Python avanzado para proyectos de seguridad
En esta sección, vamos a construir un proyecto con Scrapy que nos permite extraer los datos de las sesiones de la conferencia EuroPython siguiendo el patrón de la siguiente URL:
http://ep{year}.europython.eu/en/events/sessions
Podríamos probar con los años de 2016 a 2020:
Creación proyecto europython
La forma estándar de comenzar a trabajar con Scrapy es crear un proyecto, que se realiza con el comando startproject:
$ scrapy startproject europython
De esta forma, se creará la carpeta del proyecto con la siguiente estructura, básicamente los archivos son:
scrapy.cfg: fichero de configuración del proyecto.europython/: módulo de Python de nuestro proyecto donde incluiremos nuestro código.europython/items.py: archivo donde definimos los campos que queremos extraer.europython/pipelines.py: definimos los pipelines del proyecto.europython/settings.py: fichero de configuración.europython/spiders/: directorio donde encontramos los spiders.
En esta captura de pantalla podemos ver el resultado de crear un proyecto Scrapy:
Ficheros proyecto scrapy
Los items son contenedores que cargaremos con los datos que queremos extraer. Como nuestros items contendrán los datos relacionados con el título y la descripción, definiremos estos atributos en el archivo items.py. En la clase EuropythonItem, que se crea de forma predeterminada, añadimos el nombre de los elementos que usaremos, por ejemplo title, la forma más sencilla es instanciar objetos de la clase scrapy.Field.
Este es el contenido del archivo items.py donde definimos los campos y la información que vamos a extraer:
import scrapy from scrapy.loader.processors import Compose, MapCompose, Join clean_text = Compose(MapCompose(lambda v: v.strip()), Join()) def custom_field(text): text = clean_text(text) return text.strip() class EuropythonItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() title = scrapy.Field(output_processor=custom_field) author = scrapy.Field(output_processor=custom_field) description = scrapy.Field(output_processor=custom_field) date = scrapy.Field(output_processor=custom_field) tags = scrapy.Field(output_processor=custom_field)
La función custom_field() está utilizando para el formato de las cadenas de texto donde el método strip() nos permite eliminar cualquier espacio al principio y al final para cada uno de los campos y se aplicará automáticamente a todos los elementos que indicamos cuando los instanciamos.
Spyder eurpython
Los spiders son clases escritas por el usuario para extraer información de un dominio (o un grupo de dominios). Se definen como una lista inicial de URLs, para posteriormente definir la lógica necesaria para seguir los enlaces y analizar el contenido de esas páginas para extraer elementos.
Este spyder tendrá un método constructor init para inicializar el spyder, la url de la que queremos extraer los datos y un parámetro adicional que indica el año del que queremos extraer la información.
En el archivo europython_spider.py definimos la clase EuropythonSpider. En esta clase se define el spider que a partir de la url de inicio rastreará los enlaces que va encontrando en función del patrón indicado, y para cada entrada obtendrá los datos correspondientes a cada sesión (título, autor, descripción, fecha, tags).
Puede encontrar el siguiente código en el archivo europython_spider.py:
#!/usr/bin/env python3 import scrapy from scrapy.spiders import CrawlSpider, Rule from scrapy.linkextractors import LinkExtractor from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor from scrapy.loader import ItemLoader from europython.items import EuropythonItem class EuropythonSpider(CrawlSpider): def __init__(self, year='', *args, **kwargs): super(EuropythonSpider, self).__init__(*args, **kwargs) self.year = year self.start_urls = ['http://ep'+str(self.year)+".europython.eu/en/events/sessions"] print('start url: '+str(self.start_urls[0])) name = "europython_spider" allowed_domains = ["ep2016.europython.eu", "ep2017.europython.eu","ep2018.europython.eu","ep2019.europython.eu","ep2020.europython.eu"] # Pattern for entries that match the conference/talks and /talks format rules = [Rule(LxmlLinkExtractor(allow=['conference/talks']),callback='process_response2016_17_18'), Rule(LxmlLinkExtractor(allow=['talks']),callback='process_response_europython2019_20')] def process_response2016_17_18(self, response): itemLoader = ItemLoader(item=EuropythonItem(), response=response) itemLoader.add_xpath('title', "//div[contains(@class, 'grid-100')]//h1/text()") itemLoader.add_xpath('author', "//div[contains(@class, 'talk-speakers')]//a[1]/text()") itemLoader.add_xpath('description', "//div[contains(@class, 'cms')]//p//text()") itemLoader.add_xpath('date', "//section[contains(@class, 'talk when')]/strong/text()") itemLoader.add_xpath('tags', "//div[contains(@class, 'all-tags')]/span/text()") item = itemLoader.load_item() return item def process_response_europython2019_20(self, response): item = EuropythonItem() item['title'] = response.xpath("//*[@id='talk_page']/div/div/div[1]/h1/text()").extract() item['author'] = response.xpath("//*[@id='talk_page']/div/div/div[1]/h5/a/text()").extract() item['description'] = response.xpath("//*[@id='talk_page']/div/div/div[1]/p[3]/text()").extract() item['date'] = "July "+self.year item['tags'] = response.xpath("//span[contains(@class, 'badge badge-secondary')]/text()").extract() return item
Funcionamiento del spyder
Las variables más significativas de nuestro spyder son:
name: nombre del spyder.allowed_domains: array con los dominios permitidos.start_urls: array con las URLs a través de las cuales el spyder comienza a extraer datos.rules: reglas para la extracción de enlaces (estos enlaces también serán visitados por la araña) de esta manera podemos hacer una búsqueda recursiva.process_response: método que se ejecuta cada vez que se realiza una petición a una url. (La regla de extracción de enlaces se pasa como un parámetro).
Las reglas están definidas por objetos del tipo Rule, que reciben como parámetro un objeto extractor de enlaces LinkExtractor donde allow es una expresión regular con la que los enlaces deben coincidir, y una función de callback que se pasa como un parámetro que se ejecutará cada vez que se extraiga un enlace y se realiza una solicitud a la URL de este enlace.
Para obtener más información sobre las reglas, puede visitar la documentación oficial en http://doc.scrapy.org/en/latest/topics/spiders.html#scrapy.contrib.spiders.Rule
En el spyder definimos también los métodos process_response2016_17_18() y process_response_europython2019_20() para extraer cada uno de los campos. Si en la web visitamos la vista detallada de cualquiera de las charlas, podemos identificar la expresión xpath necesaria para extraer el título, autor, descripción y etiquetas de cada una de las conferencias.
Obtener expresiones XPath
Para extraer la información que nos interesa a partir del código html utilizaremos expresiones xpath, que podemos obtener haciendo click derecho en el navegador y seleccionando la opción de inspección. En este caso, estamos interesados en extraer el título, autor, descripción y etiquetas de una conferencia específica.
Podríamos utilizar scrapy shell para obtener las expresiones xpath necesarias para extraer dicha información:
>>> fetch('https://ep2019.europython.eu/talks/KNhQYeQ-downloading-a-billion-files-in-python/') [scrapy.core.engine] INFO: Spider opened [scrapy.core.engine] DEBUG: Crawled (200) <GET https://ep2019.europython.eu/talks/KNhQYeQ-downloading-a-billion-files-in-python/> (referer: None) >>> response.xpath("//*[@id='talk_page']/div/div/div[1]/h1/text()").extract() ['Downloading a Billion Files in Python'] >>> response.xpath("//*[@id='talk_page']/div/div/div[1]/h5/a/text()").extract() ['James Saryerwinnie'] >>> response.xpath("//*[@id='talk_page']/div/div/div[1]/p[3]/text()").extract() ["You've been given a task. You need to download some files from a server to your local machine. The files are fairly small, and you can list and access these files from the remote server through a REST API. You'd like to download them as fast as possible. The catch? There's a billion of them. Yes, one billion files."] >>> response.xpath("//span[contains(@class, 'badge badge-secondary')]/text()"). extract() ['ASYNC / Concurreny', 'Case Study', 'Multi-Processing', 'Multi-Threading', 'Performance']
Ejecutando el spyder Europython
Podemos ejecutar nuestro spyder con el siguiente comando:
$ scrapy crawl europython_sypder -o europython_items.json -t json
Donde los últimos parámetros indican que los datos extraídos se almacenan en un fichero llamado europython_items.json y que use el exportador para formato JSON.
Otra opción interesante es que los spyders pueden administrar los argumentos que se pasan en el comando de rastreo utilizando la opción -a. Por ejemplo, el siguiente comando extraerá los datos de las sesiones EuroPython del año 2018:
$ scrapy crawl europython_spider -a year=2018 -o europython_items.json -t json
De la misma forma podríamos proceder con las sesiones del año 2019:
$ scrapy crawl europython_spider -a year=2019 -o europython_items.json -t json
En esta captura de pantalla, podemos ver el archivo JSON generado después de la ejecución del comando anterior donde podemos ver las sesiones que se han obtenido:
Pipelines proyecto europython
De esta forma los archivos europython_items.csv, europython_items.json y europython_items.xml se crearán automáticamente.
¿Qué sucede si queremos separar la información o validar algunos campos antes de guardar los registros?
Para esos casos podemos hacer uso de los pipelines. Los pipelines permiten tratar la información extraída, como por ejemplo almacenar la información en otro recurso como por ejemplo una base de datos.
Para ello, primero necesitamos habilitar el uso de pipelines en el fichero settings.py. Este paso consiste en añadir una línea que indique la clase donde se definirán las reglas para el pipeline definido, en este caso, estamos definiendo 4 pipelines.
ITEM_PIPELINES = { 'europython.pipelines.EuropythonJsonExport': 100, 'europython.pipelines.EuropythonXmlExport': 200, 'europython.pipelines.EuropythonCSVExport': 300, 'europython.pipelines.EuropythonSQLitePipeline': 400 }
Fichero pipelines.py
En el fichero pipelines.py definimos la clases que procesará los resultados y los guardan en diferentes formatos.
La clase EuropythonJsonExport exportará los datos a formato json:
class EuropythonJsonExport(object): def __init__(self): self.file = codecs.open('europython_items.json', 'w+b', encoding='utf-8') def process_item(self, item, spider): line = json.dumps(dict(item), ensure_ascii=False) + "\n" self.file.write(line) return item def spider_closed(self, spider): self.file.close()
La clase EuropythonXmlExport exportará los datos a formato xml. En esta clase estamos usando la clase XmlItemExporter del paquete scrapy.exporters.
class EuropythonXmlExport(object): def __init__(self): self.files = {} @classmethod def from_crawler(cls, crawler): pipeline = cls() crawler.signals.connect(pipeline.spider_opened, signals.spider_opened) crawler.signals.connect(pipeline.spider_closed, signals.spider_closed) return pipeline def spider_opened(self, spider): file = open('europython_items.xml', 'w+b') self.files[spider] = file self.exporter = XmlItemExporter(file) self.exporter.start_exporting() def spider_closed(self, spider): self.exporter.finish_exporting() file = self.files.pop(spider) file.close() def process_item(self, item, spider): self.exporter.export_item(item) return item
De la misma manera podemos usar CsvItemExporter para exportar datos a formato csv.
Al finalizar el proceso, obtenemos como ficheros de salida:
europython_items.jsoneuropython_items.xmleuropython_items.csv
Actividad práctica: Implementar un pipeline que permita guardar los datos extraídos en un fichero sqlite
Completar el siguiente código sustituyendo las xxx por variables y métodos necesarios para guardar la información de las conferencias de la europython en un fichero en formato sqlite.
import scrapy from scrapy import signals from pony.orm import * db = Database("sqlite", "../europython.sqlite", create_db=True) class EuropythonSession(db.xxx): """Pony ORM model of the europython session table""" id = PrimaryKey(int, auto=True) author = xxx(str) title = Required(str) description = xxx(str) date = xxx(str) tags = xxx(str) class EuropythonSQLitePipeline(object): @classmethod def from_crawler(cls, xxx): pipeline = cls() crawler.signals.xxx(xxx, xxx) crawler.signals.xxx(xxx, xxx) return pipeline def spider_opened(self, xxx): db.xxx(check_tables=True, create_tables=True) def spider_closed(self, xxx): db.xxx() # Insert data in database @db_session def process_item(self, xxx, xxx): # use db_session as a context manager with xxx: try: strAuthor = str(xxx['author']) strAuthor = xxx[2:len(strAuthor)-2] strTitle = str(xxx['title']) strTitle = xxx[2:len(strTitle)-2] strDescription = str(xxx['description']) strDescription = xxx[2:len(strDescription)-2] strDate = str(xxx['date']) strDate = xxx.replace("[u'", "").replace("']", "").replace("u'", "").replace("',", ",") strTags = str(xxx['tags']) strTags = xxx.replace("[u'", "").replace("']", "").replace("u'", "").replace("',", ",") europython_session = EuropythonSession(author=xxx,title=xxx,description=xxx,date=strDate,tags=xxx) except Exception as e: print("Error processing the items in the DB %d: %s" % (e.args[0], e.args[1])) return xxx
En el fichero settings.py habría que añadir la clase correspondiente al pipeline de sqlite:
ITEM_PIPELINES = { 'europython.pipelines.EuropythonSQLitePipeline': 400 }
Solución
import scrapy from scrapy import signals from pony.orm import * db = Database("sqlite", "../europython.sqlite", create_db=True) class EuropythonSession(db.Entity): """Pony ORM model of the europython session table""" id = PrimaryKey(int, auto=True) author = Required(str) title = Required(str) description = Required(str) date = Required(str) tags = Required(str) class EuropythonSQLitePipeline(object): @classmethod def from_crawler(cls, crawler): pipeline = cls() crawler.signals.connect(pipeline.spider_opened, signals.spider_opened) crawler.signals.connect(pipeline.spider_closed, signals.spider_closed) return pipeline def spider_opened(self, spider): db.generate_mapping(check_tables=True, create_tables=True) def spider_closed(self, spider): db.commit() # Insert data in database @db_session def process_item(self, item, spider): # use db_session as a context manager with db_session: try: strAuthor = str(item['author']) strAuthor = strAuthor[2:len(strAuthor)-2] strTitle = str(item['title']) strTitle = strTitle[2:len(strTitle)-2] strDescription = str(item['description']) strDescription = strDescription[2:len(strDescription)-2] strDate = str(item['date']) strDate = strDate.replace("[u'", "").replace("']", "").replace("u'", "").replace("',", ",") strTags = str(item['tags']) strTags = strTags.replace("[u'", "").replace("']", "").replace("u'", "").replace("',", ",") europython_session = EuropythonSession(author=strAuthor,title=strTitle,description=strDescription,date=strDate,tags=strTags) except Exception as e: print("Error processing the items in the DB %d: %s" % (e.args[0], e.args[1])) return item
Settings proyecto europython
settings.py: Definimos el nombre del módulo europython.spiders y los pipelines definidos entre los que destacamos uno que permite exportar los datos en formato xml (EuropythonXmlExport) y otro que guarda los datos en una base de datos sqlite (EuropythonSQLitePipeline).
# Scrapy settings for europython project # # For simplicity, this file contains only the most important settings by # default. All the other settings are documented here: # # http://doc.scrapy.org/en/latest/topics/settings.html # BOT_NAME = 'europython' SPIDER_MODULES = ['europython.spiders'] NEWSPIDER_MODULE = 'europython.spiders' # Configure item pipelines # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { 'europython.pipelines.EuropythonJsonExport': 100, 'europython.pipelines.EuropythonXmlExport': 200, 'europython.pipelines.EuropythonCSVExport': 300, 'europython.pipelines.EuropythonSQLitePipeline': 400 } DOWNLOADER_MIDDLEWARES = { "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 110, #"europython.middlewares.ProxyMiddleware": 100, }
De esta forma ya tenemos un proyecto funcional, si lo ejecutamos, extraerá la información deseada y la guardará en los archivos correspondientes. Sin embargo, todavía hay aspectos por mejorar. Una de las más importantes es evitar el bloqueo de nuestro spyder debido a un número elevado de peticiones. El primer paso para evitar este caso es limitar la velocidad de las peticiones. En scrapy esto se puede hacer modificando la variable DOWNLOAD_DELAY en el fichero settings.py:
DOWNLOAD_DELAY=3
En este caso, se realizará como máximo una petición cada 3 segundos.
FAQ
¿Cuáles son los principales componentes de la arquitectura de scrapy?
La arquitectura de Scrapy contiene cinco componentes principales:
- El motor de Scrapy
- Planificador
- Descargador
- Arañas
- Tuberías de elementos



