Extracción de datos web y rastreo con Scrapy y MongoDB

La última vez implementamos un raspador web básico que descargó las últimas preguntas de StackOverflow y almacenó los resultados en MongoDB. En este artículo extenderemos nuestro scraper para que rastree a través de los enlaces de paginación en la parte inferior de cada página y raspe las preguntas (título de la pregunta y URL) de cada página.

Bono gratis: Haga clic aquí para descargar un esqueleto de proyecto Python + MongoDB con código fuente completo que le muestra cómo acceder a MongoDB desde Python.

Actualizaciones:

  1. 09/06/2015 – Actualizado a la última versión de Scrapy (v1.0.3) y PyMongo (v3.0.3) – vítores!

Antes de comenzar cualquier trabajo de raspado, revise la política de términos de uso del sitio y respete el archivo robots.txt. Además, siga las prácticas éticas de raspado al no inundar un sitio con numerosas solicitudes en un corto período de tiempo. Trata cualquier sitio que raspes como si fuera el tuyo.

Esta es una pieza de colaboración entre la gente de Real Python y Gyrgy, un entusiasta de Python y desarrollador de software, que actualmente trabaja en una compañía de big data y busca un nuevo trabajo al mismo tiempo. Puedes hacerle preguntas en Twitter: kissgyorgy.

Cómo empezar

Hay dos formas posibles de continuar desde donde lo dejamos.

El primero es extender nuestro Spider existente extrayendo cada enlace de la página siguiente de la respuesta en elparse_itemmétodo con una expresión xpath y soloyieldaRequestObjeto con una devolución de llamada a la mismaparse_itemmétodo. De esta manera Scrapy automáticamente hará una nueva solicitud al enlace que especifiquemos. Puede encontrar más información sobre este método en la documentación de Scrapy.

La otra opción, mucho más simple, es utilizar un tipo diferente de araña: la araña.CrawlSpider(enlace). Es una versión extendida de lo básico.Spider, diseñado exactamente para nuestro caso de uso.

The CrawlSpider (Edición española)

Utilizaremos el mismo proyecto Scrapy del último tutorial, así que toma el código del repositorio si lo necesitas.

Crea el Boilerplate

Dentro del directorio “stack”, comience por generar la placa de araña desde elcrawlplantilla:

$ scrapy genspider stack_crawler stackoverflow.com -t crawl
Created spider 'stack_crawler' using template 'crawl' in module:
  stack.spiders.stack_crawler

El proyecto Scrapy ahora debería verse así:

├── scrapy.cfg
└── stack
    ├── __init__.py
    ├── items.py
    ├── pipelines.py
    ├── settings.py
    └── spiders
        ├── __init__.py
        ├── stack_crawler.py
        └── stack_spider.py

Y el archivo stackcrawler.py debería verse así:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.contrib.linkextractors import LinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule

from stack.items import StackItem


class StackCrawlerSpider(CrawlSpider):
    name = 'stack_crawler'
    allowed_domains = ['stackoverflow.com']
    start_urls = ['http://www.stackoverflow.com/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        i = StackItem()
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return i

Solo tenemos que hacer algunas actualizaciones a este boilerplate…

Actualizar elstart_urlslist

En primer lugar, añadir la primera página de preguntas a lastart_urlslista:

start_urls = [
    'http://stackoverflow.com/questions?pagesize=50&sort=newest'
]

Actualizar elruleslist

A continuación, debemos decirle a la araña dónde puede encontrar los enlaces de la página siguiente agregando una expresión regular a la página.rulesatributo:

rules = [
    Rule(LinkExtractor(allow=r'questions?page=[0-9]&sort=newest'),
         callback='parse_item', follow=True)
]

Scrapy ahora solicitará automáticamente nuevas páginas basadas en esos enlaces y pasará la respuesta a laparse_itemMétodo para extraer las preguntas y los títulos.

Si está prestando mucha atención, esta regla limita el rastreo a las primeras 9 páginas, ¡ya que para esta demostración no queremos raspar todas las 176,234 páginas!

Actualizar elparse_itemmétodo

Ahora solo tenemos que escribir cómo analizar las páginas con xpath, lo que ya hicimos en el último tutorial, así que simplemente cópielo:

def parse_item(self, response):
    questions = response.xpath('//div[@class="summary"]/h3')

    for question in questions:
        item = StackItem()
        item['url'] = question.xpath(
            'a[@class="question-hyperlink"]/@href').extract()[0]
        item['title'] = question.xpath(
            'a[@class="question-hyperlink"]/text()').extract()[0]
        yield item

Eso es todo para la araña, pero no lo empieces todavía.

Agregar un retardador de descarga

Tenemos que ser amables con StackOverflow (y cualquier sitio, para el caso) estableciendo un retraso de descarga en settings.py:

DOWNLOAD_DELAY = 5

Esto le dice a Scrapy que espere al menos 5 segundos entre cada nueva solicitud que haga. Básicamente estás limitándote a ti mismo. Si no lo hace, StackOverflow tasa le limitará; y si continúa raspando el sitio sin imponer un límite de velocidad, su dirección IP podría ser prohibida. Por lo tanto, sea amable – Trate cualquier sitio que raspe como si fuera el suyo propio.

Ahora solo queda una cosa por hacer: almacenar los datos.

MongoDB

La última vez solo descargamos 50 preguntas, pero como estamos agarrando muchos más datos esta vez, queremos evitar agregar preguntas duplicadas a la base de datos. Podemos hacerlo usando un upsert de MongoDB, lo que significa que actualizamos el título de la pregunta si ya está en la base de datos e inserte de otra manera.

Modifique elMongoDBPipelineDefinimos anteriormente:

class MongoDBPipeline(object):

    def __init__(self):
        connection = pymongo.MongoClient(
            settings['MONGODB_SERVER'],
            settings['MONGODB_PORT']
        )
        db = connection[settings['MONGODB_DB']]
        self.collection = db[settings['MONGODB_COLLECTION']]

    def process_item(self, item, spider):
        for data in item:
            if not data:
                raise DropItem("Missing data!")
        self.collection.update({'url': item['url']}, dict(item), upsert=True)
        log.msg("Question added to MongoDB database!",
                level=log.DEBUG, spider=spider)
        return item

Para simplificar, no optimizamos la consulta y no tratamos con índices, ya que este no es un entorno de producción.

Ensayo

¡Empieza la araña!

$ scrapy crawl stack_crawler

¡Ahora siéntese y vea cómo su base de datos se llena de datos!

$ mongo
MongoDB shell version: 3.0.4
> use stackoverflow
switched to db stackoverflow
> db.questions.count()
447
>

Conclusión

Puedes descargar todo el código fuente desde el repositorio de Github. Comenta a continuación con preguntas. ¡Salud!

Bono gratis: Haga clic aquí para descargar un esqueleto de proyecto Python + MongoDB con código fuente completo que le muestra cómo acceder a MongoDB desde Python.

¿Buscas más web scraping? Asegúrese de revisar los cursos de Python Real. ¿Quieres contratar a un raspador web profesional? Echa un vistazo a GoScrape.

Enlaces Externos

Entradas relacionadas

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *