Tabla de Contenidos
Scrapy como framework de desarrollo de spyders
Módulo perteneciente al curso Python avanzado para proyectos de seguridad
En esta sección exploraremos Scrapy como un framework de desarrollo para Python que nos permite realizar tareas de web scraping y procesos de rastreo web y análisis de datos. Además, explicaremos la estructura que presenta un proyecto Scrapy y cómo crear nuestro propio proyecto, y crearemos un spyder para rastrear una página web y extraer los datos que nos interesan. Revisaremos los componentes Scrapy, creando un proyecto para configurar diferentes pipelines.
Una de las principales ventajas de Scrapy es que está construido sobre Twisted, un framework de red asincrónico y sin bloqueo para tareas de concurrencia. “Sin bloqueo” significa que no tiene que esperar a que finalice una solicitud antes de hacer otra, incluso puede lograrlo con un alto nivel de rendimiento. Esta característica mejora los crawlers y spyders desarrollados con scrapy. Entre las principales ventajas de usar scrapy podemos destacar:
- Menos uso de CPU y menos consumo de memoria.
- Muy eficiente en comparación con otros frameworks y librerías.
- La arquitectura diseñada le ofrece robustez y flexibilidad.
- Puede desarrollar fácilmente middleware personalizado para añadir funcionalidades personalizadas.
Creación de un proyecto scrapy
Para crear un proyecto con scrapy hay que ejecutar desde la consola el siguiente comando:
$ scrapy startproject <nombre_proyecto>
Cuando crea un proyecto con el comando anterior, generará la siguiente estructura de carpetas y ficheros donde podemos ver los principales componentes de un proyecto scrapy:
nombre_proyecto
scrapy.cfg #fichero de configuracion
project_name /
__init__.py
items.py #Definicion de los items
pipelines.py #configurar pipelines
settings.py #configuración de los spiders
spiders #directorio donde se guardan los spiders
        __init__.py
Cada proyecto se compone de los siguientes ficheros:
- items.py: Definimos los elementos a extraer y creamos los campos de la información que vamos a extraer.
- spiders: Es el corazón del proyecto, aquí definimos el procedimiento de extracción. Scrapy internamente lo que hace es buscar clases del tipo Spyder ubicadas en la carpeta de spiders y usará la configuración que encontramos en el archivo- settings.py.
- pipelines.py: Son los elementos para analizar lo obtenido: validación de datos, limpieza del código HTML. Se usa para recibir un elemento y realizar una acción sobre él.
Una vez que se crea el proyecto, tenemos que definir los elementos que queremos extraer, o más bien la clase donde se almacenarán los datos extraídos por scrapy. El siguiente código sería un ejemplo de fichero items.py donde definimos una clase que hereda de la clase scrapy.Item:
En esta clase básicamente tenemos que definir los campos (fields) de la información que queremos extraer(título, descripción, url, precio..)
from scrapy.item import Item, Field class MyItem(Item): name = Field()
Scrapy proporciona la clase Item para definir el formato de datos de salida. Los objetos Item son contenedores que se utilizan para recopilar los datos extraídos y especifican metadatos para el campo utilizado para caracterizar esos datos. Estos objetos proporcionan una sintaxis para declarar campos donde el objeto Field especifica los metadatos para cada campo. Para más detalles, ver la documentación de esta clase: https://doc.scrapy.org/en/latest/topics/items.html
Spyders
Scrapy usa los spyders para definir cómo se debe scrapear un sitio para obtener información. Scrapy nos permite determinar qué información queremos extraer y cómo podemos extraerla. Específicamente, las spyders son clases de Python donde pondremos toda nuestra lógica y comportamiento personalizados.
Como hemos comentado, los spyders son clases que definen la forma de navegar por un determinado sitio o dominio y como extraer datos de esas páginas, es decir, definimos de forma personalizada el comportamiento para analizar las páginas de un sitio particular.
El ciclo que sigue un spyder es el siguiente:
- Primero empezamos generando la petición inicial (Requests) para navegar por la primera URL y especificamos la funciónparse_item()que será llamada con la respuesta (Response) descargada de esa petición.
- La primera petición a hacer es obtenida llamando al métodostart_request()que por defecto genera la petición para la URL específica en las direcciones de iniciostart_urlsy la funciónparse_item()para las peticiones.
En la función de parse_item() analizamos la respuesta y se puede devolver:
- Objetos tipo Item
- Objetos tipo Request
- una unión de ambos sobre la que se puede iterar
Estas peticiones serán realizadas descargándose por Scrapy y sus respuestas manipuladas por la función parse_item(). En esta función analizamos el contenido usando los selectores (XPath Selectors) y generamos los Items con el contenido analizado. Finalmente, los Items devueltos por el spyder se podrán pasar a algún Item Pipeline.
Estructura de un spyder
Esta podría ser la estructura base de nuestro spider donde definimos el nombre del spyder y el dominio del cual queremos extraer información.
from scrapy.contrib.spiders import CrawlSpider class MySpider(CrawlSpider): name = 'myspider' allowed_domains = ['dominio.com']
En primera instancia realizamos los imports de las clases necesarias para llevar a cabo el proceso de crawling. Entre estas clases podemos destacar:
- Rule: Nos permite establecer las reglas por las cuales el crawler se va a basar para navegar por los diferentes enlaces.
- LxmlLinkExtractor: Nos permite definir una función de callback y expresiones regulares para indicarle al crawler por los enlaces que debe pasar. Permite definir las reglas de navegación entre los enlaces que queremos obtener
- HtmlXPathSelector: Permite aplicar expresiones XPath.
- CrawlSpider: provee un mecanismo que permite seguir los enlaces que siguen un determinado patrón. Además de los atributos inherentes a la clase BaseSpider, esta clase dispone de un nuevo atributo “rules” con el cual podemos indicarle al Spider el/los comportamiento/s que debe seguir.
Creando el esqueleto de nuestro spyder
Este es el código de nuestro spyder que podemos guardar en un archivo llamado MySpider.py bajo el directorio de spyders del proyecto de scrapy:
from scrapy.contrib.spiders import CrawlSpider, Rule from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor from scrapy.selector import Selector from scrapy.item import Item class MySpider(CrawlSpider): name = 'dominio.com' allowed_domains = ['dominio.com'] start_urls = ['http://www.example.com'] rules = (Rule(LxmlLinkExtractor(allow=()))) def parse_item(self, response): hxs = Selector(response) elemento = Item() return elemento
La clase CrawlSpider proporciona un mecanismo que le permite obtener los enlaces que siguen un determinado patrón. Además de los atributos inherentes de la clase BaseSpider, esta clase tiene un nuevo atributo de reglas con el que podemos indicarle al spyder el comportamiento que debe seguir.
Extracción de enlaces con scrapy
El primer paso es crear el spyder. Scrapy proporciona el comando genspider para generar la plantilla básica de nuestro spyder.
$ scrapy genspider <spidername> <website> * [[http://books.toscrape.com/|Scraping de libros]] Para comenzar, crearemos una clase ''BooksSpider'' que podemos usar para recorrer la página principal y añadir posteriormente nuestro comportamiento. Podríamos generar nuestro spyder con nombre ''BooksSpider.py'' con el siguiente comando: <code python> import scrapy class BooksSpider(scrapy.Spider): name = 'BooksSpider' allowed_domains = ['books.toscrape.com'] start_urls = ['http://books.toscrape.com/'] def parse(self, response): pass
La clase BooksSpider hereda de la clase base scrapy.Spider. Al heredar de scrapy.spider.Spider se proporcionan los siguientes métodos:
- start_requests(). Para cada url definida se crea un objeto request de scrapy, asignando los parámetros necesarios para que el motor use la conexión correcta. Como manejador de respuestas, se asigna el método- parse().
- parse(response): Scrapy llama a este método cuando obtiene un objeto de respuesta HTTP al completar con éxito una descarga de contenido. Recibe el mismo objeto de tipo response. El objetivo principal sería extraer los datos apropiados de esta respuesta.
Puede encontrar más información sobre este método en la documentación: https://doc.scrapy.org/en/latest/topics/spiders.html#scrapy.spiders.Spider.parse
- namees el nombre de la araña que se dio en el comando de generación. Usaremos este nombre para iniciar la araña desde la línea de comandos.
- allowed_domains: es una lista de los dominios permitidos que la araña puede rastrear.
- start_urlses la URL desde donde comenzará el proceso de scraping. Luego tenemos el método de análisis que analiza el contenido de la página.
- parse()es la función que permite analizar la respuesta, extraer los datos y obtener nuevas URLs para seguir creando nuevas peticiones a partir de ellas.
Fichero BooksSpider.py
El objetivo ahora es extraer los enlaces para cada libro, de esta forma, actualizaremos la clase BooksSpider en el método parse(response). El siguiente código se encuentra en el archivo BooksSpider.py.
#!/usr/bin/env python3 from scrapy import Spider from scrapy.http import Request class BooksSpider(Spider): name = 'BooksSpider' allowed_domains = ['books.toscrape.com'] start_urls = ['http://books.toscrape.com'] def parse(self, response): books = response.xpath('//h3/a/@href').extract() for book in books: absolute_url = response.urljoin(book) yield Request(absolute_url, callback=self.parse_book) # process next page next_page_url = response.xpath('//a[text()="next"]/@href').extract_first() absolute_next_page_url = response.urljoin(next_page_url) yield Request(absolute_next_page_url) def parse_book(self, response): yield { 'book_url': response.url}
La parte más importante sobre el método parse(self, response) es que devuelve las respuestas en forma de diccionario y cada diccionario se interpretará como un elemento que aparecerá en la salida de Scrapy.
Ejecución de BookSpider
Podríamos ejecutar el siguiente comando para extraer todos los enlaces en el archivo books_links.json.
$ scrapy runspider BooksSpider.py -o books_links.json -t json
En la salida del comando anterior vemos la sección de estadísticas (Dumping Scrapy stats) con información como request_count y response_count que representa la cantidad de elementos extraídos.
[scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 353325,
 'downloader/request_count': 1050,
 'downloader/request_method_count/GET': 1050,
 'downloader/response_bytes': 4711491,
 'downloader/response_count': 1050,
 'downloader/response_status_count/200': 1050,
 'dupefilter/filtered': 1,
 'elapsed_time_seconds': 13.283743,
 'finish_reason': 'finished',
 'finish_time': datetime.datetime(2020, 7, 19, 17, 13, 34, 279711),
 'item_scraped_count': 1000,
 'log_count/DEBUG': 2051,
 'log_count/INFO': 11,
 'memusage/max': 54747136,
 'memusage/startup': 54747136,
 'request_depth_max': 50,
 'response_received_count': 1050,
 'scheduler/dequeued': 1050,
 'scheduler/dequeued/memory': 1050,
 'scheduler/enqueued': 1050,
 'scheduler/enqueued/memory': 1050,
 'start_time': datetime.datetime(2020, 7, 19, 17, 13, 20, 995968)}
En la salida del comando anterior vemos la línea downloader/response_status_count/200, donde 200 significa que recibimos con éxito una respuesta del servidor. Podemos obtener otros códigos como 500 y 400, lo que significa que el servidor rechazó la solicitud o no es capaz de obtener una respuesta.
Actividad práctica: Completa el siguiente script que permite obtener los enlaces de una url con scrapy
Completa el siguiente script que permite obtener los enlaces la página de hackernews (https://news.ycombinator.com). Sustituir las xxx por variables definidas.
#!/usr/bin/env python3 import scrapy from scrapy.spiders import Spider, Rule from scrapy.linkextractors import LinkExtractor from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor from scrapy.selector import Selector class HackerNewsItem(scrapy.Item): # define the fields for your item here like name = scrapy.xxx() link = scrapy.xxx() class HackerNewsSpyder(xxx): name = "hacker_news_spyder" allowed_domains = ["xxx"] start_urls = ['xxx'] def parse(self, response): hxs = Selector(xxx) urls = hxs.xxx('//a') items=[] for url in xxx: item = xxx() item['name'] = url.xxx('text()').xxx() item['link'] = url.xxx('@href').xxx() if(item['link'].xxx("http")): items.xxx(item) return xxx
Para ejecutar el script lo podemos hacer con el comando:
$ scrapy runspider hacker_news_spyder.py
Solución
#!/usr/bin/env python3 import scrapy from scrapy.spiders import Spider, Rule from scrapy.linkextractors import LinkExtractor from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor from scrapy.selector import Selector class HackerNewsItem(scrapy.Item): # define the fields for your item here like name = scrapy.Field() link = scrapy.Field() class HackerNewsSpyder(Spider): name = "hacker_news_spyder" allowed_domains = ["news.ycombinator.com"] start_urls = ['https://news.ycombinator.com'] def parse(self, response): hxs = Selector(response) urls = hxs.xpath('//a') items=[] for url in urls: item = HackerNewsItem() item['name'] = url.xpath('text()').get() item['link'] = url.xpath('@href').get() if(item['link'].startswith("http")): items.append(item) return items
Scrapy y pipelines
Los pipelines son elementos de Scrapy a los que la información que les llega son Items que han sido previamente obtenidos y procesados por algún Spider.
Son clases en sí que tienen un simple objetivo: volver a procesar el Item que les llega pudiendo rechazarlo o dejar que pase por este pipeline.
Los usos típicos de los pipelines son:
- Limpieza de datos en HTML.
- Validación de datos scrapeados comprobando que los items contienen ciertos campos.
- Comprobación de items duplicados.
- Almacenamiento de los datos extraídos en una base de datos.
Para cada elemento que se obtiene, se envía al pipeline correspondiente, que lo procesará para guardarlo en la base de datos o para enviarlo a otra pipeline si fuera necesario.
Una pipeline de elementos es una clase de Python que sobrescribe algunos métodos específicos y debe activarse en la configuración del proyecto Scrapy.
Al crear un proyecto Scrapy, encontrará un archivo pipelines.py ya disponible para crear sus propios pipelines.
Estos objetos son clases de Python que deben implementar el método process_item(item, spider) y deben devolver un objeto tipo Item (o una subclase de este) o bien, si no lo devuelve, debe lanzar una excepción del tipo DropItem para indicar que ese Item no seguirá siendo procesado. Un ejemplo de este componente es:
#!/usr/bin/python from scrapy.exceptions import DropItem class MyPipeline(object): def process_item(self, item, spider): if item['key']: return item else: raise DropItem("No existe el elemento: %s" % item['key'])
Un punto más a tener en cuenta es que cuando creamos un objeto de este tipo debemos introducir en el fichero settings.py del proyecto una línea como la siguiente para activar el pipeline en la variable ITEM_PIPELINES:
ITEM_PIPELINES = [ 'proyecto.pipeline.MyPipeline':300,]
Fichero configuración de scrapy settings.py
El fichero de configuración lo podemos encontrar en el directorio raíz del proyecto para que el spyder se puede ejecutar correctamente. Este archivo almacena la configuración del spyder y le permite modificar el comportamiento de los spyders cuando sea necesario, por ejemplo, modificando las cabeceras de la petición, el agente de usuario y el tiempo de retardo entre peticiones con el objetivo de que nuestro spyder tenga un mejor rendimiento. Entre las principales características en forma de variables que podemos modificar podemos destacar:
- DEFAULT_REQUEST_HEADERS: que son parte de cualquier solicitud que su navegador envía al servidor web. Esta característica es similar al- USER_AGENTy se puede usar con algunos sitios que no le permiten ver sus datos sin usar cabeceras en las peticiones.
- USER_AGENT: que en realidad es parte de los encabezados, pero también se puede configurar por separado. Con respecto al agente de usuario, puede configurarlo de forma personalizada.
- DOWNLOAD_DELAY: que permite establecer un tiempo de retardo entre solicitudes concurrentes a diferentes páginas del mismo sitio web. Cada solicitud se retrasa el tiempo en segundos especificado en esta variable. Por ejemplo, si lo configura como 5, esto retrasa cada petición 5 segundos. Antes de iniciar Scrapy, se recomienda modificar la configuración y limitar la velocidad a la que se accede a los datos y evitar los ataques DOS (Denegación de servicio).
- ROBOTSTXT_OBEY: que ofrece una opción para seguir o ignorar el archivo- robots.txten el sitio web. El archivo- robots.txt, almacenado en la raíz del sitio web, describe el comportamiento deseado de los bots en el sitio web.
Este podría ser un ejemplo del archivo de configuración settings.py:
# Identificar el agente de usuario USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' #respeta las reglas definidas en el fichero de robots.txt ROBOTSTXT_OBEY = True # Sobrescribe las cabeceras de la petición predeterminadas DEFAULT_REQUEST_HEADERS = { "Accept": "application/json, text/javascript, */*; q=0.01", "DNT": "1", "Accept-Encoding": "gzip, deflate, br", "Accept-Language":"en-GB,en-US;q=0.9,en;q=0.8", "x-requested-with": "XMLHttpRequest", } #Configure un tiempo de retardo para las solicitudes en el mismo sitio web # (predeterminado: 0) # See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs DOWNLOAD_DELAY = 3
Exportación de resultados en formatos json, csv, xml
Con Scrapy podemos recopilar la información y guardarla en un archivo en uno de los formatos compatibles (XML, JSON o CSV), o incluso directamente en una base de datos usando un pipeline. En este caso, estamos ejecutando el comando scrapy pasando como argumento el formato JSON:
$ scrapy craw <crawler_name> -o items.json -t json
Los últimos parámetros indican que los datos extraídos se almacenan en un archivo llamado items.json y que el exportador utiliza para el formato JSON. Se puede hacer de la misma manera para exportar a formatos CSV y XML.
La opción -o items.csv proporciona como parámetro el nombre del archivo de salida que contendrá los datos que ha extraído.
Con la opción -t csv, obtendremos un archivo CSV con el resultado del proceso de scraping:
$ scrapy crawl <crawler_name> -o items.csv -t csv
Con la opción -t json, obtendremos un archivo JSON con el resultado del proceso de scraping:
$ scrapy crawl <crawler_name> -o items.json -t json
Con la opción -t xml, obtendremos un archivo XML con el resultado del proceso de scraping:
$ scrapy crawl <crawler_name> -o items.xml -t xml
Consejos y trucos de ejecución de scrapy
Al ejecutar Scrapy, podemos seguir estas reglas para administrar la ejecución del rastreador:
- Si el proceso de scraping falla, puede buscar en el registro de la consola las líneas que incluyen[Scrapy] DEBUG.
- Si desea parar el proceso de Scrapy mientras aún se está procesando basta con presionar la combinación de teclas Ctrl + Ctrl.
- Cuando Scrapy haya terminado de procesar los datos, mostrará la siguiente información en la consola de registro:[scrapy] INFO: Spider closed (finished)
- Por defecto, Scrapy añade nuevos datos al final del archivo de salida si ya existe. Si el archivo no existe, creará uno. Por lo tanto, si solo desea obtener datos nuevos y descartar los anteriores, es recomendable eliminar el archivo anterior.

