Quando lo scraping non serve come script una tantum ma come parte di un servizio web completo — con interfaccia, esecuzione pianificata, salvataggio nel database ed elaborazione in background — entra in gioco Django. Il framework offre l’ORM per l’archiviazione, i comandi di management per l’avvio e, in coppia con Celery, i task in background e periodici.
Questo articolo è la continuazione pratica della guida generale «Web scraping con Python». Le tecniche di base (requests, BeautifulSoup, codifiche) sono spiegate lì; qui l’accento è sull’integrazione con Django.
Indice
- Perché fare scraping con Django
- Come otteniamo la pagina
- Librerie per il parsing del contenuto
- Salvare i risultati nei modelli
- Avviare lo scraping: comandi di management e Celery
- Codifiche e caratteri speciali in Django
- Multithreading e task in background
- Proxy
- Scraping attraverso TOR
- HTTPS/SSL
- Lavorare con i cookie
- Stato della risposta e header
- Archiviazione di URL e code tramite l’ORM
- Pro e contro
1. Perché fare scraping con Django
Django si giustifica quando lo scraping è solo una parte del prodotto:
- un aggregatore di prodotti, offerte di lavoro o notizie con vetrina web;
- il monitoraggio dei prezzi con storico nel database e dashboard;
- l’aggiornamento periodico del catalogo secondo una pianificazione;
- un pannello di amministrazione per gestire le fonti e consultare i risultati.
Django offre «di serie»: l’ORM (archiviazione e deduplicazione), l’admin (gestione delle fonti), il sistema di migrazioni e, con Celery, l’esecuzione in background e periodica, così lo scraping pesante non blocca le richieste web.
2. Come otteniamo la pagina
Il client HTTP in Django non differisce in nulla dal Python abituale: prendiamo requests o httpx. La logica di scraping va tenuta fuori dalle view: le view devono essere veloci, e le richieste di rete si spostano in un service layer o nei task di Celery.
# parser/services.py
import requests
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; MyParserBot/1.0)",
"Accept-Language": "it-IT,it;q=0.9",
}
def fetch_page(url: str) -> str | None:
try:
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding
return resp.text
except requests.RequestException as exc:
# registriamo l'errore e non facciamo cadere l'applicazione
import logging
logging.getLogger("parser").warning("fetch failed %s: %s", url, exc)
return NoneLa regola d’oro: mai avviare uno scraping lungo direttamente in una view — l’utente resterebbe in attesa e il worker del web server si bloccherebbe. La richiesta si mette in coda e la risposta torna subito.
3. Librerie per il parsing del contenuto
Dentro il servizio si usano gli stessi strumenti di sempre — BeautifulSoup per la comodità, lxml per la velocità:
from bs4 import BeautifulSoup
def parse_products(html: str) -> list[dict]:
soup = BeautifulSoup(html, "lxml")
items = []
for card in soup.select(".product-card"):
items.append({
"title": card.select_one(".title").get_text(strip=True),
"price": card.select_one(".price").get_text(strip=True),
"url": card.select_one("a")["href"],
})
return itemsL’analisi dettagliata dei parser è in «Web scraping con Python» e in «lxml». Se la fonte sono le tabelle, vedi «Estrarre tabelle HTML con Python e BeautifulSoup». Se l’API restituisce JSON — «Parsing di JSON».
4. Salvare i risultati nei modelli
La forza di Django è l’ORM. Descrivi il modello, e deduplicazione, filtri e storico degli aggiornamenti diventano banali.
# parser/models.py
from django.db import models
class Product(models.Model):
source_url = models.URLField(unique=True) # unicità = protezione dai duplicati
title = models.CharField(max_length=500)
price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titleSalvataggio con protezione dai duplicati tramite update_or_create:
from .models import Product
def save_products(items: list[dict]):
for item in items:
Product.objects.update_or_create(
source_url=item["url"],
defaults={"title": item["title"], "price": item["price"]},
)update_or_create aggiorna il record esistente o ne crea uno nuovo — ideale per lo scraping ripetuto.
5. Avviare lo scraping: comandi di management e Celery
Comando di management — per l’avvio manuale e via cron
# parser/management/commands/run_parser.py
from django.core.management.base import BaseCommand
from parser.services import fetch_page, parse_products, save_products
class Command(BaseCommand):
help = "Avvia lo scraping del catalogo"
def add_arguments(self, parser):
parser.add_argument("--url", required=True)
def handle(self, *args, **options):
html = fetch_page(options["url"])
if html:
items = parse_products(html)
save_products(items)
self.stdout.write(self.style.SUCCESS(f"Salvati {len(items)} prodotti"))Esecuzione: python manage.py run_parser --url https://example.com/catalog. Un comando del genere è comodo da agganciare al cron di sistema.
Celery — per i task in background e periodici
# parser/tasks.py
from celery import shared_task
from .services import fetch_page, parse_products, save_products
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def parse_url_task(self, url):
html = fetch_page(url)
if html is None:
raise self.retry() # nuovo tentativo in caso di errore
items = parse_products(html)
save_products(items)
return len(items)L’esecuzione periodica si configura con django-celery-beat direttamente nell’admin — per esempio, aggiornare il catalogo ogni 6 ore. Il retry di Celery dà resilienza ai problemi temporanei di rete senza gestione manuale.
6. Codifiche e caratteri speciali in Django
Il problema è lo stesso di qualsiasi scraper in Python: una codifica della risposta rilevata male, che rompe lettere accentate e caratteri speciali. Si cura allo stesso modo — resp.apparent_encoding o il lavoro con resp.content (vedi in dettaglio «Web scraping con Python», sezione sulle codifiche).
La specificità di Django: assicurati che il database usi UTF-8 (per PostgreSQL la codifica UTF8, per MySQL utf8mb4), altrimenti i caratteri accentati si romperanno già in fase di salvataggio, non di scraping. In un progetto nuovo è il valore di default, ma lavorando con un database datato conviene controllare.
7. Multithreading e task in background
In Django si usano di rado i thread «nudi»: per il parallelismo c’è Celery con il suo pool di worker. Avviando più worker ottieni l’elaborazione parallela della coda senza gestire i thread a mano:
celery -A myproject worker --concurrency=8 --loglevel=infoSe invece serve percorrere in fretta una lista di URL dentro un singolo task, va bene ThreadPoolExecutor (vedi la sezione sul multithreading della guida generale). Per una concorrenza molto alta dentro il task si può usare async — ma in Django questo richiede attenzione con l’ORM (sync_to_async). Il tema è trattato in «Scraping asincrono in Python».
8. Proxy
I proxy si agganciano a livello del service layer — Django qui non c’entra. La lista dei proxy è comodo tenerla in un modello e marcare quelli «sani»:
class Proxy(models.Model):
address = models.CharField(max_length=200) # http://user:pass@ip:port
is_active = models.BooleanField(default=True)
fail_count = models.IntegerField(default=0)Il servizio prende quindi un proxy attivo a caso e, in caso di errore, incrementa fail_count; superata la soglia, lo disattiva. Le tecniche generali di rotazione sono nella guida generale, sezione «Proxy».
9. Scraping attraverso TOR
TOR si collega come proxy SOCKS5 (socks5h://127.0.0.1:9050) nello stesso fetch_page. Django non aggiunge nulla di specifico — vedi la sezione su TOR della guida generale. Sul server con Django il demone TOR gira come processo separato (servizio systemd).
10. HTTPS/SSL
La verifica dei certificati lavora a livello di requests. In produzione lascia verify=True. Se il server ha certificati radice obsoleti, aggiorna certifi nell’ambiente virtuale del progetto. I dettagli sono nella guida generale.
11. Lavorare con i cookie
Per lo scraping con autenticazione usa requests.Session dentro il task. Se devi riutilizzare la sessione tra task di Celery, salva il cookie-jar (per esempio serializzato in Redis o in un modello) e ripristinalo prima della richiesta. Le tecniche di base sono nella guida generale, sezione «Cookie».
12. Stato della risposta e header
Registra nel log il codice di risposta e decidi se ripetere il task. In Celery questo si sposa elegantemente con il meccanismo di retry:
@shared_task(bind=True, max_retries=5)
def fetch_task(self, url):
resp = requests.get(url, timeout=15)
if resp.status_code == 429:
delay = int(resp.headers.get("Retry-After", 60))
raise self.retry(countdown=delay)
if resp.status_code in (403, 503):
raise self.retry(countdown=120) # probabile ban, aspettiamo più a lungo
resp.raise_for_status()
return resp.textCosì i blocchi anti-bot (429/403/503) si trasformano in tentativi ripetuti e controllati, con la giusta attesa, invece che in dati persi.
13. Archiviazione di URL e code tramite l’ORM
In Django il frontier si implementa comodamente con una tabella a stati — sopravvive ai riavvii ed è visibile nell’admin.
class CrawlURL(models.Model):
STATUS = [("new", "new"), ("in_progress", "in_progress"),
("done", "done"), ("failed", "failed")]
url = models.URLField(unique=True) # unique = deduplicazione automatica
status = models.CharField(max_length=20, choices=STATUS, default="new")
attempts = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)Il worker preleva gli URL «nuovi», li marca in_progress, li elabora e li porta a done/failed. L’indice univoco su url esclude i duplicati. Nei progetti ad alto carico la coda vera conviene tenerla in Redis/nel broker di Celery, e nel database salvare solo il risultato e lo storico. La panoramica generale degli approcci alle code è nella guida generale, sezione «Code».
14. Pro e contro dello scraping con Django
Pro:
- L’ORM ti solleva da archiviazione, deduplicazione e migrazioni.
- Admin di serie: gestione delle fonti e consultazione dei dati.
- Celery + beat: task in background e periodici affidabili, con retry.
- Un’unica code base: scraper e vetrina web nello stesso progetto.
Contro:
- Eccessivo per gli script una tantum: un normale
.pyè più semplice. - Richiede infrastruttura: broker (Redis/RabbitMQ), worker Celery.
- L’async in Django funziona ancora con delle riserve (l’ORM è in gran parte sincrono).
- Per scandagliare milioni di pagine lo specializzato Scrapy è più efficiente.