Home > python, scrapy, xpath > scrapy: spider

scrapy: spider

12 Ottobre 2017

Articolo precedente: primi passi con scrapy

Spiders

Gli Spiders sono classi che definiscono come un sito o gruppi di siti, debbano essere analizzati.
In queste classi si decide come sarà il crawling/parsing del sito o dei gruppi di siti e come
saranno strutturati i dati estratti da tali pagine web.

Il progetto è stato già creato precedentemente, si tratta ora di creare lo spider adatto al nostro scopo.
Esiste un direttorio nome-progetto/nome-progetto//spiders all’interno del quale creeremo un file nome-progetto_spider.py nel mio caso, nel direttorio ansa/ansa/spiders, creerò un file ‘ansa_spider.py‘ con all’interno il codice necessario per lo scraping/crawling del sito in oggetto.

Vediamo il ciclo di funzionamento dello spider.

1. Requests iniziali

Generazione delle Requests iniziali per “raschiare” (crawl) le prime URLs e
specifica della funzione di callback da invocare con il Response ottenuto da queste prime Requests.
Le prime requests da eseguire sono ottenute con la chiamata del metodo start_requests()
che di default genera Request per le URLs specificate in una lista, chiamando per ognuna di esse
la corrispondente funzione di callback parse().

import scrapy


class AnsaSpider(scrapy.Spider):
    name = "ansa"
    allowed_domains = ["ansa.it"]

    def start_requests(self):
        urls = ["http://www.ansa.it",]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        pass

“name” è il nome univoco con il quale scrapy troverà ed utilizzerà lo spider.

“allowed_domains” è la lista dei domini autorizzati ad essere analizzati dallo spider.
Se il middleware OffsiteMiddleware sarà abilitato, i domini non presenti in questa lista,
non verranno analizzati dallo spider.

“start_requests” è il metodo che ritorna la lista con le prime Requests da analizzare. Viene invocato da Scrapy
quando apre lo spider per iniziare l’analisi.

Callback parse()

Nella funzione di callback parse(), eseguiamo appunto il parsing del response (web page) e ritorniamo:
– dizionario con i dati estratti
– Item object
– Request objects, o un iterable di tali oggetti

Con sintassi xpath, il primo abbozzo di spider sarebbe:

# ansa_spider.py
import scrapy


class AnsaSpider(scrapy.Spider):
    name = "ansa"
    allowed_domains = ["ansa.it"]

    def start_requests(self):
        start_urls = ["http://www.ansa.it",]
        for url in start_urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        xf = '//article[@class="news"]/descendant::a/@href'
        for url in response.xpath(xf).extract():
             yield {"title": url}

mentre la stessa con il metodo css():

# ansa_spider.py
import scrapy


class AnsaSpider(scrapy.Spider):
    name = "ansa"
    allowed_domains = ["ansa.it"]

    def start_requests(self):
        start_urls = ["http://www.ansa.it",]
        for url in start_urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        for url in response.css('article.news a::attr(href)').extract():
             yield {"title": url}

Lanciamo lo spider e viediamo che succede:

scrapy crawl ansa

come si nota, lo spider estrapola tutti i link presenti nella web page, ritornando un dizionario per ogni
link trovato. Se vogliamo generare un file con questi dati:

scrapy crawl ansa -o ansa.json

Nota:

Al momento scrapy esegue solo l’append dei dati e non sovrascrive il file, se già esistente.
Ricordarsi di eliminarlo prima di procedere con l’estrazione dei dati!

volendo estrarre i dati dal file, entriamo nella shell:

scrapy shell
import json


with open('ansa.json') as f:
    data = json.loads(f.read())

d = {n: v.get('link') for n, v in enumerate(data, 1) if v.get('link') != "javascript:void(0);"}

Tutte queste Requests potranno essere parsate e scaricate a loro volta da scrapy, facendo riferimento ad
una ulteriore callback (anche la stessa parse() volendo).

import scrapy


class AnsaSpider(scrapy.Spider):
    name = "ansa"
    allowed_domains = ["ansa.it"]

    def start_requests(self):
        start_urls = ["http://www.ansa.it",]
        for url in start_urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        xf = '//article[@class="news"]/descendant::a/@href'
        for url in response.xpath(xf).extract():
            if url and url != "javascript:void(0);":
                yield response.follow(url, callback=self.parse_news)

    def parse_news(self, response):
        title = response.xpath('//title/text()').extract_first()
        x_body = '//div[@class="news-txt"]/descendant::p'
        body = ' '.join(([node.xpath('string()').extract()[0]
                          for node in response.xpath(x_body)]))
        yield {'title': title,
               'body': body,
               }

Ora possiamo accedere ad un dizionario che al posto dei singoli link, ha i testi degli articoli.
Ma se volessimo portarci dietro dal primo parse, all’interno delle pagine figlie qualche attributo tipo ad es.
il link alla pagina figlia? Questo ha senso ovviamento per parametri che non sono reperibili nelle pagine figlie,
ma che debbano essere “trasportati” da un parse all’altro.

In questo caso ci vengono in aiuto gli items.

Dentro al file ansa/items.py definiamo il nostro personale Item:

import scrapy


class AnsaItem(scrapy.Item):
    # define the fields for your item here like:
    link = scrapy.Field()

Con l’ausilio di quest’ultimo, possiamo modificare il codice dello spider come segue:

import scrapy
from ansa.items import AnsaItem
from scrapy.exceptions import NotSupported


class AnsaSpider(scrapy.Spider):
    name = "ansa"
    allowed_domains = ["ansa.it"]

    def start_requests(self):
        start_urls = ["http://www.ansa.it",]
        for url in start_urls:
            print '[INFO] Parsing url %s...' % url
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        xf = '//article[@class="news"]/descendant::a[position()=1]/@href'
        for url in response.xpath(xf).extract():
            if url and url != "javascript:void(0);":
                short_url = '/.../%s' % url.split('/')[-1]
                print '[INFO] Parsing urls %s = %s' % (n, short_url)
                ansa_item = AnsaItem()
                # ansa_item assignments
                ansa_item['link'] = url
                yield response.follow(url, callback=self.parse_news,
                                      meta={'ansa_item': ansa_item})

    def parse_news(self, response):
        ansa_item = response.meta.get('ansa_item')
        link = ansa_item.get('link')
        try:
            title = response.xpath('//title/text()').extract_first()
            x_body = '//div[@class="news-txt"]/descendant::p'
            body = ' '.join(([node.xpath('string()').extract()[0]
                              for node in response.xpath(x_body)]))
            yield {'link': link.strip(),
                   'title': title.strip(),
                   'body': body.strip(),
                  }
        except NotSupported:
            print "[WARNING] Response %s not crawled" % response

Nell’Item, gli assegnamenti agli attributi e l’accesso ad essi si comportano
allo stesso modo dei dizionari.
Come si vede, nella prima callback parse(), viene creato l’item e gli si assegna alla chiave ‘link’ il
valore di url. Quando utilizzeremo il metodo follow dell’oggetto response, per analizzare le sottopagine,
richiameremo la callback parse_news() e passeremo al metodo follow(),
l’attributo meta, un dizionario con all’interno il nostro item.
Questo item, viene recuperato all’interno della callback parse_news() ed il valore della sua
chiave ‘link’, ritornato nel dizionario con yield, insieme agli altri valori.

Eliminiamo un eventuale file json preesistente e ricreiamo il tutto:


>>> import json
>>> with open('ansa.json') as f:
...     data = json.loads(f.read())
...
>>> news_dict = {n: value for n, value in enumerate(data, 1)}
>>> news_1 = news_dict.get(1)
>>> news_1.get('title')
u"Ultimatum..."
>>> news_1.get('link')
u'/sito/notizie/mondo/europa/...'

link utili:
scrapy: appunti
scrapy
xpath: appunti

Categorie:python, scrapy, xpath Tag: , ,
I commenti sono chiusi.