Il web scraping è la raccolta automatica di dati dalle pagine web e la loro conversione in un formato strutturato: tabelle, JSON o una base di dati. Python è diventato lo standard de facto per questo compito: ha una sintassi semplice, un ecosistema di librerie enorme e una community molto attiva.
Questo materiale è un «hub» panoramico. Qui ripercorreremo l’intero processo, dalla richiesta della pagina all’archiviazione delle code di URL, daremo una mappa di tutte le librerie e soluzioni chiave e, per i temi più specifici, rimanderemo ad articoli dettagliati a parte.
Indice
- Che cos’è il web scraping e di che cosa si compone
- Panoramica di librerie e soluzioni
- Come otteniamo la pagina
- Librerie per il parsing del contenuto
- Risolvere i problemi di codifica e caratteri speciali
- Uso del multithreading
- Uso dei proxy
- Scraping attraverso TOR
- Lavorare con HTTPS/SSL
- Lavorare con i cookie
- Stato della risposta e intestazioni
- Archiviazione di URL e code
- Pro e contro dell’implementazione
- Aspetti legali ed etici
1. Che cos’è il web scraping e di che cosa si compone
Qualunque scraper, a prescindere dalla scala, si compone di quattro fasi:
- Recupero della pagina — richiesta HTTP al server e ricezione della risposta (HTML, XML, JSON).
- Parsing del contenuto — estrazione dei dati necessari dalla struttura tramite selettori (CSS, XPath) o espressioni regolari.
- Normalizzazione e archiviazione — conversione dei dati in un formato uniforme e scrittura su file o database.
- Gestione della scansione — coda di URL, deduplicazione, limite di velocità, tentativi ripetuti.
Uno script semplice può risolvere tutto questo in 10 righe. Un crawler industriale separa ogni fase in un livello a sé, con code, proxy e worker distribuiti.
2. Panoramica di librerie e soluzioni
Per non perdersi, dividiamo gli strumenti in base al loro ruolo.
Scaricare le pagine (client HTTP)
| Libreria | Tipo | Quando usarla |
|---|---|---|
| requests | sincrono | lo standard per la maggior parte dei compiti, API comoda |
| urllib | sincrono | inclusa nella libreria standard, senza dipendenze |
| httpx | sincr./asincr. | sostituto moderno di requests con supporto async e HTTP/2 |
| aiohttp | asincrono | alta concorrenza, migliaia di richieste |
| pycurl | sincrono | controllo fine della richiesta, massima velocità |
Parsing di HTML/XML
| Libreria | Motore | Particolarità |
|---|---|---|
| BeautifulSoup (bs4) | html.parser / lxml | l’API più amichevole, tollera l’HTML «sporco» |
| lxml | libxml2 (C) | massima velocità, XPath completo |
| parsel | lxml | CSS + XPath, la base di Scrapy |
| selectolax | Modest/Lexbor (C) | parser CSS molto veloce per grandi volumi |
| pyquery | lxml | sintassi in stile jQuery |
Siti dinamici (JavaScript)
| Strumento | Scopo |
|---|---|
| Selenium | controllo di un browser reale, il classico |
| Playwright | alternativa moderna, più veloce e stabile |
| Pyppeteer | port di Puppeteer in Python |
Framework e piattaforme
| Soluzione | Scopo |
|---|---|
| Scrapy | framework completo per crawler: code, pipeline, middleware |
| Scrapy + Splash/Playwright | Scrapy con rendering di JS |
| Django + Celery | scraping come parte di un’applicazione web con task in background |
Come scegliere
- Pagina semplice senza JS, compito una tantum → requests + BeautifulSoup.
- Ti serve velocità su grandi volumi → httpx/aiohttp + lxml/selectolax.
- Centinaia di migliaia di pagine, scansione dell’intero sito → Scrapy. Maggiori dettagli nell’articolo «Web scraping in Python con Scrapy».
- Il contenuto viene renderizzato con JavaScript → Playwright/Selenium.
- Lo scraping fa parte di un servizio web → Django, vedi «Web scraping con Django».
- Ti serve la massima concorrenza → approccio asincrono.
3. Come otteniamo la pagina
Richiesta di base con requests:
import requests
url = "https://example.com"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"Accept-Language": "it-IT,it;q=0.9",
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # lancia un'eccezione in caso di 4xx/5xx
html = response.textPunti chiave:
- User-Agent: impostalo sempre — molti siti bloccano le richieste che arrivano con il
python-requestsdi default. - timeout: indicalo sempre, altrimenti lo script può restare appeso per sempre.
- raise_for_status() ti risparmia il controllo manuale del codice di risposta.
Se la pagina viene renderizzata con JavaScript, requests restituirà uno scheletro vuoto. In quel caso serve un motore browser:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_selector(".content") # aspettiamo che compaiano i dati
html = page.content()
browser.close()Se in uscita non ti aspetti HTML ma dati strutturati di un’API, vedi il materiale a parte «Parsing di JSON».
4. Librerie per il parsing del contenuto
BeautifulSoup — il punto d’ingresso per chi inizia
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml") # il parser lxml è più veloce di html.parser
title = soup.find("h1").get_text(strip=True)
links = [a["href"] for a in soup.select("a.product-link")]
price = soup.select_one(".price").textBeautifulSoup tollera il markup rotto e si legge quasi come linguaggio naturale. Per l’estrazione delle tabelle c’è un approfondimento a parte: «Estrarre tabelle HTML con Python e BeautifulSoup».
lxml — velocità e XPath
from lxml import html as lxml_html
tree = lxml_html.fromstring(html)
titles = tree.xpath('//h2[@class="title"]/text()')
prices = tree.xpath('//span[@class="price"]/text()')lxml è scritto in C e sui grandi volumi lavora molte volte più veloce. L’XPath completo offre una flessibilità che i selettori CSS non raggiungono. L’analisi approfondita è nell’articolo «Web scraping in Python con lxml».
parsel e selectolax
parsel (il nucleo di Scrapy) combina CSS e XPath:
from parsel import Selector
sel = Selector(text=html)
sel.css("h1::text").get()
sel.xpath("//a/@href").getall()selectolax è la scelta giusta quando bisogna analizzare centinaia di migliaia di documenti: sulle selezioni CSS è nettamente più veloce perfino di lxml.
Espressioni regolari
re è appropriato solo per pattern semplici e piatti (telefono, email, codice articolo). Non conviene fare il parsing di HTML annidato con le regex: la struttura si rompe con troppa facilità.
5. Risolvere i problemi di codifica e caratteri speciali
Il fastidio più frequente sono i caratteri corrotti (mojibake) al posto delle lettere accentate: perché invece di perché, più invece di più. La causa è una codifica della risposta rilevata male.
Perché succede
requests prova a indovinare la codifica dall’intestazione Content-Type. Se il server non la invia o la invia sbagliata, il testo viene decodificato con la codifica errata (spesso si assume ISO-8859-1 al posto di windows-1252 o utf-8).
Soluzione 1: impostare la codifica a mano
response = requests.get(url)
response.encoding = "utf-8" # oppure "windows-1252" per i siti più vecchi
html = response.textSoluzione 2: rilevamento automatico
response = requests.get(url)
response.encoding = response.apparent_encoding # viene rilevata dal contenuto
html = response.textapparent_encoding usa la libreria charset-normalizer (o chardet), che analizza i byte e deduce la codifica.
Soluzione 3: lavorare direttamente con i byte
La via più affidabile è consegnare i byte al parser e lasciare che sia lui a leggere il <meta charset>:
from bs4 import BeautifulSoup
response = requests.get(url)
soup = BeautifulSoup(response.content, "lxml") # .content, non .textresponse.content sono i byte «grezzi»; lxml e BeautifulSoup trovano da soli la dichiarazione di codifica dentro l’HTML.
Soluzione 4: decodifica manuale
html = response.content.decode("windows-1252", errors="replace")Il parametro errors="replace" sostituisce i caratteri non convertibili con � senza far crollare lo script. Le sfumature dell’XML con caratteri speciali sono descritte in «Parsing di XML in Python».
6. Uso del multithreading
Lo scraping è soprattutto attesa della rete (I/O-bound), quindi i thread danno un’accelerazione tangibile nonostante il GIL: mentre un thread aspetta la risposta, un altro lavora.
ThreadPoolExecutor — la via più semplice
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [f"https://example.com/page/{i}" for i in range(1, 101)]
def fetch(url):
r = requests.get(url, timeout=10)
return url, r.status_code
with ThreadPoolExecutor(max_workers=10) as executor:
for url, status in executor.map(fetch, urls):
print(url, status)Quando ricorrere a multiprocessing
Se il collo di bottiglia non è la rete ma il parsing pesante dell’HTML e l’elaborazione (CPU-bound), i thread si scontrano con il GIL. Lì aiuta multiprocessing: più processi, ognuno con il proprio interprete.
L’alternativa migliore: async
Per migliaia di richieste simultanee i thread consumano troppa memoria. L’asincronia (asyncio + aiohttp) tiene decine di migliaia di connessioni in un solo thread. È un grande tema a parte: vedi «Scraping asincrono in Python».
Importante: un’alta concorrenza non dà il diritto di «buttare giù» il server altrui. Limita la velocità e rispetta
robots.txt.
7. Uso dei proxy
Con uno scraping intensivo il sito banna il tuo IP in base al numero di richieste. La soluzione: un pool di proxy con rotazione.
Collegamento semplice di un proxy
proxies = {
"http": "http://user:pass@123.45.67.89:8080",
"https": "http://user:pass@123.45.67.89:8080",
}
response = requests.get(url, proxies=proxies, timeout=15)Rotazione dei proxy
import random
import requests
PROXIES = [
"http://user:pass@ip1:port",
"http://user:pass@ip2:port",
"http://user:pass@ip3:port",
]
def fetch_with_rotation(url):
proxy = random.choice(PROXIES)
return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=15)Tipi di proxy
- Datacenter — economici e veloci, ma facili da individuare e bannare.
- Residenziali — IP di provider reali; più cari, ma meno visibili.
- Mobili — IP di operatori mobili, i più «affidabili» e i più costosi.
In produzione conviene mantenere una lista di proxy «sani»: scartare quelli che restituiscono timeout o 403 e verificarli periodicamente.
8. Scraping attraverso TOR
TOR è un modo gratuito di cambiare l’IP di uscita. È più lento dei proxy a pagamento e va bene per volumi modesti, ma non richiede spese.
Collegamento
Dopo l’installazione di TOR (il demone o Tor Browser), questo avvia un proxy SOCKS5 su 127.0.0.1:9050:
import requests
proxies = {
"http": "socks5h://127.0.0.1:9050",
"https": "socks5h://127.0.0.1:9050",
}
# serve il pacchetto: pip install requests[socks]
r = requests.get("https://httpbin.org/ip", proxies=proxies)
print(r.json()) # vedrai l'IP del nodo di uscita di TOR, non il tuoLo schema socks5h (con la lettera h) è importante: la risoluzione DNS passa attraverso TOR e non avviene in locale; altrimenti la tua vera richiesta DNS finirebbe esposta.
Cambiare IP (nuovo nodo di uscita)
Per ottenere un nuovo nodo di uscita, invia il segnale NEWNYM attraverso la porta di controllo (9051) con la libreria stem:
from stem import Signal
from stem.control import Controller
def renew_tor_ip():
with Controller.from_port(port=9051) as controller:
controller.authenticate(password="tua_password")
controller.signal(Signal.NEWNYM)La porta di controllo va abilitata nel torrc, impostando anche l’hash della password (tor --hash-password).
Tieni presente: molti grandi siti conoscono la lista dei nodi di uscita di TOR e li bloccano o mostrano un captcha. TOR va bene per compiti senza esigenze di velocità e con volumi bassi.
9. Lavorare con HTTPS/SSL
Di default requests verifica i certificati SSL tramite il pacchetto certifi. Nella maggior parte dei casi non c’è nulla da configurare. I problemi nascono sui siti con certificati autofirmati o scaduti.
Disattivare la verifica (solo per il debug)
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get(url, verify=False) # NON adatto alla produzioneDisattivare la verifica è insicuro: apre la porta a un attacco MITM. È ammissibile solo in locale, per il debug.
La via corretta: indicare la propria CA
response = requests.get(url, verify="/path/to/custom-ca-bundle.crt")Aggiornare i certificati radice
Se ricevi SSLCertVerificationError su siti normali, aggiorna certifi:
pip install --upgrade certifi
10. Lavorare con i cookie
I cookie servono per le sessioni, l’autenticazione e per superare le pagine «di protezione» che impostano un token e fanno un redirect.
Session conserva i cookie in automatico
import requests
session = requests.Session()
# facciamo il login: il server restituirà il cookie di sessione
session.post("https://example.com/login", data={"user": "u", "pass": "p"})
# le richieste successive sono già autenticate
profile = session.get("https://example.com/profile")Session conserva e reinvia da sola i cookie tra le richieste e, in più, riutilizza le connessioni TCP (più veloce) e le intestazioni comuni.
Passare i cookie a mano
cookies = {"sessionid": "abc123", "csrftoken": "xyz789"}
response = requests.get(url, cookies=cookies)Leggere i cookie ricevuti
response = requests.get(url)
for name, value in response.cookies.items():
print(name, value)
11. Stato della risposta e intestazioni
Controllare la risposta del server è obbligatorio; altrimenti finirai per fare il parsing di una pagina di errore come se fossero «dati».
response = requests.get(url)
print(response.status_code) # 200, 404, 403, 500 ...
print(response.reason) # 'OK', 'Not Found'
print(response.headers["Content-Type"])
print(response.headers.get("Server"))
print(response.url) # URL finale dopo i redirect
print(response.elapsed) # tempo di rispostaGestione corretta degli stati
if response.status_code == 200:
parse(response.text)
elif response.status_code == 404:
log("Pagina non trovata")
elif response.status_code == 429:
# Too Many Requests: ci stanno rallentando
wait = int(response.headers.get("Retry-After", 60))
time.sleep(wait)
elif response.status_code in (403, 503):
rotate_proxy() # probabilmente un ban: cambiamo IPL’intestazione Retry-After indica quanto aspettare prima di riprovare. Gli stati 403/503 tradiscono spesso una protezione anti-bot: aiutano il cambio di proxy, un altro User-Agent e una pausa.
12. Archiviazione di URL e code (panoramica)
Quando lo scraper percorre un sito intero, bisogna conservare da qualche parte gli URL «ancora da visitare» e quelli «già visitati». È quello che si chiama frontier (frontiera di scansione).
La variante più semplice: strutture in memoria
from collections import deque
to_visit = deque(["https://example.com"])
visited = set()
while to_visit:
url = to_visit.popleft()
if url in visited:
continue
visited.add(url)
# ... scaricare, analizzare, aggiungere i nuovi link a to_visitset garantisce una deduplicazione istantanea; deque funziona come coda FIFO.
Quando i dati crescono
- Redis — coda condivisa per più worker, sopravvive ai riavvii. Le liste e i set di Redis sono ideali per la scansione distribuita.
- Database (PostgreSQL/SQLite) — tabella di URL con stati
new / in_progress / done / failed; comoda per la resilienza e l’analisi. - Code di task (Celery + broker, RabbitMQ) — quando lo scraping è integrato in un’applicazione; vedi «Web scraping con Django».
- Filtro di Bloom — risparmia memoria con milioni di URL: verifica probabilistica dell’«abbiamo già visto questo URL».
In Scrapy la gestione della coda, la deduplicazione e le priorità sono integrate di serie: è una delle ragioni principali per scegliere questo framework nei progetti grandi. Maggiori dettagli in «Web scraping in Python con Scrapy».
13. Pro e contro dell’implementazione in Python
Pro:
- Barriera d’ingresso bassa, codice leggibile, prototipo rapido.
- Un ecosistema ricchissimo: da requests a Scrapy e Playwright.
- Una community enorme: quasi ogni problema è già stato risolto.
- Integrazione semplice con l’analisi dei dati (pandas, numpy) e i database.
Contro:
- Il GIL limita l’elaborazione CPU-bound (si aggira con async e multiprocessing).
- Il Python puro è più lento dei linguaggi compilati nel parsing pesante (lo salvano lxml/selectolax, scritti in C).
- I siti dinamici con JS richiedono motori browser pesanti.
- Fragilità: quando il sito cambia il layout, i selettori si rompono e serve manutenzione.
14. Aspetti legali ed etici
Il web scraping è uno strumento potente, e conviene usarlo con responsabilità:
- Rispetta robots.txt e le condizioni d’uso del sito.
- Non generare un carico eccessivo: aggiungi pause e limita la concorrenza.
- Non raccogliere dati personali senza una base giuridica (ricorda il GDPR).
- Indica uno User-Agent onesto dove è opportuno e metti in cache le risposte per non stressare il server più del necessario.