Matteo Vignoli

Sviluppatore Web Full-Stack autodidatta, curioso per natura,
attualmente impiegato a Milano presso ContactLab per conto di Anoki S.r.L.

Scrapy, Mailgun: raccogliere i risultati degli spider ed inviarli via mail

  Tempo di lettura:

Scrapy, per chi non lo conosce, è un framework scritto in python usato per effettuare scraping e crawling di pagine web, molto veloce e potente ma abbastanza semplice da impostare anche per chi, come me, non è un esperto di questo linguaggio.

In un progettino personale su cui sto lavorando attualmente ho degli spider in cron che effettuano lo scraping di alcune pagine web (ogni sito ha il suo spider dedicato, ma tutti ereditano un ItemLoader comune) e, attraverso una pipeline, scrivono l'esito del loro elaborazione su di un file html.

Creare un semplice ItemExporter personalizzato

Un ItemExporter non è altro che una classe utilizzata da Scrapy per salvare/scrivere il risultato del parsing, e viene di solito richiamato all'interno della Pipeline configurata per accogliere l'output degli spider. Ad esempio:

class ProjectPipeline(object):

  @classmethod
  def from_crawler(cls, crawler):
        pipeline = cls()
        crawler.signals.connect(pipeline.spider_opened, signals.spider_opened)
        crawler.signals.connect(pipeline.spider_closed, signals.spider_closed)
        return pipeline

  def spider_opened(self, spider):
        self.exporter = CustomItemExporter()
        self.exporter.name = spider.name
        # ....etc.....
        self.exporter.start_exporting()

    def spider_closed(self, spider):
        self.exporter.finish_exporting()
        # ........

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        # ........

Come si può vedere l'exporter ha, fondamentalmente, tre metodi che vengono richiamati:

  1. start_exporting(), nel quale inizializziamo l'esportazione, ad esempio creando un'intestazione per il file
  2. export_item(self, item), che si occupa, effettivamnte, di esportare la singola riga. item contiene il dizionario passato dallo spider alla pipeline, ed è possibile accedere agli elementi usandone l'indice o il metodo get()
  3. finish_exporting(self), che viene richiamato al termine e lo possiamo utilizzare per chiudere l'esportazione dello spider corrente, ad esempio inserendo un separatore

Questo il codice completo del mio semplice CustomItemExporter:


from scrapy.exporters import BaseItemExporter
from w3lib.html import remove_tags

class CustomItemExporter(BaseItemExporter):

    def __init(self):
        self._configure(kwargs, dont_fail=True)

    def start_exporting(self):
        # self.name viene passato all'exporter dalla pipeline
        self.file.write('<h2>{} - Esito per il sito {}</a></h2>'.format(datetime.now().strftime('%d/%m/%Y'), self.name))

    def export_item(self, item):
        line = "<h3>{}</h3><br />Testo: {}<br />Data: {}".format(item.get('titolo', 'n.d.'), remove_tags(item.get('articolo', 'n.d.')), item.get('data_pubblicazione', 'n.d.'))
        self.file.write(line)

    def finish_exporting(self):
        self.file.write('<hr />')

Purtroppo la documentazione di Scrapy, seppur ben fatta, è un po' carente su certi aspetti, e per capire bene come fare a strutturare un exporter sono dovuto ricorrere al loro repo git: https://github.com/scrapy/scrapy/blob/master/scrapy/exporters.py

Raccogliere tutto ed inviarlo via mail con Mailgun

mailgun

Ok, ora abbiamo degli spider che girano, fanno il loro mestiere (più o meno egregiamente) e scrivono il loro risultato in un file html. Ed ora? Scrapy è basato su Twisted che è un framework asincrono, ed infatti le richieste fatte dai miei spider non sono sequenziali fra loro ma parallele.

Ho trovato diverse soluzioni su StackOverflow, tuttavia quella presentata sulla documentazione si è rivelata la più semplice ed efficace: utilizzando CrawlerProcess faccio partire gli spider in un unico processo, attenendo semplicemente che finisca per eseguire il resto del codice.

from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
from myproject.spiders.Spider1 import Spider1Spider
from myproject.spiders.Spider2 import Spider2Spider
from myproject.spiders.Spider3 import Spider3Spider
import requests

process = CrawlerProcess(settings)
process.crawl(Spider1Spider)
process.crawl(Spider2Spider)
process.crawl(Spider3Spider)
process.start() # <-- Lo script si ferma a questa linea fino a che gli spider non 
# emettono il segnale di fine elaborazione, solo allora l'esecuzione continua

A questo punto non resta che creare la stringa di html finale ed effettuare la chiamata API a Mailgun per inviare la mail completa. Si, lo so, avrei potuto (e forse dovuto) utilizzare un template engine come jinja2 o simili per creare l'html, ma vuoi la semplicità della struttura, vuoi il poco tempo a disposizione, rimando per gli update futuri questa implementazione!

mail_body = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Esito scraping</title></head><body>'

oggi = datetime.now().strftime('%Y%m%d')
for file in ['Spider1', 'Spider2', 'Spider3']:
  with open(settings.get('EXTRACTION_PATH') + '/{}_{}.html'.format(file, oggi), 'r', encoding="utf8") as total:
    mail_body += total.read()

mail_body += '</body></html>'
mail_subject = 'Esito scraping di oggi {}'.format(oggi)

requests.post(settings.get('MAILGUN_ENDPOINT'),
          auth=('api', settings.get('MAILGUN_API')),
          data={'from': settings.get('MAIL_FROM'),
                'to': settings.get('MAIL_TO'),
                'subject': mail_subject,
                'html': mail_body
                }
)

Ho scelto di usare Mailgun perchè m'è parso subito molto semplice da usare e rapido da implementare, e 1000 e-mail al mese gratuite sono un buon punto di partenza per testare semplici applicazioni; unica pecca è che per inviare ad indirizzi diversi da quello dell'account bisogna prima che questi autorizzino esplicitamente la richiesta (altrimenti bisogna passare al tier a pagamento, dove questa limitazione non c'è più), ma alla fine è solo un piccolo fastidio che non porta via tempo.
Se qualcuno ha qualche suggerimento su altri strumenti da usare ben venga! 🙃

Composer update su server con poca memoria, come risolvere il problema

Composer è una delle innovazioni migliori che siano arrivate nell'ecosistema PHP degli ultimi anni (2012), ed è venuto a colmare un gap che stava diventando sempre...

I "side project" di un programmatore

A quale progetto personale (side project) si stia lavorando è probabilmente uno degli argomenti di conversazione più frequenti usato dai programmatori di tutto il mondo, ed...

MatteoVignoli.it   Non perderti nulla da MatteoVignoli.it, ricevi aggiornamenti via mail.