Quando devi percorrere non una pagina ma un sito intero — migliaia e milioni di URL — uno script fatto in casa si trasforma presto in un groviglio di code, tentativi ripetuti e rotazione dei proxy. Scrapy risolve tutto questo per te: è un framework asincrono completo per il crawling, con code integrate, deduplicazione, pipeline di elaborazione e un sistema di middleware.
Questo articolo prosegue la panoramica «Web scraping con Python». Se ti serve una raccolta una tantum da un paio di pagine, bastano requests + BeautifulSoup; Scrapy dà il meglio di sé su larga scala.
Indice
- Architettura di Scrapy
- Come si scarica la pagina: Spider e Request
- Parsing del contenuto: i selettori
- Lettere accentate e codifiche in Scrapy
- Concorrenza e velocità
- Proxy
- Scraping attraverso TOR
- HTTPS/SSL
- Lavorare con i cookie
- Stato della risposta e header
- Code e deduplicazione degli URL
- Item Pipeline: salvataggio dei dati
- Pro e contro
1. Architettura di Scrapy
Scrapy è costruito su un motore asincrono (Twisted) ed è composto da componenti collegati tra loro:
- Spider — la tua classe: da quali URL partire e come elaborare le risposte.
- Scheduler — la coda delle richieste, con deduplicazione e priorità.
- Downloader — scarica le pagine in modo asincrono.
- Middlewares — intercettori di richieste/risposte (proxy, header, tentativi ripetuti).
- Item Pipeline — elaborazione e salvataggio dei dati estratti.
Tu scrivi solo lo Spider e la Pipeline: di tutto il resto si occupa il framework.
Creazione del progetto
pip install scrapy
scrapy startproject myparser
cd myparser
scrapy genspider example example.com
2. Come si scarica la pagina: Spider e Request
In Scrapy non scrivi il ciclo delle richieste a mano: restituisci (yield) oggetti Request e il motore li esegue in modo asincrono.
import scrapy
class CatalogSpider(scrapy.Spider):
name = "catalog"
start_urls = ["https://example.com/catalog"]
custom_settings = {
"USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
"DOWNLOAD_DELAY": 1.0, # pausa tra le richieste
"ROBOTSTXT_OBEY": True, # rispettiamo il robots.txt
}
def parse(self, response):
# estraiamo le schede prodotto
for card in response.css(".product-card"):
yield {
"title": card.css(".title::text").get(),
"price": card.css(".price::text").get(),
"url": response.urljoin(card.css("a::attr(href)").get()),
}
# passiamo alla pagina successiva
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)response.follow completa automaticamente l’URL relativo e mette la richiesta in coda. Così la paginazione si descrive in un paio di righe. Esecuzione:
scrapy crawl catalog -o products.json
3. Parsing del contenuto: i selettori
Sotto il cofano Scrapy usa la libreria parsel (basata su lxml), che supporta sia CSS sia XPath:
# CSS
response.css("h1::text").get()
response.css(".price::text").getall()
response.css("a::attr(href)").getall()
# XPath
response.xpath("//h1/text()").get()
response.xpath('//div[@class="price"]/text()').get()
# espressioni regolari direttamente nel selettore
response.css(".price::text").re_first(r"\d+")get() restituisce il primo risultato (o None); getall(), una lista. È più comodo di lxml «nudo» grazie alla gestione sicura degli elementi mancanti. Per approfondire XPath: «Web scraping in Python con lxml».
Se in realtà la pagina interroga una API e restituisce JSON invece di HTML, in Scrapy lo si elabora direttamente con response.json(), senza alcun selettore. Come pulire e strutturare queste risposte è mostrato in dettaglio in «Parsing di JSON».
Items e ItemLoader
Nei progetti strutturati conviene descrivere i dati come Item e popolarli tramite ItemLoader con processori di pulizia (rimozione degli spazi, conversione dei tipi). Per gli spider semplici bastano i normali dizionari, come nell’esempio sopra.
4. Lettere accentate e codifiche in Scrapy
Nella maggior parte dei casi Scrapy rileva da solo la codifica corretta dagli header e dal <meta charset>: le lettere accentate «funzionano e basta». Se compaiono caratteri corrotti, puoi indicare la codifica esplicitamente alla creazione della risposta o decodificare il corpo a mano:
def parse(self, response):
# rileggere il corpo forzando la codifica corretta
text = response.body.decode("windows-1252", errors="replace")
sel = scrapy.Selector(text=text)La teoria generale del problema delle codifiche è nell’hub, sezione «Codifiche». Controlla anche FEED_EXPORT_ENCODING = "utf-8" nelle impostazioni, perché i caratteri accentati non finiscano trasformati in sequenze \uXXXX nel JSON finale.
5. Concorrenza e velocità
Il grande vantaggio di Scrapy è l’asincronia di serie. Decine di richieste vengono eseguite contemporaneamente, senza thread. Si regola dalle impostazioni:
# settings.py
CONCURRENT_REQUESTS = 16 # richieste simultanee in totale
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # per singolo dominio
DOWNLOAD_DELAY = 0.5 # pausa di base
AUTOTHROTTLE_ENABLED = True # regolazione automatica della velocità
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0AutoThrottle è una funzione particolarmente intelligente: Scrapy riduce da solo la velocità se il server comincia a rispondere più lentamente, bilanciando rapidità e cortesia. Ti risparmia la regolazione manuale dei ritardi. Dato che è tutto asincrono, il multithreading non ti serve — confrontalo con l’approccio di «Scraping asincrono in Python», che Scrapy implementa «sotto il cofano».
6. Proxy
Il modo più semplice è indicare il proxy nel meta della richiesta:
yield scrapy.Request(url, meta={"proxy": "http://user:pass@ip:port"})Per la rotazione di un pool è più comodo un pacchetto già pronto:
pip install scrapy-rotating-proxies# settings.py
ROTATING_PROXY_LIST = [
"ip1:port",
"ip2:port",
"ip3:port",
]
DOWNLOADER_MIDDLEWARES = {
"rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
"rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}Il middleware ruota i proxy da solo, individua quelli «bannati» ed esclude quelli morti. La teoria generale è nell’hub, sezione «Proxy».
7. Scraping attraverso TOR
TOR si collega come proxy SOCKS5 tramite meta:
yield scrapy.Request(url, meta={"proxy": "socks5h://127.0.0.1:9050"})Il cambio del nodo di uscita tramite stem (segnale NEWNYM) è descritto nell’hub, sezione «TOR». In pratica con Scrapy si scelgono più spesso proxy a pagamento con rotazione: sono più veloci e vengono bannati meno dei nodi di uscita di TOR.
8. HTTPS/SSL
Per default Scrapy verifica i certificati. Se devi allentare il controllo per un sito problematico (solo con cognizione di causa):
# settings.py
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
# per i certificati autofirmati si può configurare la context factoryNella maggior parte dei casi SSL «funziona e basta». I principi generali della sicurezza della connessione sono nell’hub, sezione «HTTPS/SSL».
9. Lavorare con i cookie
In Scrapy i cookie sono attivi per default (COOKIES_ENABLED = True): il motore mantiene la sessione tra le richieste in automatico. Per passare i cookie a mano:
yield scrapy.Request(url, cookies={"sessionid": "abc123"})Per il login è comodo FormRequest:
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={"username": "user", "password": "pass"},
callback=self.after_login,
)from_response raccoglie da solo i campi nascosti del form (incluso il token CSRF), eliminando il tipico mal di testa dell’autenticazione.
10. Stato della risposta e header
L’accesso allo stato e agli header passa dall’oggetto response:
def parse(self, response):
print(response.status) # 200, 404 ...
print(response.headers.get("Content-Type"))Per default Scrapy elabora solo i 2xx e lascia passare i 4xx/5xx. I tentativi ripetuti si governano con il RetryMiddleware integrato:
# settings.py
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 403]Lo stato 429 (Too Many Requests) Scrapy sa rispettarlo insieme all’header Retry-After. La logica dei codici di stato in generale è nell’hub, sezione «Stato e header».
11. Code e deduplicazione degli URL
Qui Scrapy è particolarmente forte: ciò che in uno scraper fatto in casa va costruito a mano (vedi l’hub, sezione «Code») qui è integrato:
- Lo Scheduler mantiene la coda delle richieste con le priorità.
- Il Dupefilter scarta automaticamente gli URL già incontrati (in base all’impronta della richiesta).
- La coda può essere portata su disco (
JOBDIR) per riprendere una scansione interrotta:
scrapy crawl catalog -s JOBDIR=crawls/catalog-1Per il crawling distribuito su più macchine c’è scrapy-redis: coda e dupefilter condivisi in Redis, così più worker possono scandagliare lo stesso sito insieme.
12. Item Pipeline: salvataggio dei dati
Gli elementi estratti passano per la pipeline, dove vengono validati, puliti e salvati:
# pipelines.py
import pymongo
class MongoPipeline:
def open_spider(self, spider):
self.client = pymongo.MongoClient("mongodb://localhost:27017")
self.db = self.client["scraping"]
def process_item(self, item, spider):
self.db["products"].update_one(
{"url": item["url"]}, {"$set": dict(item)}, upsert=True
)
return item
def close_spider(self, spider):
self.client.close()# settings.py
ITEM_PIPELINES = {"myparser.pipelines.MongoPipeline": 300}Per un’esportazione semplice la pipeline non serve: l’opzione -o products.csv (o .json, .jsonl) salva direttamente il risultato.
13. Pro e contro di Scrapy
Pro:
- Asincronia, code, deduplicazione e tentativi ripetuti, tutto di serie.
- Prestazioni elevate su grandi quantità di pagine.
- Architettura pulita: Spider, Middleware e Pipeline ben separati.
- Estensioni già pronte: rotazione dei proxy, scrapy-redis, auto-throttling.
- Job riprendibili ed esportazione comoda in qualsiasi formato.
Contro:
- Curva di apprendimento ripida: bisogna capire l’architettura e Twisted.
- Eccessivo per un paio di pagine (lì è più semplice requests + BeautifulSoup).
- I siti JavaScript richiedono un’integrazione aggiuntiva (scrapy-playwright o Splash).
- Il modello asincrono di Twisted risulta poco familiare rispetto al moderno asyncio.
Per i siti dinamici si aggiunge scrapy-playwright, che renderizza le pagine con un browser reale. Se invece il contenuto è semplice e statico e l’infrastruttura Django c’è già, a volte è più semplice fare scraping da Django.