Web Scraping for Data Analysis in Python (Parte III): Scrapping


En el artículo anterior descargamos una copia local de todas las páginas que contenían información relevante para nosotros. Esto nos va a permitir trabajar con libertad a partir de este momento ya que no necesitaremos volver a acceder al portal. En otras palabras, podemos trabajar completamente offline.

Presentando: Scrapy

Mientras Selenium era una librería capaz de darnos el control absoluto de un navegador completo, Scrapy sigue una aproximación diferente: Analiza páginas HTML y ofrece un método muy sencillo para extraer información estructurada de ellas en formatos como CSV o JSON.

En muchos casos, Scrapy puede ser der usada directamente contra el servidor, pero en el caso de nuestro portal, se observó que el funcionamiento natural de la herramienta hacía que fuera muy fácil de detectar y las configuraciones necesarias para alterar ese funcionamiento eran complejas e inconsistentes.

Configuración

De modo similar a Selenium, los pre-requisitos son:

  • python==3.6
  • scrapy==1.7.3

En este caso, al no usar un navegador ni ningún otro servicio no será necesario descargar nada más. Por otro lado, sí que habrá que conocer la estructura de programa que Scrapy requiere, ya que éste es mucho más restrictivo que Selenium en ese aspecto.

Funcionamiento

La clase Spyder

La naturaleza de framework de Scrapy implica que hemos de adaptarnos a la estructura que propone, en este caso, extendiendo la clase Spider en un fichero python llamado, por ejemplo, scrapy.py:

Fragmento extraído de la documentación oficial

import scrapy


class MySpyfer(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)
  • La función start_requests() debe devolver iterativamente objetos de tipo Request con la dirección deseada.
  • La función parse() será llamada por Scrapy recibiendo el objeto Response correspondiente a cada Request. De este objeto obtendremos toda la información deseada.

Creación de las URI

En nuestro caso, como las páginas estan descargadas localmente, las URI han de tener la estructura: file:///ruta/local_directorio/fichero.html

Otra opción es ejecutar un servidor local como http-server (de Node). scrapy crawl main -o ./data/main.csv

Scrapping de la página

Este apartado es fuertemente dependiente del sitio web a scrapear, ya que todas las directivas css y xpath son específicas para portal.

Tal y como se introdujo en con Selenium, el método de selección de elementos que usaremos será la sintaxis de CSS combinada con la sintaxis XPath (similar, pero más potente en algunos aspectos). El funcionamiento específico de ambas sintaxis es extenso y hay suficiente literatura escrita como para profundizar en ello todo lo que se desee.

En el caso que nos ocupa, los campos pueden ser extraidos de la siguiente manera:

def parse(self, response):
	flat = response.css('section.detail-info')

	title = flat.css('span.main-info__title-main::text').get()

	description = flat.css('.adCommentsLanguage').xpath('.//text()').getall()
	description = ' '.join(description)

Los dos casos básicos son, extracción de campos simples, como el título; y extración de listas, como la descripción, que luego es concatenada con espacios.

Devolución del resultado

Finalmente, con todos los campos extraídos en variables, queda sólo devolver el resultado en forma de diccionario python:

yield {
	'title': title,
	'description': description
}

Ejecución del experimento

Para lanzar el spider, será necesario ejecutar:

python -m scrapy runspider scrapy.py -o .data.csv

Esto iniciará el proceso de recolección de datos y debería generar un fichero CSV con una fila por cada item y una columna por cada campo devuelto en el diccionario python.