I siti moderni raramente restituiscono l’HTML già pronto con i dati. Il catalogo dei prodotti, il feed, i prezzi, le recensioni — quasi tutto viene caricato dopo che la pagina è già stata aperta, tramite JavaScript. Per uno scraper questo significa che una semplice richiesta GET della pagina e il successivo parsing dell’HTML restituiranno uno scheletro vuoto, senza dati.
Ci sono due modi radicalmente diversi di risolvere questo problema:
- Intercettare le richieste API — individuare le richieste con cui il browser stesso preleva i dati e ripeterle direttamente, senza browser.
- Emulazione completa del browser — avviare un browser reale (o headless), lasciare che renderizzi il JS e operare sulla pagina come un utente: cliccare, scorrere, trascinare elementi.
Il primo approccio è più veloce e consuma meno risorse; il secondo è più universale e più robusto di fronte a logiche insolite. Nella pratica vengono spesso combinati.
Approccio 1. Intercettare ed emulare le richieste API
L’idea
Quando una pagina «completa» i propri dati al volo, quasi sempre effettua richieste HTTP in background (XHR/fetch) a un’API interna che restituisce JSON. Se individui questo endpoint e riproduci la richiesta con header, cookie e token corretti, puoi ottenere i dati direttamente, senza passare per il rendering. È decine di volte più veloce e non richiede un browser.
Come individuare l’API
- Apri i DevTools (
F12) → scheda Network. - Filtra per Fetch/XHR.
- Scorri la pagina, premi «Mostra altro», cambia categoria — in una parola, provoca il caricamento dei dati.
- Individua la richiesta nella cui risposta si trova il JSON che ti serve (prodotti, prezzi ecc.).
- Esaminala: URL, metodo, parametri della richiesta, header, corpo e cookie.
- Clic destro → Copy → Copy as cURL — un ottimo punto di partenza: puoi importarlo in Postman o tradurlo direttamente in codice.
Token: CSRF, sessioni, autenticazione
La difficoltà principale di questo approccio è che la richiesta non è quasi mai «nuda». Il server si aspetta una serie di dati di conferma e, senza di essi, restituisce 401, 403 o 419.
Token CSRF (Cross-Site Request Forgery). Protezione contro la falsificazione delle richieste tra siti. Il server emette un token casuale che il client deve restituire nelle richieste che modificano i dati (e talvolta anche in quelle di lettura). Dove si trova di solito:
- in
<meta name="csrf-token" content="...">, nell’HTML della pagina; - in un campo nascosto del form
<input type="hidden" name="_token" value="...">; - in un cookie (spesso
XSRF-TOKEN) che poi va duplicato nell’headerX-CSRF-TokenoX-XSRF-TOKEN.
Lo schema è questo: prima si carica la pagina normale, si estraggono il token e i cookie di sessione, poi li si inserisce nella richiesta all’API.
Cookie di sessione. Al primo accesso il server invia Set-Cookie (per esempio sessionid, PHPSESSID, laravel_session). Vanno conservati tra una richiesta e l’altra — per questo si usa un oggetto sessione (requests.Session, httpx.Client), che lo fa automaticamente.
Autenticazione (Bearer / JWT / chiave API). Se i dati sono dietro un login, l’header di solito contiene Authorization: Bearer <token>. I token JWT si ottengono tramite l’endpoint di login e poi si allegano a ogni richiesta.
Altri campi di protezione. X-Requested-With: XMLHttpRequest (spesso obbligatorio per gli endpoint AJAX), Referer, Origin e, a volte, parametri firmati (signature, nonce, timestamp) generati dal JavaScript del frontend.
Quando questo approccio si rompe. Se il token o la firma della richiesta vengono generati da un JavaScript offuscato direttamente nel browser (o in WASM), riprodurlo lato server è estremamente difficile. È il segnale che conviene passare al secondo approccio — l’emulazione del browser, dove il JS si esegue da solo.
Esempio: Python + requests (con CSRF e paginazione)
import re
import requests
session = requests.Session()
BASE = "https://example-shop.com"
# 1. Carichiamo la pagina per ottenere il token CSRF e i cookie di sessione
resp = session.get(f"{BASE}/catalog", headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
})
# Il CSRF può trovarsi in un tag meta...
m = re.search(r'name="csrf-token"\s+content="([^"]+)"', resp.text)
csrf = m.group(1) if m else session.cookies.get("XSRF-TOKEN")
headers = {
"X-CSRF-Token": csrf,
"X-Requested-With": "XMLHttpRequest",
"Referer": f"{BASE}/catalog",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept": "application/json",
}
# 2. Interroghiamo l'API interna pagina per pagina
all_items = []
page = 1
while True:
r = session.get(
f"{BASE}/api/products",
params={"category": "phones", "page": page, "per_page": 48},
headers=headers,
)
r.raise_for_status()
payload = r.json()
items = payload.get("items", [])
if not items:
break
all_items.extend(items)
page += 1
for it in all_items:
print(it["title"], it["price"])Esempio: Python + httpx (async, più veloce su grandi volumi)
import asyncio
import httpx
async def fetch_page(client, page):
r = await client.get("/api/products", params={"page": page, "per_page": 48})
return r.json().get("items", [])
async def main():
async with httpx.AsyncClient(base_url="https://example-shop.com",
headers={"X-Requested-With": "XMLHttpRequest"}) as client:
tasks = [fetch_page(client, p) for p in range(1, 11)]
results = await asyncio.gather(*tasks)
items = [x for chunk in results for x in chunk]
print(len(items))
asyncio.run(main())Esempio: Node.js + fetch
const csrf = "..."; // estratto in anticipo dall'HTML/cookie
const res = await fetch("https://example-shop.com/api/products?page=1&per_page=48", {
headers: {
"X-CSRF-Token": csrf,
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json",
"Cookie": "sessionid=abc123; XSRF-TOKEN=" + csrf,
},
});
const data = await res.json();
data.items.forEach(item => console.log(item.title, item.price));Approccio 2. Emulazione completa del browser
L’idea
Avviamo un motore reale (Chromium, Firefox, WebKit): scarica la pagina, esegue tutto il JS e renderizza il DOM. Da lì lavoriamo sulla pagina come farebbe una persona: aspettiamo che compaiano gli elementi, clicchiamo, scorriamo, trasciniamo gli slider. Tutti i token, le firme e gli script anti-bot si eseguono da soli — non dobbiamo riprodurli.
Svantaggi: è di un ordine di grandezza più lento, vorace di CPU/RAM e più facile da rilevare per i sistemi anti-bot (ma a questo si rimedia con appositi modi «stealth»).
Con che cosa emulare
- Selenium — lo standard più datato; supporta Python, Java, C#, JavaScript e Ruby. Controlla browser reali tramite WebDriver.
- Playwright — un framework moderno di Microsoft. Python, JavaScript/TS, .NET, Java. Chromium, Firefox e WebKit «pronti all’uso», attesa automatica intelligente degli elementi e una comoda intercettazione delle richieste di rete.
- Puppeteer — Node.js; in origine solo Chromium (esiste un supporto sperimentale per Firefox). Molto veloce e maturo per Chrome.
Azioni sulla pagina
Qui sotto — le stesse quattro azioni (clic, scroll, scroll fino a un elemento, tenere premuto il tasto del mouse + movimento) su stack diversi. Tenere premuto + movimento è la base del drag-and-drop, degli slider e dei captcha a slider.
Playwright (Python)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example-shop.com/catalog")
# CLIC
page.click("button.load-more")
# SCROLL con la rotellina
page.mouse.wheel(0, 2000)
# SCROLL fino a un elemento specifico
page.locator("footer").scroll_into_view_if_needed()
# TIENI PREMUTO il tasto + MOVIMENTO (drag / slider)
box = page.locator(".slider-handle").bounding_box()
start_x = box["x"] + box["width"] / 2
start_y = box["y"] + box["height"] / 2
page.mouse.move(start_x, start_y)
page.mouse.down() # tasto premuto
page.mouse.move(start_x + 200, start_y, steps=25) # spostamento graduale (25 passi)
page.mouse.up() # rilasciamo
browser.close()Playwright (JavaScript/Node)
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example-shop.com/catalog');
// CLIC
await page.click('button.load-more');
// SCROLL
await page.mouse.wheel(0, 2000);
// SCROLL fino all'elemento
await page.locator('footer').scrollIntoViewIfNeeded();
// TIENI PREMUTO + MOVIMENTO
const box = await page.locator('.slider-handle').boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 200, box.y, { steps: 25 });
await page.mouse.up();
await browser.close();
})();Selenium (Python) — con ActionChains
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example-shop.com/catalog")
wait = WebDriverWait(driver, 10)
# CLIC (con attesa della cliccabilità)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.load-more")))
btn.click()
# SCROLL
driver.execute_script("window.scrollBy(0, 2000)")
# SCROLL fino all'elemento
footer = driver.find_element(By.CSS_SELECTOR, "footer")
driver.execute_script("arguments[0].scrollIntoView({block:'center'})", footer)
# TIENI PREMUTO + MOVIMENTO
handle = driver.find_element(By.CSS_SELECTOR, ".slider-handle")
(ActionChains(driver)
.click_and_hold(handle) # tasto premuto
.move_by_offset(200, 0) # spostiamo di 200 px a destra
.pause(0.3)
.release() # rilasciamo
.perform())
driver.quit()Selenium (Java)
WebDriver driver = new ChromeDriver();
driver.get("https://example-shop.com/catalog");
// CLIC
driver.findElement(By.cssSelector("button.load-more")).click();
// SCROLL
((JavascriptExecutor) driver).executeScript("window.scrollBy(0, 2000)");
// TIENI PREMUTO + MOVIMENTO
WebElement handle = driver.findElement(By.cssSelector(".slider-handle"));
new Actions(driver)
.clickAndHold(handle)
.moveByOffset(200, 0)
.release()
.perform();Puppeteer (Node.js)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('https://example-shop.com/catalog');
// CLIC
await page.click('button.load-more');
// SCROLL
await page.evaluate(() => window.scrollBy(0, 2000));
// SCROLL fino all'elemento
await page.$eval('footer', el => el.scrollIntoView());
// TIENI PREMUTO + MOVIMENTO
const handle = await page.$('.slider-handle');
const box = await handle.boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 200, box.y, { steps: 25 });
await page.mouse.up();
await browser.close();
})();Modalità di mascheramento (stealth): eludere il rilevamento dell’automazione
Un browser headless eseguito «così com’è» si distingue facilmente da uno reale. I sistemi anti-bot (Cloudflare, DataDome, PerimeterX/HUMAN, Akamai ecc.) controllano decine di segnali e, se anche solo una parte tradisce l’automazione, arrivano il captcha, il challenge o il blocco. La modalità stealth è un insieme di patch e tecniche che mascherano questi indizi.
Da quali indizi ti individuano
navigator.webdriver === true— la spia più evidente, che il browser controllato da WebDriver/CDP attiva automaticamente.- Artefatti dell’headless. Assenza di
window.chrome, elenco dei plugin vuoto (navigator.plugins), valori anomali dinavigator.languages, un renderer WebGL del tipoSwiftShader/Google Inc.invece di una scheda grafica reale. - Fingerprinting. Canvas, WebGL, AudioContext e l’insieme dei font generano un’«impronta» stabile dell’ambiente; in un headless di default risulta sospettosamente generica.
- Impronta TLS/JA3. A livello della connessione HTTP stessa, l’«handshake» di un client Python o Node differisce da quello di Chrome — e viene rilevato ancora prima dell’esecuzione del JS (vale anche per l’approccio 1).
- Comportamento. Clic istantanei senza movimento del mouse, tempi perfettamente regolari, l’accesso diretto a una pagina interna senza navigazione — tutto questo non è umano.
- Reputazione dell’IP. Gli intervalli dei data center (AWS, Hetzner ecc.) sono segnalati; gli indirizzi residenziali (residential) e mobili destano meno sospetti.
Strumenti già pronti
puppeteer-extra+puppeteer-extra-plugin-stealth(Node) — il pacchetto più noto; nascondenavigator.webdriver, corregge WebGL/plugins/languages e decine di altre «fughe».playwright-extracon lo stesso plugin stealth — l’equivalente per Playwright su Node.undetected-chromedriver(Python, sopra Selenium) — un ChromeDriver patchato che supera molti controlli di Cloudflare. La sua evoluzione ènodriver(senza il protocollo webdriver, puramente tramite CDP).SeleniumBasein modalità UC (--uc) — un wrapper su Selenium con anti-detection integrato.rebrowser-patches— patch di basso livello per il runtime di Puppeteer/Playwright che chiudono fughe di CDP più sottili.
Importante: nessun plugin stealth offre garanzie. I sistemi anti-bot si aggiornano di continuo e ciò che passava ieri domani può essere rilevato. È una «corsa agli armamenti», non una configurazione una tantum.
Esempi
Python — undetected-chromedriver:
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.add_argument("--lang=it-IT")
# per l'anti-detection di solito NON si usa headless, oppure si usa la modalità nuova:
# options.add_argument("--headless=new")
driver = uc.Chrome(options=options)
driver.get("https://example-shop.com/catalog")
print(driver.title)
driver.quit()Node — puppeteer-extra + stealth:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
// UA credibile e header coerenti
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
);
await page.goto('https://example-shop.com/catalog');
await browser.close();
})();Node — playwright-extra + stealth:
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();
chromium.use(stealth);
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example-shop.com/catalog');
await browser.close();
})();Tecniche manuali (in aggiunta ai plugin o al loro posto)
- Nascondere
webdrivere sistemare l’ambiente. Tramite CDP/init scriptprima che la pagina carichi:
# Playwright (Python): viene eseguito in ogni nuovo documento PRIMA degli script del sito
page.add_init_script(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
)- Nuova modalità headless. Nelle versioni recenti di Chrome il flag
--headless=newè più vicino a un browser normale rispetto al vecchio headless; a volte conviene eseguire direttamente in modalità non headless sotto un display virtuale (Xvfb). - Profilo persistente. Avviare con
user_data_dirconserva i cookie e il «riscaldamento» della sessione tra un’esecuzione e l’altra — assomiglia meno a un bot appena creato. - Comportamento umano. Pause casuali, movimento del mouse lungo una curva (Bézier), scroll a scatti anziché in un unico salto. Per generare le traiettorie esistono librerie come
pyautogui/bezieroppure il parametrostepsintegrato inmouse.move. - Proxy. I proxy residenziali e mobili con rotazione riducono sensibilmente la quota di challenge rispetto agli IP dei data center.
Mascheramento (stealth) per l’approccio 1 (senza browser)
Anche l’intercettazione dell’API viene rilevata — tramite l’impronta TLS del client HTTP. Perché la richiesta sembri, già a livello di «handshake», un Chrome reale, si usano client che falsificano l’impronta TLS:
# curl_cffi sa imitare il TLS/JA3 di un browser specifico
from curl_cffi import requests
r = requests.get(
"https://example-shop.com/api/products?page=1",
impersonate="chrome124", # falsifichiamo l'handshake di Chrome 124
)
print(r.json())Alternative: tls-client (Python/Go), curl-impersonate (un binario di sistema). Questo spesso risolve il problema quando un requests «nudo» riceve un 403 mentre nel browser quella stessa pagina si apre.
Cosa sa fare ciascuna libreria
Lo spartiacque fondamentale: se lo strumento esegue JavaScript e se sa imitare le azioni del mouse. I client HTTP e i parser HTML non fanno né l’una né l’altra cosa — servono solo per i siti statici o per il primo approccio (l’emulazione dell’API).
| Libreria | Linguaggio | Rendering JS | Azioni (clic/scroll/drag) | Uso |
|---|---|---|---|---|
| requests / httpx | Python | No | No | Client HTTP |
| aiohttp | Python | No | No | HTTP asincrono |
| BeautifulSoup / lxml | Python | No | No | Parsing di HTML |
| Scrapy | Python | No (serve il plugin Splash/Playwright) | No | Framework di crawling |
| Selenium | Python/Java/C#/JS/Ruby | Sì | Sì | Controllo del browser |
| Playwright | Python/JS/.NET/Java | Sì | Sì | Controllo del browser |
| Puppeteer | Node.js | Sì (Chromium) | Sì | Controllo del browser |
| Cypress | JS | Sì | Sì (ma orientato ai test e2e) | Test |
| axios / fetch / got | Node.js | No | No | Client HTTP |
| cheerio | Node.js | No | No | Parsing di HTML (stile jQuery) |
| Colly | Go | No | No | Framework di crawling |
| chromedp / rod | Go | Sì | Sì | Controllo del browser |
| HtmlUnit | Java | Parziale/instabile | Limitate | Browser headless |
| jsoup | Java | No | No | Parsing di HTML |
Conclusione breve:
- Devi solo ripetere una richiesta all’API →
requests/httpx(Python),fetch/got(Node),Colly(Go). - Fare il parsing di un HTML già ottenuto →
BeautifulSoup/lxml,cheerio,jsoup. - Ti serve il rendering del JS e le azioni con il mouse →
Playwright,Selenium,Puppeteer,chromedp/rod. - HtmlUnit gestisce un po’ di JS, ma con le SPA moderne inciampa spesso — per un rendering serio si ricorre a Playwright/Selenium.
Caso pratico: monitoraggio di negozi online e scroll infinito
È forse il compito pratico più frequente. Nei cataloghi i prodotti di solito non vengono mostrati tutti in una volta: si applica il lazy loading / scroll infinito — le nuove schede si caricano man mano che scorri (o premendo «Mostra altro»). Una semplice richiesta dell’HTML restituirà solo la prima «porzione».
Ci sono due strade, entrambe molto usate nel monitoraggio di prezzi e assortimento:
Strada A (preferibile): intercettare l’API di paginazione
Durante lo scroll il negozio chiama quasi sempre qualcosa come /api/catalog?page=2&offset=48. Se è così — dimentica il browser e raccogli i dati pagina per pagina in modo diretto (vedi l’approccio 1). È veloce, stabile e scala fino a migliaia di prodotti. È così che si costruisce la maggior parte dei monitoraggi industriali: il browser si usa una sola volta — per esplorare la struttura dell’API e i token, mentre la raccolta vera e propria passa da un client HTTP.
Strada B: rendering + scroll quando l’API è chiusa
Se l’endpoint è protetto da una firma non banale o i dati vengono generati esclusivamente sul client, non resta che scorrere con il browser e raccogliere le schede dal DOM. L’algoritmo: scorriamo verso il basso → aspettiamo il caricamento → contiamo le schede → ripetiamo finché il numero cresce.
from playwright.sync_api import sync_playwright
def scrape_catalog(url):
products = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url)
prev_count = -1
stable_rounds = 0
while stable_rounds < 2: # 2 scroll «vuoti» consecutivi = siamo arrivati alla fine
# scorriamo fino in fondo
page.mouse.wheel(0, 4000)
page.wait_for_timeout(1500) # diamo tempo al caricamento
cards = page.locator(".product-card")
count = cards.count()
if count == prev_count:
stable_rounds += 1
else:
stable_rounds = 0
prev_count = count
# raccogliamo tutte le schede dopo lo scroll completo
cards = page.locator(".product-card")
for i in range(cards.count()):
card = cards.nth(i)
products.append({
"title": card.locator(".title").inner_text(),
"price": card.locator(".price").inner_text(),
"url": card.locator("a").get_attribute("href"),
})
browser.close()
return products
data = scrape_catalog("https://example-shop.com/catalog/phones")
print(f"Prodotti raccolti: {len(data)}")La stessa tecnica per il pulsante «Mostra altro» — clicchiamo finché il pulsante esiste:
while page.locator("button.load-more").count() > 0:
page.click("button.load-more")
page.wait_for_timeout(1200)Ibrido: il meglio dei due mondi
Playwright e Puppeteer sanno ascoltare il traffico di rete. Puoi aprire la pagina nel browser (così passano tutti i token e i controlli anti-bot), ma prelevare i dati non dal DOM, bensì dalle risposte di quella stessa API che il browser chiama durante lo scroll:
def handle_response(response):
if "/api/products" in response.url:
data = response.json()
# salviamo il JSON già pronto — non serve fare il parsing dell'HTML
save(data["items"])
page.on("response", handle_response)
page.goto("https://example-shop.com/catalog")
# poi basta scorrere — i dati «arrivano» da soli al gestoreÈ spesso l’opzione ottimale per il monitoraggio: la robustezza dell’emulazione del browser + un JSON pulito e strutturato al posto del fragile parsing del markup.
Consigli pratici per il monitoraggio
- Aspetta i dati, non il tempo. Invece di «dormire 1,5 secondi», usa l’attesa della comparsa di un elemento (
wait_for_selector) o della quiete della rete (wait_for_load_state("networkidle")) — è più affidabile e spesso più veloce. - Deduplicazione. Con lo scroll infinito alcune schede possono essere lette più volte — raccogli per
id/urlunivoco. - Limita la frequenza. Richieste troppo aggressive sovraccaricano il sito e portano rapidamente al blocco. Inserisci ritardi e usa i pool di proxy con cautela e nel rispetto della legge.
- Metti in cache la fase di esplorazione. Individua la struttura dell’API e i token una sola volta; in produzione esegui una raccolta HTTP leggera e tieni il browser pesante come riserva.
Come scegliere l’approccio
| Criterio | Intercettazione dell’API | Emulazione del browser |
|---|---|---|
| Velocità | Molto alta | Bassa |
| Consumo di risorse | Minimo | Alto (CPU/RAM) |
| Complessità di configurazione | Maggiore (reverse engineering dei token) | Minore (tutto «come un utente») |
| Robustezza ai cambi di markup | Alta (dipende dall’API) | Media (dipende dai selettori) |
| Superare token/firme lato client | Difficile | Automatico |
| Scalabilità sul volume | Eccellente | Limitata |
Regola pratica: verifica sempre prima se basta l’intercettazione dell’API — è più veloce, più economica e più stabile. Passa all’emulazione del browser solo quando l’API è nascosta dietro crittografia lato client, protetta da una logica anti-bot complessa o quando devi riprodurre un’interazione non banale (drag-and-drop, slider, form a più passaggi).