Scraping per linguaggio 8 min di lettura

Web scraping in Python con Scrapy

Introduzione al framework Scrapy: spider, pipeline, middleware e impostazioni che trasformano uno script in uno scraper di livello industriale.

TW
Team Web-Scraping.it
Raccolta dati per le esigenze del business
Pubblicato il 9 gennaio 2025

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

  1. Architettura di Scrapy
  2. Come si scarica la pagina: Spider e Request
  3. Parsing del contenuto: i selettori
  4. Lettere accentate e codifiche in Scrapy
  5. Concorrenza e velocità
  6. Proxy
  7. Scraping attraverso TOR
  8. HTTPS/SSL
  9. Lavorare con i cookie
  10. Stato della risposta e header
  11. Code e deduplicazione degli URL
  12. Item Pipeline: salvataggio dei dati
  13. 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

bash
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.

python
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:

bash
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:

python
# 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:

python
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:

python
# 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.0

AutoThrottle è 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:

python
yield scrapy.Request(url, meta={"proxy": "http://user:pass@ip:port"})

Per la rotazione di un pool è più comodo un pacchetto già pronto:

bash
pip install scrapy-rotating-proxies
python
# 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:

python
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):

python
# settings.py
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
# per i certificati autofirmati si può configurare la context factory

Nella maggior parte dei casi SSL «funziona e basta». I principi generali della sicurezza della connessione sono nell’hub, sezione «HTTPS/SSL».


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:

python
yield scrapy.Request(url, cookies={"sessionid": "abc123"})

Per il login è comodo FormRequest:

python
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:

python
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:

python
# 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:
bash
scrapy crawl catalog -s JOBDIR=crawls/catalog-1

Per 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:

python
# 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()
python
# 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.