Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ParserVantaggiSvantaggi
html.parserIncluso in Python, nessuna dipendenzaLeggermente più lento su documenti enormi
lxmlPiù veloce, più tollerante su HTML malformatoRichiede installazione separata (pip install lxml)

Tip

Per quello che serve a noi… html.parser basta e avanza!
Se il web scraping vi prende proprio la mano… provate anche lxml!!!

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.txt non è 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-Agent onesto invece di camuffarsi da browser per aggirare controlli.

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:

  • .text restituisce il testo contenuto in un elemento (e nei suoi figli), come stringa.
  • elemento["attributo"] e elemento.get("attributo") fanno la stessa cosa, ma .get() restituisce None invece di sollevare un’eccezione se l’attributo non esiste — più sicuro quando l’HTML non è del tutto regolare.
  • Se find() o select_one() non trovano nulla, restituiscono None: chiamare .text su None genera 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.