Scraping per linguaggio 8 min di lettura

Web scraping con Django: raccolta dati all’interno di un’applicazione web

Come integrare la raccolta dati direttamente in un’applicazione Django: comandi di management, task di Celery e salvataggio dei risultati nei modelli e nell’admin.

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

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

  1. Perché fare scraping con Django
  2. Come otteniamo la pagina
  3. Librerie per il parsing del contenuto
  4. Salvare i risultati nei modelli
  5. Avviare lo scraping: comandi di management e Celery
  6. Codifiche e caratteri speciali in Django
  7. Multithreading e task in background
  8. Proxy
  9. Scraping attraverso TOR
  10. HTTPS/SSL
  11. Lavorare con i cookie
  12. Stato della risposta e header
  13. Archiviazione di URL e code tramite l’ORM
  14. 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.

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

La 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à:

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

L’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.

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

Salvataggio con protezione dai duplicati tramite update_or_create:

python
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

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

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

bash
celery -A myproject worker --concurrency=8 --loglevel=info

Se 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»:

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


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:

python
@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.text

Così 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.

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