Web Scraping

Cos'è e a cosa serve il web scraping

Per web scraping si intende un processo per automatizzare la ricerca e il download di contenuti (file o anche solo testo) da pagine web. È importante sapere che questa pratica non sempre è consentita: molti siti non se ne preoccupano, altri addirittura sono strutturati in modo da agevolarla, ma alcuni sono contrari. Ciò succede principalmente in siti che vogliono essere visitati, come quelli che vivono su pubblicità e simili. È sempre bene, quindi, leggere (se ci sono) i termini e le condizioni del sito che volgiamo "scrapeare".

A cosa serve il web scraping? Come tutte le automatizzazioni, principalmente a risparmiare tempo. Però, come vedremo, il tempo che si può guadagnare è tanto. Possiamo, ad esempio, scaricare e filtrare automaticamente centinaia di offerte di lavoro, o prezzi di prodotti online, oppure ancora meglio predisporre un computer che stia sempre acceso (un server) e che controlli queste cose nel tempo, avvisandoci appena nota cambiamenti interessanti.

Prima d'iniziare, c'è da dire una cosa: il web scraping spesso non è il metodo più efficace per scaricare dati dal web. Infatti, un sito web può cambiare nel tempo, in modo indipendente da noi. La struttura e la grafica di un sito web, in particolare, vengono spesso aggiornati nel tempo e questo può creare dei problemi ai nostri sistemi di 'lettura automatica'. Inoltre, questa tecnica ci permette di leggere i contenuti statici (HTML) di un sito. Leggere i contenuti dinamici, quindi quelli trasmessi tramite JS, è spesso semplice: la cosa complicata è capire come cambiano, e cosa viene trasmesso. Vedremo un esempio anche di questo. In alcuni casi, per ovviare a questi problemi esistono le API (Application Programming Interface): delle specie di comandi con cui noi possiamo chiedere direttamente ad un sito i dati di cui abbiamo bisogno. Ovviamente questa cosa deve essere predisposta dai fornitori dei dati, che non sempre hanno intenzione di rendere facilmente acessibile il loro materiale e, quando lo fanno, spesso lo fanno a pagamento. Se esistono, però, le API sono un metodo molto più efficace e durevole nel tempo.

BeautifulSoup

La libreria che utilizzeremo oggi per il web scraping si chiama BeautifulSoup. Può venire installata tramite pip:

In [ ]:
!pip3 install beautifulsoup4

Una volta installata, importiamola:

In [ ]:
from bs4 import BeautifulSoup

Un'altra libreria che ci servirà è la libreria request dal pacchetto urllib (questa dovrebbe essere preinstallata, altrimenti potete scaricarla con il soilto comando):

In [ ]:
import urllib.request

Analisi Preliminare

Andiamo ora a vedere a cosa serve questa libreria. Prenderemo come esempio il sito web Monster, un sito di annunci di lavoro americano. La prima cosa da fare è capire come funziona il sito: abbiamo delle offerte di lavoro a sinistra, al centro un pannello dove compare l'offerta su cui abbiamo cliccato, e in alto due form di ricerca per tipo di lavoro e area geografica. Osserviamo che ogni volta che cambiamo dei parametri, l'URL cambia di conseguenza, ma sempre in modo simile: in particolare ha sempre la forma:

https://www.monster.com/jobs/search/?q= (Tipo di lavoro) &where= (Luogo) &jobid= (Numero identificativo del lavoro)

Questa cosa è molto comune nei siti web: dopo l'url, /? indica l'inizio di un elenco di variabili, che sono inserite e valutate nell'url come variabile1=valore1&variabile2=valore2, separate da &. Capire cosa rappresentano le variabili (in questo caso è abbastanza chiaro da subito) ci aiuta a sapere cosa dobbiamo cercare.

Un altro strumento utile è la 'developer console', che potete aprire dal browser premendo F12. Questo tool ci permette di vedere una serie di informazioni, come il sorgente della pagina (l'HTML), oppure i pacchetti che vengono via via inviati alla pagina dal server. Non spaventatevi se non capite quello che c'è scritto, non è complicato ma bisogna abituarsi un po'. Ogni contenuto della pagina è racchiuso da 'tag', ovvero istruzioni tra < >, che vengono chiusi con </ >. Il tag 'generico' è il div: contiene ogni cosa, e in genere le informazioni specifiche le troviamo al suo interno. Un esempio è < div id="...">, che ci indica che tutto il contenuto di questo 'div' ha un identificativo. Un altro tag importante è < a href="..." >, che indica i link. Possiamo vedere diversi esempi nella pagina, che chiariranno meglio.

Download della pagina

Andiamo adesso a scaricare la pagina:

In [ ]:
url = 'https://www.monster.com/jobs/search/?q=Software-Developer&where=Australia'
page = urllib.request.urlopen(url)

Con questa url stiamo cercando i lavori come Software Developer in Australia. Il comando request.urlopen 'richiede' al sito web la sorgente html della pagina. Attenzione: come accennato sopra, il sorgente HTML di una pagina è il suo contenuto statico. Se la pagina cambia, bisogna fare attenzione o adottare altre tecniche.

Importiamo ora la pagina scaricata dentro alla nostra libreria BeautifulSuop:

In [ ]:
soup = BeautifulSoup(page, 'html.parser')

Il secondo parametro serve ad indicare cosa stiamo cercando di leggere. Di nuovo, fate attenzione: se passa troppo tempo il comando urlopen non è più valido (in realtà non 'scarica' nulla, ma crea una connessione che viene disattivata dopo un po' di tempo). Dobbiamo quindi rilanciarlo prima di importare la pagina nella variabile soup (da quel momento non possiamo più perderla).

Vediamo ora come ottenere specifici elementi dalla nostra pagina. Abbiamo visto che alcuni div contengono un id: generalmente ciò è vero per la maggior parte degli elementi più importanti di una pagina. Vediamo come ottenere gli elementi con un certo id:

In [ ]:
results = soup.find(id='ResultsContainer')
print(results.prettify())

ResultsContainer, come abbiamo visto analizzando la pagina, è il contenitore delle offerte di lavoro. Il comando prettify ci fa stampare l'html ordinato e indentato correttamente.

Vediamo ora che le offerte di lavoro sono contenute in tag di tipo section, con classe card-content. Abbiamo un comando apposta per cercarle:

In [ ]:
job_elems = results.find_all('section', class_='card-content')

Il comando find_all() ci restituisce un iterable con i contenuti che ci interessano (le offerte di lavoro):

In [ ]:
for job_elem in job_elems:
    print(job_elem, end='\n'*2)

Evidentemente è ancora un po' scomodo da leggere. Proviamo allora a localizzare meglio i dati contenuti, e ad impostare la bozza del nostro "scraper": in ogni offerta abbiamo un tag h2 (heading, intestazione, di secondo livello) con la classe title, che sarà il titolo della nostra offerta, un div company, e un div location. Mostriamo solo quello che ci interessa:

In [ ]:
for job_elem in job_elems:
    title_elem = job_elem.find('h2', class_='title')
    company_elem = job_elem.find('div', class_='company')
    location_elem = job_elem.find('div', class_='location')
    print(title_elem)
    print(company_elem)
    print(location_elem)
    print()

Questo non è ancora il risultato che ci interessa, ma ci serve comunque per un utlimo controllo. Scorrendo tra i campi interessanti notiamo che alcune offerte non li hanno: si tratta probabilmente di elementi mal formati o di pubblicità. In ogni caso, ciò che andremo a fare ci darebbe errore in questi casi; dobbiamo quindi trattarli separatamente.

Ora che il grosso è fatto, possiamo prendere il testo da questi tag, semplicemente con la sintassi elemento.text, dove elemento sono i div che otteniamo dal ciclo. Ogni passo in cui ci sono dati mancanti viene saltato, per evitare errori, mentre la funzione strip() serve solo a cancellare spazi iniziali e finali:

In [ ]:
for job_elem in job_elems:
    title_elem = job_elem.find('h2', class_='title')
    company_elem = job_elem.find('div', class_='company')
    location_elem = job_elem.find('div', class_='location')
    if None in (title_elem, company_elem, location_elem):
        continue
    print(title_elem.text.strip())
    print(company_elem.text.strip())
    print(location_elem.text.strip())
    print()

Poniamo ora di essere interessati solo a lavori per amanti del pyhton. BeautifulSoup ci permette di effettuare una comoda ricerca per stringa direttamente dal comando find_all:

In [ ]:
python_jobs = results.find_all('h2',string="python")
print(python_jobs)

La cosa sembra non funzionare: siamo sicuri che ci fosse almeno un'offerta per un Python Developer, e stiamo cercando in tutte le h2 della pagina. Tuttavia, noi stiamo cercando "python" mentre sopra c'è scritto "Python": questa differenza, in questo caso risolvibile facilmente, nel caso ad esempio di due parole da vita a 4 combinazioni, ma comunque spesso può portare a complicazioni. Per questo è sempre meglio passare non una stringa ma una funzione anonima come parametro per string:

In [ ]:
python_jobs = results.find_all('h2',string=lambda text: "python" in text.lower())
print(len(python_jobs))

Un'ultima cosa è molto importante: abbiamo visto finora come ottenere il testo da un tag. Spesso però i tag contengono altre informazioni importanti: un esempio sono i link, che figurano come attributo href di un tag a (< a href="http....). Per ottenere gli attributi di un certo tag, utilizziamo la solita sintassi di accesso con parentesi quadre:

In [ ]:
for p_job in python_jobs:
    link = p_job.find('a')['href']
    print(p_job.text.strip())
    print(f"Apply here: {link}\n")

Dati dinamici: i JSON

Può capitare le informazioni in un sito cambino senza modificare l'URL: di solito, in questi casi, i dati vengono spediti al sito tramite richieste al server successive a quella principale, usando pacchetti chiamati JSON. Ovviamente anche queste richieste dovranno avere un loro indirizzo. Se siamo di fronte a richieste di questo tipo, dobbiamo soltato cercare l'URL della richiesta, perchè il grosso lavoro di ordinamento dei dati è già stato fatto da chi gestisce il server.

Per individuare gli indirizzi che ci servono possiamo tornare sulla console di sviluppo del browser, alla sezione 'rete', e cercare i pacchetti json. Solitamente si può filtrare solo le richieste di tipo 'XHR'.

Andiamo per esempio sul sito www.gazzetta.it. A sinistra vediamo un pannello con le ultime notizie. Se primiamo F12 e ricarichiamo la pagina, filtrando i pacchetti di rete di tipo XHR, troviamo in cima un 'latest.json' che sembra fare il caso nostro. Su 'Risposta' vediamo che effettivamente sono le ultime notizie, e dalla sezione 'Header' possiamo ottenere il link di questa richiesta:

https://components2.gazzettaobjects.it/rcs_gaz_searchapi/v1/latest.json

Provando ad inserirlo nel browser vediamo subito che forma ha questo contenuto. Proviamo ora a leggerlo: per farlo ci servirà anche la libreria json di python.

In [ ]:
import json
url = 'https://components2.gazzettaobjects.it/rcs_gaz_searchapi/v1/latest.json'
response = urllib.request.urlopen(url)
jresponse = json.load(response)
print(jresponse)

Guardando attentamente vediamo che si tratta di un dizionario, con la prima chiave 'responseHeader' che contiene solo dati tecnici, mentre la seconda, 'response', contiene un nuovo dizionario. Alla voce 'docs' di questo dizionario c'è una lista di altri dizionari che sono ciò che ci interessa. Questa struttura annidata è tipica dei json, per esplorarla più velocemente il metodo più comodo è usare un formattatore (qualcosa che indenti i sottoelementi) oppure usare il browser, che ne ha uno preinstallato. Tuttavia scoperta la struttura accedervi è semplice, dato che sono solo liste e dizionari, e possiamo anche sfruttare la libreria pprint, costruita apposta per formattare queste struttre.

In [ ]:
import pprint
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(jresponse['response']['docs'][0])

Chiarita la definitiva struttura delle nostre notizie, possiamo velocemente mostrarle (i comandi Image servono solo per le immagini, non dovete preoccuparvi: ce ne occuperemo più avanti, ma così viene una cosa molto più carina).

In [ ]:
a = jresponse['response']['docs'][0]
json.loads(a['image'])['images'][0]['u']
In [ ]:
for i in jresponse['response']['docs']:
    print('------------------')
    print(i['headline'])
    print('------------------')
    print(i['standFirst'])
    print('------------------')
    print('URL: '+i['url'])
    print('------------------')
    print()
    print()

Due esercizi

Provate, come esercizio, a scaricare in automatico tutti i link contenuti in una pagina. Prendete ad esempio questa. Si tratta di una situazione abbastanza realistica nel mondo della ricerca di dati online. Potete anche trovare qui un esempio di svolgimento.

Prezzi Amazon

Un buon modo per sfruttare le conoscenze acquisite fin qui è provare a scrivere una funzione che, dato in input un link di amazon, ci restituisca il prezzo del prodotto. In questo modo, facendo girare la funzione ad esempio una volta ogni minuto (si può fare in automatico) potremo venire avvisati quando cala il prezzo. Anche sulle modalità di avviso ci possiamo sbizzarrire: è molto semplice ad esempio inviare una mail o un messaggio telegram direttamente da python, anche se non vedremo come in questo corso.