Estrazione dei dati con Scrapy Shell

Il metodo migliore per imparare come estrarre i dati da una pagina HTML usando i selettori CSS o XPath è quello di lanciare la Scrapy Shell da terminale andando ad indicare la pagina che vogliano analizzare:

scrapy shell 'http://quotes.toscrape.com/page/1/'

NOTA: Nel caso di OS Windows ricordarsi di utilizzare le doppie virgolette:

# Doppie virgolette per Windows 
scrapy shell "http://quotes.toscrape.com/page/1/"

La Scrapy Shell è particolarmente adatta durante le fasi di sviluppo o di debug di un progetto di estrazione dati perchè permette di testare rapidamente i selettori da usare ed i risultati che essi producono.

Quello che ottentiamo sara qualcosa di simile a questo:

2019-10-27 00:04:17 [scrapy.core.engine] DEBUG: Crawled (200)  (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler
[s] item {}
[s] request
[s] response <200 http://quotes.toscrape.com/page/1/>
[s] settings
[s] spider
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
In [1]: _

A questo punto abbiamo attivato la nostra console di Scrapy ed abbiamo estratto la pagina che ci interessava che ora è disponibile come oggetto response. Ora possiamo provare ad estrarre degli elementi della pagina utilizzando i selettori CSS con l’oggetto response appena ottenuto:

 
>>> response.css('title') [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

Quello che otteniamo da questo comando è una lista di elementi chiamata SelectorList. che rappresenta una lista di oggetti Selector contenuti in elementi XML/HTML che ci permettono di effettuare ulteriori operazioni di ricerca ed estrazione dati.

Quindi abbiamo ottenuto degli oggetti, dei nodi della pagina HTML, ma non sono quello che cerchiamo effettivamente, nella maggior mparte dei casi quello che cerchiamo è il testo contenuto in essi. Per estrarre il testo possiamo scrivere il comando:

  
>>> response.css('title::text').get()
'Quotes to Scrape'

Con questo comando viene riportato solo il primo elemento trovato dal selettore, nel caso in cui ci fosse una lista di elementi e volessimo ottenerli tutti dobbiamo scrivere:

>>> response.css('title').getall() 
['<title>Quotes to Scrape</title>']

Otteniamo così il risultato sotto forma di array o lista di elementi.

Le combinazioni dei selettori CSS per individuare uno o più elementi della pagina sono davvero numerose, ovviamente dovete conoscere anche un po’ di HTML, DOM (Document Object Model) e di CSS (Cascade Style Sheet). Se siete pigri o volete la vita facile esiste un efficace strumento chiamato SelectorGadget, supportato dalla maggior parte dei browser, che aiuta molto nella ricerca del giusto selettore CSS.

Introduzione ai selettori XPath

A fianco dei selettori CSS esiste un altro tipo di selettori che hanno maggiore precisione, questi possono essere utilizzati con le espressioni XPath.

 
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'

I selettori XPath sono molto potenti e sono alla base delle ricerche dei selettori in Scrapy. Infatti i selettori CSS vengono trasformati da Scrapy in selettori XPath mentre è in esecuzione. Anche se hanno una sintassi e delle regole più difficili da comprendere a prima vista , le espressioni XPath permettono di navigare all’interno della struttura del documento ed avere una maggiore precisione. L’argomento è ampio è bene cercare della documentazione dedicata per comprendere meglio il loro utilizzo Nel pezzo successivo andiamo a vedere come trovare i selettori XPath utilizzando lo strumento per sviluppatori del nostro web browser.

Creare un progetto in Scrapy

Andiamo a vedere come realizzare il nostro primo progetto in Scrapy, supponiamo che abbiate già provveduto all’installazione, ma se così non fosse è sufficiente utilizzare il gestore di pacchetti python pip scrivendo nel nostro terminale:

pip install scrapy

Molto semplice, in pochi istanti il nostro sistema sarà pronto per accettare il nostro primo comando con il quale andremo a creare un progetto Scrapy, basta editare:

scrapy startproject tutorial

Questo andrà a creare una directory chiamata tutorial con il seguente contenuto:

tutorial/
scrapy.cfg # file per le configurazioni
tutorial/  # directory dove scriveremo il nostro codice
__init__.py
items.py # file per la definizione degli item
middlewares.py # file per la definizione dei middlewares
pipelines.py # file per la definizione dei pipeline
settings.py # file per la definizione delle impostazioni
spiders/ # directory dove metteremo gli spiders
__init__.py

Il nostro primo Spider

Gli ‘Spiders‘ sono classi che Scrapy utilizza per raccogliere informazioni da un sito Web (o da un gruppo di siti Web). Devono sotto-classare scrapy.Spider e definire le richieste iniziali da effettuare, possiamo anche indicare eventualmente come seguire i collegamenti nelle pagine e come analizzare il contenuto della pagina scaricata per estrarre i dati.

Questo è il codice per il nostro primo Spider. Salvalo in un file chiamato ‘quotes_spider.py‘ nella directory tutorial / spiders nel tuo progetto:

import scrapy 

class QuotesSpider(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)

Abbiamo creato una classe QuoteSpider che è una sottoclasse scrapy.Spider e definisce alcuni attributi e metodi:

  • name: identifica il nostro Spider. Deve essere univoco all’interno di un progetto, ovvero non è possibile impostare lo stesso nome per Spider diversi.
  • start_requests (): deve restituire un iterabile di Requests (puoi restituire un elenco di URLs o scrivere una funzione generatore) da cui Spider inizierà a lavorare. Le richieste successive verranno generate successivamente da queste richieste iniziali.
  • parse (): un metodo che verrà chiamato per gestire la risposta scaricata per ciascuna delle richieste fatte. Il parametro response è un’istanza di TextResponse che contiene il contenuto della pagina e ha ulteriori metodi utili per gestirlo. Il metodo parse () di solito analizza la risposta, estraendo i dati scartati come dicts e anche trovando nuovi URL da seguire e creando nuove richieste da essi.

Lanciamo il nostro Spider

Per far funzionare il nostro ragno, saliamo nella directory del progetto ed eseguiamo:

scrapy crawl quotes

Questo comando esegue lo spider chiamato ‘quotes’ che abbiamo appena creato, ed invierà alcune richieste per il dominio quotes.toscrape.com come abbiamo indicato nella nostra funzione start_requests().

Ora controllando i file nella directory corrente. Possiamo notare che sono stati creati due nuovi file: quotes-1.html e quotes-2.html, con il contenuto dei i rispettivi URL, come indicato dal nostro metodo parse().

Scrapy ha pianificato gli oggetti scrapy.Request restituiti dal metodo start_requests del nostro Spider. Alla ricezione di una risposta per ciascuno, crea un’istanza di oggetti Response e chiama il metodo di callback associato alla richiesta (in questo caso, il metodo di parse) passando la risposta come argomento.

Nota: il tutorial appena presentato è stato tratto dal tutorial ufficiale del sito del projetto scrapy.org. Viene presentato tradotto in Italiano per pura divulgazione a scopo educativo.

Introduzione a Scrapy

Scrapy è un framework di alto livello per il web crawling ed il web scraping, viene usato per navigare tra le pagine in rete ed estrarre dati da esse. Scrapy può essere utilizzato per molteplici scopi dall’estrazione di dati a fini statistici, dal monitoraggio di prezzi, alla creazione di arvhivi e basi di dati ed al testing automation.

Uno dei principali vantaggi di Scrapy: è che le richieste vengono pianificate ed elaborate in modo asincrono. Ciò significa che Scrapy non ha bisogno di aspettare che una richiesta sia finita ed elaborata, può inviare un’altra richiesta o fare altre cose nel frattempo. Ciò significa anche che altre richieste possono continuare anche se alcune richieste falliscono o si verifica un errore durante la gestione permettendo di completare il lavoro di ricerca ed estrazione dati senza interruzioni del programma.

Sebbene ciò ti consenta di eseguire ricerche per indicizzazione molto veloci (inviando contemporaneamente più richieste simultanee, in modo tollerante ai guasti) Scrapy permette anche di avere il controllo della ricerca per indicizzazione attraverso alcune impostazioni. Puoi fare cose come impostare un ritardo di download tra ogni richiesta, limitare la quantità di richieste simultanee per dominio o per IP e persino usare un’estensione a limitazione automatica che prova a capirle automaticamente.

Panoramica dell’architettura

Immagine originale tratta dal sito scrapy.org https://docs.scrapy.org/en/latest/_images/scrapy_architecture_02.png

Il flusso di dati in Scrapy è controllato da un motore centrale (EXECUTION ENGINE) ed è rappresentato dai seguenti passaggi:

  1. Il motore ottiene gli indirizzi di partenza per far partire il crawler dai vari SPIDERS di un progetto.
  2. Il motore pianifica le richieste nell’utilità di pianificazione (SCHEDULER) e richiede la scansione delle richieste successive.
  3. Lo SCHEDULER restituisce le richieste successive al motore.
  4. Il motore invia le richieste al DOWNLOADER, passando attraverso i Downloader Middlewares (tramite i process_request ()).
  5. Una volta terminato il download della pagina, DOWNLOADER genera una risposta (con quella pagina) e la invia all’ENGINE, passando attraverso i Downloader Middlewares (tramite i process_response ()).
  6. Il motore riceve la risposta dal DOWNLOADER e la invia allo SPIDER per l’elaborazione, passando attraverso Spider Middleware (process_spider_input ()).
  7. Lo SPIDER elabora la risposta e restituisce gli oggetti raschiati dalla pagina e le nuove richieste (da seguire) al motore, passando attraverso il middleware Spider (process_spider_output ()).
  8. Il motore invia gli oggetti elaborati alle pipeline degli oggetti (ITEM PIPELINE), quindi invia le richieste elaborate all’utilità di pianificazione (SCHEDULER) e chiede eventuali richieste successive da sottoporre a scansione.
  9. Il processo si ripete (dal passaggio 1) fino a quando non ci sono più richieste dallo SCHEDULER.

Nota: il tutorial appena presentato è stato tratto dal tutorial ufficiale del sito del projetto scrapy.org. Viene presentato tradotto in Italiano per pura divulgazione a scopo educativo.

Python, Requests e Tor proxy

Nel caso in cui avessimo necessita di utilizzare un proxy per estrarre pagine web in modo anonimo possiamo utilizzare l’accoppiata Requests e Tor.

Request è una libreria HTTP di Python che permette di effettuare in modo semplice delle chiamate ad un web server.

Tor è un software che permette di instaurare un servizio di comunicazione anonimo in grado di tutelare la privacy di chi naviga sfruttando i nodi del suo circuito.

Ovviamente dobbiamo installare il programma Tor, potete trovarlo a questo indirizzo: torproject.org.

Dobbiamo anche avere installato la liberia Requests ed una libreria accessoria chiamata pysocks, basta semplicemente utilizzare pip:

pip install requests
pip install pysocks

Per dimostrare il funzionamento andiamo ad usare come servizio verifica del nostro IP il sito httpbin.org. Questo site permette di effettuare vari test con le chiamate HTTP.

Nel nostro caso andiamo a verificare l’indirizo IP che usiamo per navigare andano a fare una richiesta get all’indirizzo: ‘http://httpbin.org/ip‘.

import requests
page = requests.get('http://httpbin.org/ip')
ip = page.text
print(ip)

Otteniamo come risultato in nostro ip, nel mio caso il seguente:
{
"origin": "93.35.165.226, 93.35.165.226"
}

Per utilizzare Tor come proxy per le nostre chiamate dobbiamo creare una sessione con Request, impostare il parametro proxies per poi procedere come abbiamo fatto nella chiamata precedente:

session = requests.session()
session.proxies = {'http':'socks5://127.0.0.1:9150',
'https': 'socks5://127.0.0.1:9150'}
anonymous_request = session.get('http://httpbin.org/ip')
print(anonymous_request.text)

Otteniamo come risultato quello dell'ultimo nodo del circuito Tor che stiamo usando:

{
"origin": "209.141.61.45, 209.141.61.45"
}

I proxies che andiamo ad impostare, per le chiamate HTTP ed HTTPS, vanno a puntare alla porta 9150 che è quella impostata nella nostra macchina (localhost = 127.0.0.1) per le chiamante al servizio di Tor.

Di seguito vi riporto il codice completo dello script ed il link per scaricarlo da GitHub.

import requests
# Chiamata in chiaro
simple_request = requests.get('http://httpbin.org/ip')
response = simple_request.json()
print(f"Questo è il mio indirizzo in chiaro:{response['origin']}")

# Chiamata usando Tor
session = requests.session()
session.proxies = {'http': 'socks5://127.0.0.1:9150',
'https':'socks5://127.0.0.1:9150'}
anonymous_request = session.get('http://httpbin.org/ip')
response = anonymous_request.json()
print(f"Questo è il mio indirizzo mascherato:{response['origin']}")

Dato che il sito che interroghiamo ci risponde con dei dati in formato JSON abbiamo fatto alcune modifiche ed abbiamo usato la funzione json() per formattare i dati ricevuti.

Utilizzo di indirizzi IP multipli di server proxy.

Un problema comune che si incontra durante la scansione di un sito è quello di venir identificati come un agente malevolo e venir bloccati attraverso l’iscrizione del proprio IP in una black-list del sito.

Ci sono varie tecniche per evitare questo problema, in questo articolo vedremo come proteggerci con l’uso di un proxies che cambiano ad ogni chiamata che effettuiamo al server che stiamo interrogando.

L’utilizzo di proxy e di indirizzi IP rotanti in combinazione con user-agents rotanti può aiutarti a superare la maggior parte delle misure anti-scraping e impedire di essere rilevato come un bot.

proxy rotation www.ferromauro.it

Il concetto di rotazione degli indirizzi IP durante lo scraping è semplice: si cerca far credere al sito web oggetto dell’analisi che non sei un singolo “bot” o una persona che accede con più richieste, ma più utenti che accedono al sito da più postazioni. Se questa tecnica viene utilizzata in modo corretto le possibilità di essere bloccati sono minime.

L’utilizzo di un proxy con Python 3 e la libreria Requests è molto semplice:

import requests
url= 'https://httpbin.org/ip'
proxies = {
  'http': 'http://10.10.1.10:3128',
  'https': 'http://10.10.1.10:1080',
}
requests.get(url, proxies=proxies)
print(response.json())

Nell’esempio sopra riportato viene indicato un indirizzo IP di un proxy gratuito, che molto probabilmente non sarà più in funzione nel momento in cui state leggendo quest’articolo.

Per individuare una lista di proxies gratuiti ci si può affidare a diversi siti web come ad esempio:

Dato che i proxies di queste liste cambiano piuttosto di frequente dobbiamo trovare un modo per automatizzare il controllo dei proxies disponibili, fortunatamente ci piace lo scraping e possiamo fare affidamento su Python!

Partiamo dall’ultimo sito dell’elenco, possiamo una funzione che effettui una chiamata al sito per scaricare la pagina con la libreria ‘requests‘ e successivamente con la libreria ‘lxml‘ andare a cercare le informazioni che ci interessano: l’indirizzo IP e la porta dei proxies con livello di sicurezza ‘elite’.

def proxy_list():
    url = 'https://free-proxy-list.net/'
    response = requests.get(url)
    parser = etree.HTML(response.content)
    proxies = set()
    for i in parser.xpath('//*[@id="proxylisttable"]/tbody/tr')[:10]:
        if i.xpath('.//td[5][contains(text(),"elite proxy")]'):
            proxy = ":".join([i.xpath('.//td[1]/text()')[0], i.xpath('.//td[2]/text()')[0]])
            proxies.add(proxy)
    return(proxies)

Questo è il risultato che ottengo mentre scrivo, mentre leggete l’articolo la pagina potrebbe avere subito delle variazioni e potrebbe essere necessario modificare il percorso xpath di alcuni elementi:


{'1.20.101.175:38263', '190.248.136.18:46090', '125.25.45.120:49887', '140.227.204.8:3128', '109.172.65.134:58147', '190.90.193.96:32455', '170.247.31.159:44669', '88.116.12.182:32871', '118.175.93.185:47007', '86.120.78.21:33666'}

Potete provare a fare lo stesso lavoro con gli altri siti elencati: noterete che per via di alcuni accorgimenti adottati il compito sarà più complicato (ma non impossibile!).

Ora che abbiamo un metodo per avere una lista sempre aggiornata di indirizzi IP di server proxy possiamo creare un programma che utilizzi in modo ciclico gli indirizzi IP della lista simulando che le nostre richieste provengano da diverse località.

Possiamo fare una prova verificando l’indirizzo IP con il sito httpbin.org (cliccando sul link vedrete il vostro indirizzo IP attualmente in uso):

import requests
from lxml import etree
from itertools import cycle

def main():
    proxies = proxy_list()
    proxy_pool = cycle(proxies)
    
    url = 'https://httpbin.org/ip'
    for i in range(1,16):
        proxy = next(proxy_pool)
        print(f'Request {i}')
        try:
            response = requests.get(url,proxies={"http": proxy, "https": proxy})
            print(response.json())
        except:
            print("Skipping. Proxy Not Responding")    
 
    
def proxy_list():
    url = 'https://free-proxy-list.net/'
    response = requests.get(url)
    parser = etree.HTML(response.content)
    proxies = set()
    for i in parser.xpath('//*[@id="proxylisttable"]/tbody/tr')[:10]:
        if i.xpath('.//td[5][contains(text(),"elite proxy")]'):
            proxy = ":".join([i.xpath('.//td[1]/text()')[0], i.xpath('.//td[2]/text()')[0]])
            proxies.add(proxy)
    return(proxies)

if __name__ == '__main__':
    main()

Il risultato ottenuto sarà simile al seguente:

Request 1
{'origin': '202.93.128.98'}
Request 2
{'origin': '206.189.148.118'}
Request 3
{'origin': '129.205.160.160'}
Request 4
{'origin': '109.207.112.212'}
Request 5
{'origin': '77.82.88.249'}
Request 6
{'origin': '125.25.165.127'}
Request 7
{'origin': '36.89.181.155'}
Request 8
{'origin': '5.9.239.164'}
Request 9
{'origin': '202.93.128.98'}
Request 10
{'origin': '206.189.148.118'}
Request 11
{'origin': '129.205.160.160'}
Request 12
{'origin': '109.207.112.212'}
Request 13
{'origin': '77.82.88.249'}
Request 14
{'origin': '125.25.165.127'}
Request 15
{'origin': '36.89.181.155'}

In alcuni casi la connessione con un proxy può fallire per questo motivo è bene creare una chiamata try except per verificare che tutto vada bene.

Questo semplice script permette di usufruire di proxy server sempre aggiornati utile durante un lavoro di scansione, se unito ad un metodo di rotazione dell’user-agent costituisce una buona difesa contro il blocco da parte di un sito web.

Si consiglia di utilizzare solo proxy con livello di protezione alto (elite proxy), solo questi permettono di essere simulare la chiamata come se si trattasse di un utente reale che non stia utilizzando alcun proxy.

Si segnala che l’utilizzo di un proxy gratuito in alcuni casi è inefficace e sarebbe bene utilizzare un proxy a pagamento. I proxy gratuiti a volte vengono inseriti in black-list e subito respinti dai siti con una valida protezione.