Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:python_avanzado_proyectos_seguridad:proyecto_scrapy_extraer_conferencias_europtyon

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.&nbsp; You need to download some files from a server to your local machine. &nbsp; The files are fairly small, and you can list and access these files from the remote server through a REST API.&nbsp; You'd like to download them as fast as possible.&nbsp; The catch?&nbsp; There's a billion of them.&nbsp; 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.json
  • europython_items.xml
  • europython_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
informatica/programacion/cursos/python_avanzado_proyectos_seguridad/proyecto_scrapy_extraer_conferencias_europtyon.txt · Última modificación: por tempwin