Beautiful Soup
Nei capitoli precedenti abbiamo usato requests per interrogare API e servizi che restituiscono JSON (Open-Meteo, httpbin.org).
Ma buona parte del web non espone API: i dati sono “intrappolati” dentro pagine HTML pensate per essere lette da un umano, non da un programma.
Il web scraping è la tecnica per estrarre dati strutturati da queste pagine, e Beautiful Soup è la libreria Python più diffusa per farlo.
Per installare Beautiful Soup, dal terminale di Thonny, esegui:
pip install beautifulsoup4
Beautiful Soup non fa il parsing da solo: si appoggia a un parser esterno. Le due opzioni principali sono:
| Parser | Vantaggi | Svantaggi |
|---|---|---|
html.parser | Incluso in Python, nessuna dipendenza | Leggermente più lento su documenti enormi |
lxml | Più veloce, più tollerante su HTML malformato | Richiede installazione separata (pip install lxml) |
Tip
Per quello che serve a noi…
html.parserbasta e avanza!
Se il web scraping vi prende proprio la mano… provate anchelxml!!!
Dal testo grezzo al DOM
Quando facciamo requests.get(), otteniamo una stringa di testo:
import requests
response = requests.get("https://quotes.toscrape.com")
print(type(response.text)) # <class 'str'>
print(response.text) # una stringa HTML, difficile da "interrogare"
Una stringa non è comoda da esplorare: per trovare un titolo, un prezzo, un link, dovremmo scrivere codice di parsing manuale (regex, split, ecc.), fragile e propenso a errori. Beautiful Soup trasforma quella stringa in un albero navigabile — il DOM (Document Object Model) — che possiamo interrogare con metodi dedicati.
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
print(soup.prettify())
Confrontando response.text e soup.prettify(), il contenuto è identico, ma la seconda rappresentazione è indentata e navigabile:
è la stessa differenza che c’è tra leggere un libro senza punteggiatura e uno formattato correttamente.
Prima del web scraping: il controllo di robots.txt
Prima ancora di scrivere una riga di codice che estrae dati, dobbiamo chiederci: posso farlo?
La convenzione robots.txt è il primo strumento per rispondere a questa domanda: è un file pubblico, presente alla radice di quasi ogni sito,
che dichiara quali parti del sito possono essere visitate da programmi automatici (crawler, bot, scraper) e quali no.
import requests
def controlla_robots(base_url):
robots_url = base_url + "/robots.txt"
r = requests.get(robots_url)
if r.status_code == 200:
print(f"robots.txt trovato su {robots_url}:\n")
print(r.text)
else:
print(f"Nessun robots.txt trovato (status {r.status_code}).")
print("Attenzione: l'assenza di robots.txt non significa 'via libera' automatico.")
return
controlla_robots("https://sito_di_cui_vuoi_fare_scraping.boh")
Un robots.txt tipico contiene righe come:
User-agent: *
Disallow: /
User-agent: * significa “questa regola vale per tutti i bot”; Disallow: / significa “non è permesso accedere a nessuna pagina del sito in modo automatizzato”.
Se proviamo questo script sull’indirizzo dove la nostra scuola pubblica l’orario (cspace.spaggiari.eu), troviamo esattamente questa situazione: il dominio, gestito dal fornitore della piattaforma, vieta l’accesso automatizzato — indipendentemente dal fatto che il contenuto pubblicato (l’orario) sia stato caricato dalla scuola stessa.
Qui il discorso si chiude! Quando un sito dichiara Disallow, un programma(tore) serio si adegua e non procede. Non è solo una questione tecnica: è una convenzione rispettata dai crawler dei motori di ricerca, dagli strumenti professionali, e che dovremmo rispettare anche noi.
Note
robots.txtnon è una legge, ma è una convenzione universalmente rispettata. Ignorarlo è considerato scorretto anche quando non ci sono conseguenze legali dirette.I
Termini di Servizio(ToS) possono vietare lo scraping anche di contenuti pubblici. Un sito può essere liberamente visitabile da un browser e allo stesso tempo vietare esplicitamente l’accesso automatizzato nei propri ToS.I dati personali o sensibili non si scrapano mai senza una base legale chiara, anche quando si tratta dei “propri” dati. Un esempio concreto: il registro elettronico della scuola contiene voti e informazioni personali protette da GDPR e da ToS specifici; anche uno scraper scritto per leggere solo i propri voti sarebbe un accesso automatizzato non previsto dal sistema, con possibili implicazioni sia contrattuali che, in casi estremi, penali (accesso abusivo a sistema informatico).
Rate limiting e identificazione onesta: uno scraper non deve martellare un server con richieste ravvicinate, e dovrebbe identificarsi con uno
User-Agentonesto invece di camuffarsi da browser per aggirare controlli.
Navigazione base del DOM
Una volta ottenuto l’oggetto soup, i metodi principali per cercare elementi sono:
# Ricerca per tag (il primo che trova)
titolo = soup.find("h3")
# Ricerca per tag (tutti quelli che trova, restituisce una lista)
titoli = soup.find_all("h3")
# Ricerca per classe CSS (attenzione alla underscore: 'class' è parola riservata in Python)
prezzi = soup.find_all("p", class_="price_color")
# Ricerca per id
header = soup.find(id="header")
# Selettori CSS, come nei fogli di stile (utile se avete già fatto il corso di web development)
articoli = soup.select("article.product_pod")
titolo_singolo = soup.select_one("h3 a")
select() e select_one() accettano la stessa sintassi dei selettori CSS (.classe, #id, tag.classe, discendenti con lo spazio, ecc.): dimostrate
che avevate capito quanto studiato sul CSS (oppure andate a riguardarlo!!!)
Estrazione dati
Trovare un elemento è solo metà del lavoro: bisogna poi estrarne il contenuto o gli attributi.
for articolo in soup.select("article.product_pod"):
titolo = articolo.h3.a["title"] # attributo, con sintassi a dizionario
prezzo = articolo.select_one("p.price_color").text # testo contenuto nel tag
print(f"{titolo}: {prezzo}")
Punti da tenere a mente:
.textrestituisce il testo contenuto in un elemento (e nei suoi figli), come stringa.elemento["attributo"]eelemento.get("attributo")fanno la stessa cosa, ma.get()restituisceNoneinvece di sollevare un’eccezione se l’attributo non esiste — più sicuro quando l’HTML non è del tutto regolare.- Se
find()oselect_one()non trovano nulla, restituisconoNone: chiamare.textsuNonegenera un errore. Su HTML “selvaggio” (non scritto da voi) conviene sempre controllare:
elemento = articolo.select_one("p.price_color")
prezzo = elemento.text if elemento else "N/D"
Tabelle HTML
Gli orari scolastici, i listini prezzi, e molti altri dati tabulari sul web sono rappresentati con i tag <table>, <tr> (riga), <td>/<th> (cella):
tabella = soup.find("table")
righe = tabella.find_all("tr")
for riga in righe:
celle = riga.find_all(["td", "th"])
valori = []
for cella in celle:
valori.append(cella.text.strip())
print(valori)
Warning
.strip()è importante!
Il testo estratto da HTML contiene spesso spazi, tabulazioni e/o newline residui!
Salvataggio dei dati estratti
Una volta estratti, i dati vanno tipicamente salvati in un formato riutilizzabile… ad esempio, il formato CSV!!!
import csv
campi = ("Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato")
file = open("orario.csv", "w", newline="")
scrittore = csv.DictWriter(file, campi)
scrittore.writeheader()
for riga in righe_estratte:
scrittore.writerow(riga)
file.close()
Esercizi
Esercizio bs01 — Controllo robots.txt
Scrivere uno script che, data una URL come parametro, scarica e stampa il robots.txt del dominio corrispondente, evidenziando eventuali righe Disallow.
Esercizio bs02 — Estrazione titoli
Da https://quotes.toscrape.com, estrarre tutte le citazioni presenti nella pagina insieme al rispettivo autore, stampandole in coppie “citazione” - autore.
Esercizio bs03 — Estrazione link
Data una pagina qualsiasi, estrarre tutti i link (<a href="...">) e stampare solo quelli che iniziano per http (scartando link relativi o interni tipo #top).
Esercizio bs04 — Conteggio elementi
Su https://books.toscrape.com, contare quanti prodotti sono presenti nella pagina (quante volte compare il tag/classe corrispondente a un prodotto).
Esercizio bs05 — Da tabella a CSV
Data una pagina con una tabella HTML generica, estrarne il contenuto riga per riga e salvarlo in un file CSV, riutilizzando il modulo csv visto nel capitolo precedente.
Progetto finale: L’orario scolastico
Verificare il robots.txt di https://cspace.spaggiari.eu e osservare che l’accesso automatizzato non è consentito.
Motivare per iscritto, in una o due frasi, perché non si procede con lo scraping di quel link.
Verificare il robots.txt di https://sito_che_il_prof_vi_deve_dire.com e verificare che l’accesso automatizzato è consentito.
Il sito ospita una copia identica dell’orario scolastico.
Estrarre la tabella oraria della propria classe da quella pagina e salvarla in un file CSV con colonne giorno, ora, materia.