SecNot

jun 27, 2015

mar 18, 2014

Web crawler con Scrapy

Los programas que recopilan información de una página web en busca de información se llaman crawler, spider, o arañas en castellano. Es una tarea tan común, que existen multitud de bibliotecas y frameworks con este propósito, en algunos casos simplificando la programación hasta el punto en el que se puede programar un crawler sencillo con menos de 100 lineas de código. Y eso es justamente lo que voy a hacer en este post.

Imaginemos que quiero crear una pequeña base de datos para organizar que libros he leído, y cuales me faltan por leer, pero no quiero tener que introducir manualmente la información de cada libro, así que voy a obtener toda la informacón de una página web, por ejemplo epublibre.

Para programar la araña me he decantado por Python, y un framework llamado Scrapy del que he leido mucho últimamente.

Instalación

La instalación de un paquete en python suele ser muy sencilla, pero en este caso he tenido algún problema con las dependencias, y he necesitado instalar algunos paquetes en el sistema:

$ apt-get install libxml2-dev libxslt-dev libffi-dev

Tras esto se instala scrapy con pip:

$ pip install scrapy

Uso

El primer paso es iniciar un nuevo proyecto:

$ scrapy startproject epublibre_crawler

Esto crea un directorio de proyecto con la siguiente estructura:

epublibre_crawler/
├── epublibre_crawler
│   ├── __init__.py
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       └── __init__.py
└── scrapy.cfg

Una vez creado el proyecto, tenemos que definir los items que queremos extraer, o mejor dicho la clase donde se almacenaran los datos extraídos por scrapy. En mi caso el titulo del libro, y su autor es suficiente, así que añadimos al archivo items.py la subclase de Item:

from scrapy.item import Item, Field

class BookItem(Item):
    title = Field()
    author  = Field()

El siguiente paso es describir usando expresiones XPath, como se puede extraer la información del titulo y autor, de manera que Scrapy pueda diferenciarla del resto de código html de la página de cada libro.

Si miramos el código html de la página de un libro, el título está contenido en un div de la forma:

<div class="det_titulo" id="titulo_libro" style="display:inline-block;">
        Omega
</div>

Dado que el titulo tiene un identificador único "titulo_libro", es fácil de extraer:

xpath("//div[@id='titulo_libro']/text()[normalize-space()]")

para el nombre del autor, tenemos:

<div class="negrita aut_sec" style="display:inline-block;">
    <a href="http://www.epublibre.org/autor/index/425">Jack McDevitt</a>
</div>

este es el único div de la página que usa la clase aut_sec, así que podemos extraer el nombre con:

xpath("//div[@class='negrita aut_sec']/a/text()")

Una vez tenemos las reglas de extracción, hay que describir como son las direcciones de las páginas de libro a las que queremos aplicarlas. El formato es http://www.epublibre.org/libro/detalle/6467, con el numero final variando para cada libro:

'libro/detalle/\d+'

Por último hay que crear la araña que usara scrapy en el directorio spiders:

#epublibre_spider.py
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.selector import Selector

# Importar la clase donde almacenar los resultados
from epublibre_crawler.items import BookItem

class BookSpider(CrawlSpider):

    # Nombre de la araña.
    name = 'epublibre'

    # Dominios en los que el crawler tiene permiso a acceder
    allowed_domains = ['epublibre.org']

    # La direccion de inicio para el crawler
    start_urls = ['http://www.epublibre.org']

    # Regla para diferenciar los enlaces de libros y función que se les aplica
    rules = [Rule(SgmlLinkExtractor(allow=['/libro/detalle/\d+']), 'parse_book')]

    def parse_book(self, response):
        """ Parser para las pagina de detalle de los libros"""
        sel = Selector(response)

        # Creamos un nuevo libro y asignamos los valores extraidos a
        # los campos correspondientes.
        book = BookItem()
        author = sel.xpath("//div[@class='negrita aut_sec']/a/text()").extract()
        title = sel.xpath("//div[@id='titulo_libro']/text()[normalize-space()]").extract()

        # Con Strip eliminamos tabulaciones y linea nueva.
        book['title']  = title[0].strip("\t\n\r")
        book['author'] = author[0].strip("\t\n\r")

        return book

Antes de iniciar scrapy hay que modificar las configuración y limitar la velocidad a la que los datos son accedidos, para no crear un ataque DOS:

# Scrapy settings for epublibre_scrapy 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 = 'epublibre_crawler'

SPIDER_MODULES = ['epublibre_scrapy.spiders']
NEWSPIDER_MODULE = 'epublibre_scrapy.spiders'

# Maximo una pagina cada minuto
DOWNLOAD_DELAY = 60

# Identificate
# USER_AGENT = 'epublibre_crawler (+http://www.tudominio.com)'

Finalmente solo queda ejecutar scrapy para que recolecte la información y la guarde en un archivo en alguno de los formatos soportados (XML, JSON, CSV), o hasta directamente en una base de datos usando una pipeline. En mi caso JSON es el formato elegido:

$ scrapy crawl epublibre -o libros.json -t json

el nombre epublibre es el que asigné a la araña BookSpider en la variable name.

El formato del archivo de salida es el siguiente:

[{"author": ["Ray Bradbury"], "title": ["Fahrenheit 451"]},
 {"author": ["John Scalzi"], "title": ["Redshirts"]}
]

Nota: Este programa es sólo un ejemplo para explicar el funcionamiento de scrapy, si realmente deseas extraer la información de epublibre.org, la propia página distribuye un archivo CSV con todos los datos de los libros disponibles.

Click to read and post comments