Architettura di Web Scraping su Larga Scala: Una Guida Pratica
Alcune centinaia di pagine sono uno script. Milioni sono un sistema distribuito. Ecco l'architettura che ti porta lì — code, lavoratori asincroni, stratificazione dei proxy, tentativi di ripetizione, deduplicazione e controlli di qualità dei dati, con codice funzionante.
Scraping di alcune centinaia di pagine è uno script. Scraping di milioni è un sistema distribuito. Una volta che il tuo obiettivo passa da "gira tutta la notte sul mio laptop" a "deve finire questa settimana senza surriscaldarsi", la parte difficile smette di essere il parsing e diventa tutto ciò che lo circonda — come metti in coda il lavoro, lo distribuisci, eviti blocchi, ripeti i fallimenti e archivi e controlli il risultato. Questo è web scraping su larga scala come architettura, non un frammento, con codice funzionante in ogni fase.
La scala è la concorrenza, non un ciclo più grande
Un dato rende tutto concreto. Prendi una categoria con 20.000 pagine di annunci, 20 elementi ciascuna — 400.000 pagine da recuperare. Con un realistico 2,5 secondi per pagina, un'esecuzione strettamente sequenziale dura circa 1.000.000 di secondi, grosso modo 11,5 giorni di attesa per il caricamento delle pagine prima di analizzare un singolo campo. Gestisci 200 pagine in parallelo e quei 11,5 giorni si riducono a un'ora di tempo reale. Il tempo è il vincolo su larga scala, e la concorrenza è come lo recuperi. Tutto il resto nell'architettura esiste per rendere quella concorrenza sostenibile.

L'architettura in sintesi
Uno scraper che sopravvive a milioni di pagine è un piccolo sistema distribuito con una manciata di parti nominate, ognuna risolvendo un problema che si presenta solo a volume:
- Una coda contiene gli URL ancora da recuperare e disaccoppia la scoperta dal lavoro.
- Lavoratori asincroni o distribuiti estraggono dalla coda e recuperano in modo concorrente — è qui che si trovano i risparmi di tempo reale.
- Un livello di proxy e anti-bot ruota gli IP e presenta traffico da browser reale in modo che nessun singolo indirizzo superi un limite di velocità.
- Rendering, solo quando necessario, perché un browser senza testa è la cosa più costosa nella pipeline.
- Tentativi di ripetizione con backoff, deduplicazione, archiviazione e monitoraggio più controlli di qualità dei dati completano il tutto.
Coda prima: disaccoppia la scoperta dal recupero
La decisione strutturale più importante è mettere una coda tra "cosa raschiare" e "fare lo scraping". Un produttore enumera gli URL; un pool di lavoratori li svuota. Nessuna delle due parti sa quanto velocemente l'altra funzioni, e puoi aggiungere lavoratori senza toccare il produttore. In Python questo è Celery o RQ su Redis; in Node, BullMQ; su scala maggiore, RabbitMQ o Kafka. Il modello in un file:
import asyncio, aiohttp
CONCURRENCY = 50
queue = asyncio.Queue()
async def worker(session):
while True:
url = await queue.get()
try:
async with session.get(url, timeout=20) as resp:
await handle(url, await resp.text(), resp.status)
except Exception as err:
await on_failure(url, err)
finally:
queue.task_done()
async def run(urls):
for u in urls:
queue.put_nowait(u)
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(worker(session)) for _ in range(CONCURRENCY)]
await queue.join()
for t in tasks:
t.cancel()
La manopola che conta è CONCURRENCY. Troppo bassa spreca il parallelismo che rende possibile la scala; troppo alta sovraccarica sia il target che la tua uscita. Trovi il valore giusto osservando l'aumento dei tassi di errore — ed è esattamente per questo che il monitoraggio è una parte di prima classe del sistema, non un ripensamento.

Il livello proxy si rompe per primo
A basso volume noti a malapena le difese anti-bot; su larga scala interrompono l'esecuzione per prime. Invia alcune centinaia di migliaia di richieste da un IP e vieni limitato, poi sfidato, poi bloccato. La soluzione è la rotazione su molti indirizzi. Stratifica: IP di datacenter economici per target e API indulgenti, IP residenziali rotanti per target commerciali difficili che si aspettano traffico da utenti reali. Ma la sola rotazione non basta — le difese moderne leggono anche le impronte digitali TLS e l'ordine degli header, quindi il traffico deve sembrare provenire da un browser, non solo da un IP nuovo.
Tentativi di ripetizione: il fallimento è lo stato normale
Con un milione di richieste, un tasso di fallimento transitorio dell'1% significa 10.000 pagine fallite. Il fallimento non è un caso limite a questo volume — è routine, e la pipeline deve trattare un recupero fallito come normale piuttosto che fatale. Ripeti con backoff esponenziale e un limite, poi sposta l'URL in una coda di lettere morte invece di bloccare l'esecuzione. Leggi perché è fallito: un timeout o un 503 vale la pena riprovare, un 404 duro no.
import asyncio, random
async def fetch_with_retry(session, url, tries=4):
for attempt in range(tries):
try:
async with session.get(url, timeout=20) as r:
if r.status == 404:
return None # don't retry a hard 404
if r.status < 400:
return await r.text()
except Exception:
pass
await asyncio.sleep(2 ** attempt + random.random()) # backoff + jitter
await dead_letter(url) # give up after the cap
return None
Deduplicazione: non raschiare la stessa pagina due volte
La scoperta su larga scala produce duplicati costantemente — lo stesso prodotto raggiungibile da tre percorsi, parametri di tracciamento che fanno sembrare una pagina come dieci. Normalizza gli URL prima che entrino nella coda, poi mantieni un insieme visto (un set Redis, o un filtro Bloom una volta che l'insieme raggiunge centinaia di milioni):
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
seen = set()
def normalize(url):
s = urlsplit(url.lower())
q = [(k, v) for k, v in parse_qsl(s.query) if not k.startswith("utm_")]
return urlunsplit((s.scheme, s.netloc, s.path.rstrip("/"), urlencode(sorted(q)), ""))
def enqueue(url):
key = normalize(url)
if key not in seen:
seen.add(key)
queue.put_nowait(key)
Archiviazione e qualità dei dati
Due abitudini specifiche per la scala: scrivi in batch in modo che l'archiviazione non sia il tuo collo di bottiglia, e separa i dati grezzi da quelli analizzati in modo da poter ri-analizzare senza ri-raschiare quando i selettori cambiano. Poi aggiungi la metà del monitoraggio che la maggior parte dei team salta — controlli di qualità dei dati. Un'esecuzione può riportare il 100% di successo HTTP e comunque produrre spazzatura se il layout è cambiato e i tuoi selettori ora non corrispondono a nulla. Assicura che i campi richiesti non siano vuoti e che i valori siano sensati:
def validate(row):
assert row.get("title"), "empty title — selector may have drifted"
price = row.get("price")
assert isinstance(price, (int, float)) and 0 < price < 1_000_000, "bad price"
return row
# fail loud on page 5,000, not silently after 5,000,000 empty rows
Scarica proxy, anti-bot e rendering su un'unica API
Renderizza solo quando devi
Un browser senza testa è l'operazione più costosa nella pipeline — CPU, memoria e secondi per pagina, che dominano tutto a un milione di pagine. Molti siti ancora inviano dati nell'HTML iniziale o in un endpoint JSON; un recupero semplice più un parser è un ordine di grandezza più economico. Prova prima il percorso economico, conferma che i campi siano presenti, ed esegui il rendering solo per le pagine che ne hanno bisogno. La nostra analisi di costo del rendering rispetto a HTTP mette numeri reali su quel divario.
Costruire vs acquistare i livelli difficili
Tutto quanto sopra è costruibile, quindi la domanda onesta è quali parti meritano il tuo tempo di ingegneria. Il modello di dati, la logica di parsing, i controlli di qualità e lo schema di archiviazione sono specifici per il tuo progetto — solo tu puoi costruirli bene. Il pool di proxy, la gestione anti-bot, la flotta di rendering senza testa e la coda di tentativi e consegna sono infrastrutture generiche che sono costose da costruire e una fatica da mantenere sane mentre i target evolvono. Questa è la linea su cui si posiziona un'API Scraper gestita: noleggia le parti che sono le stesse per tutti. La nostra analisi di costruire versus acquistare lavora il costo di manutenzione in dettaglio, e eseguire scraper come software di produzione copre il mantenimento dell'intero sistema osservabile.
Domande frequenti
Come fai a raschiare milioni di pagine?
Con la concorrenza, non un ciclo più grande. Metti una coda tra la scoperta degli URL e il recupero, svuotala con un pool di lavoratori asincroni o distribuiti, ruota gli IP per evitare blocchi, ripeti i fallimenti transitori con backoff, deduplica gli URL e scrivi in batch nell'archiviazione. Un'esecuzione sequenziale di un milione di pagine richiede giorni; lo stesso lavoro attraverso un pool di lavoratori concorrenti si completa in poche ore.
Qual è la migliore architettura per il web scraping su larga scala?
Una pipeline di coda e lavoratori: un produttore enumera gli URL su una coda (Redis, RabbitMQ o Kafka), i lavoratori recuperano in modo concorrente attraverso un livello di proxy rotante, rendono solo le pagine che necessitano di JavaScript, ripetono i fallimenti in una coda di lettere morte, deduplicano con un insieme visto e archiviano i dati grezzi e analizzati separatamente. Avvolgilo nel monitoraggio con controlli di qualità dei dati in modo che le derive emergano presto.
Quante richieste puoi eseguire in parallelo?
Dipende dal target e dalla tua uscita, non da un numero fisso. Lo scraping è limitato dall'IO, quindi una macchina modesta può gestire molte centinaia di richieste in corso. Inizia con una concorrenza di circa 50, osserva il tasso di errore e aumentalo fino a quando i fallimenti aumentano — quello è il tuo limite. Oltre i limiti di una macchina, aggiungi lavoratori distribuiti piuttosto che spingere un singolo nodo più forte.
Come gestisci i fallimenti su larga scala?
Assumi il fallimento — con un milione di richieste, anche un tasso di errore dell'1% significa 10.000 pagine morte. Ripeti errori transitori (timeout, 503) con backoff esponenziale e jitter, limita i tentativi e sposta i fallimenti persistenti in una coda di lettere morte invece di bloccare l'esecuzione. Non ripetere i 404 duri. Randomizzare l'ordine di raccolta distribuisce anche i fallimenti in modo che non fallisca sulle stesse pagine ad ogni esecuzione.
La scala è principalmente le parti che non è divertente costruire: rotazione degli IP, anti-bot, rendering senza testa, code e tentativi di ripetizione. Possiedi i pezzi specifici per i tuoi dati — il modello, i parser, i controlli di qualità — e noleggia l'infrastruttura generica che è la stessa fatica per tutti. Ottieni la coda e la concorrenza giuste prima; tutto il resto serve a far sopravvivere quella concorrenza al contatto con un milione di pagine reali.