Il tuo scraper restituisce una pagina vuota? È JavaScript. Ecco come risolverlo

Il tag body è vuoto nel tuo scraper ma pieno in DevTools. Non è un bug — è il rendering lato client. Ecco come riconoscerlo, tre modi per ottenere i dati e come evitare di lanciare un browser che non ti serve.

Recuperi la pagina, stampi l'HTML e la parte che volevi manca — un <body> vuoto, un elenco di nodi vuoto o un messaggio che dice "JavaScript è richiesto." Nel frattempo, l'ispettore del browser mostra i dati proprio lì. Questo non è un bug dello scraper. È una pagina che renderizza il suo contenuto lato client con JavaScript, e la tua richiesta HTTP ottiene solo il guscio vuoto che viene eseguito prima del JavaScript. Ecco come confermarlo, tre modi per ottenere comunque i dati e come evitare di avviare un browser headless che non ti serve realmente.

Perché il body è vuoto

Un client HTTP grezzo — Python requests, Node fetch, cURL — scarica l'HTML inviato dal server e si ferma. Non esegue JavaScript. I siti costruiti su React, Vue, Angular o Next.js inviano uno scheletro HTML quasi vuoto più un bundle che recupera i dati e costruisce il DOM nel browser. Parser come BeautifulSoup o Cheerio vedono lo scheletro, quindi un selettore che funziona perfettamente in DevTools restituisce un array vuoto nel tuo codice. Cheerio che restituisce [] su una pagina React è il sintomo canonico; non esegue JavaScript e non lo farà mai.

import * as cheerio from "cheerio";

const html = await (await fetch("https://shop.example.com/products")).text();
const $ = cheerio.load(html);

console.log($("div.product").length);
// 0  ->  the products are drawn by JavaScript, not in the HTML

Confermarlo in dieci secondi

Prima di ricorrere a un browser, prova la diagnosi. Due controlli rapidi ti dicono esattamente con cosa hai a che fare: confronta la risposta grezza con ciò che il browser renderizza e conta gli elementi nella console. Se la console trova nodi che l'HTML grezzo non contiene, il contenuto è lato client. Se l'HTML grezzo li ha già, il problema è il tuo selettore o le tue intestazioni, non il rendering.

# does the raw response actually contain the data?
curl -s https://shop.example.com/products | grep -c 'class="product"'
# 0 -> not in the HTML  |  24 -> it IS there, fix your selector

# then, in the browser DevTools console on the same page:
# document.querySelectorAll('div.product').length   -> 24

Un altro ramo da escludere: un body vuoto non è sempre JavaScript. A volte il server trattiene il contenuto finché non invii le intestazioni o i cookie giusti — un cookie di sessione, un referer, un Accept-Language o un User-Agent che non urla "script." Se cURL restituisce una pagina breve ma il tuo browser (che invia un set completo di intestazioni) restituisce una completa, prova a riprodurre le intestazioni della richiesta del browser prima di assumere il rendering. È un test di cinque minuti che può salvarti dal lanciare un browser che non ti serviva.

Diagramma di checklist che distingue le pagine HTML statiche dalle pagine renderizzate con JavaScript tramite segni come tag body vuoti, JSON di stato incorporato e chiamate XHR
Body vuoto, un blob di stato del framework nell'HTML grezzo o chiamate XHR nella scheda Network: tre indicatori affidabili che la pagina viene renderizzata lato client.

Soluzione 1: accedi all'API JSON nascosta (più veloce)

Ecco la cosa che la maggior parte delle persone manca: se una pagina si disegna con JavaScript, i dati arrivano da qualche parte — di solito un endpoint JSON interno che il frontend chiama dopo il caricamento. Apri la scheda Network, filtra per Fetch/XHR, ricarica e cerca la richiesta che restituisce i tuoi dati. Chiamare direttamente quell'endpoint è più veloce, leggero e molto meno fragile che analizzare l'HTML, perché salti tutto il browser e ottieni subito JSON strutturato. A volte l'endpoint necessita di un token che l'HTML iniziale incorpora — prendilo dalla pagina, poi chiama l'API.

// 1) the SPA loads data from its own endpoint after paint
const res = await fetch("https://shop.example.com/api/catalog?page=1", {
  headers: { accept: "application/json", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" },
});
const { products } = await res.json();
console.log(products.length); // real data, no browser

// 2) if it needs a token, the initial HTML usually carries it
const page = await (await fetch("https://shop.example.com/")).text();
const token = page.match(/"s_token":\s*"([^"]+)"/)?.[1];

Soluzione 2: estrai il JSON di idratazione

Anche quando non c'è una chiamata API separata, i framework incorporano lo stato iniziale direttamente nell'HTML in modo che il client possa "idratare" senza un viaggio di ritorno. Next.js lo invia in un tag <script id="__NEXT_DATA__">; altri stack usano window.__INITIAL_STATE__ o un blob simile. Quel JSON è nella risposta grezza che hai già scaricato — non è necessario il rendering. Analizza il tag script e spesso ottieni esattamente i dati strutturati che la pagina avrebbe mostrato, più puliti rispetto a fare scraping del DOM renderizzato.

import * as cheerio from "cheerio";

const html = await (await fetch(url)).text();
const $ = cheerio.load(html);

const blob = $("#__NEXT_DATA__").html();      // Next.js embeds page state here
if (blob) {
  const data = JSON.parse(blob);
  const products = data.props.pageProps.products;
  console.log(products.length);               // structured, from static HTML
}

Tra l'API nascosta e l'estrazione di idratazione, la maggior parte dei problemi di "pagina vuota" sono risolti senza mai lanciare un browser. La nostra guida allo scraping di siti pesanti di JavaScript approfondisce come trovare questi endpoint in modo affidabile.

Una parola di cautela su entrambi: gli endpoint nascosti e i blob di stato sono superfici non ufficiali, quindi cambiano senza preavviso. Avvolgi l'analisi in un controllo che fallisce rumorosamente quando la forma cambia — una chiave mancante o un array di lunghezza zero dovrebbe sollevare un'eccezione, non restituire silenziosamente nulla. In questo modo, una ristrutturazione dell'API del sito appare come un errore chiaro nei tuoi log invece di un dataset vuoto che non noti per una settimana. Fissa il User-Agent e le intestazioni che hai usato quando funzionava, anche; alcuni di questi endpoint si basano silenziosamente su di essi.

Soluzione 3: rendi — ma trattala come ultima risorsa

Quando i dati sono realmente costruiti nel DOM senza JSON esposto, devi eseguire il JavaScript. Playwright o Puppeteer guidano un vero browser, aspettano il contenuto e ti consegnano l'HTML finito. Funziona ovunque, ma è l'opzione costosa: il rendering costa circa 10-50 volte la larghezza di banda e il tempo di una chiamata JSON, e un browser headless su un IP del datacenter viene bloccato rapidamente. Instradalo attraverso un proxy residenziale e aspetta il segnale giusto, non un'attesa fissa.

import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext({
  proxy: { server: "http://gate.quantumproxies.io:8000", username: "USER", password: "PASS" },
});
const page = await context.newPage();
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForSelector("div.product");     // wait for data, not a sleep
const html = await page.content();
await browser.close();

Rendi e ruota in una sola chiamata con Scraper API

Diagramma di flusso della scala delle soluzioni per le pagine renderizzate con JavaScript: API JSON nascosta, estrazione di idratazione, rendering con proxy, poi Scraper API
Scendi dalla scala: prova prima l'API nascosta e il JSON di idratazione, e rendi solo quando entrambi falliscono.

Quando il rendering autonomo smette di valerne la pena

Gestire la propria flotta di browser significa gestire il rilevamento headless, la rotazione dei proxy, le perdite di memoria e la logica di attesa per ogni obiettivo. Una volta che stai mantenendo tutto ciò, una Scraper API che renderizza JavaScript, ruota gli IP residenziali e restituisce HTML, JSON o markdown puliti in una sola richiesta è di solito meno codice e un tasso di successo più alto — e può eseguire l'estrazione AI sulla pagina renderizzata in modo da saltare completamente i selettori. La nostra analisi del costo del rendering rispetto alle richieste semplici mostra quando fare quel passaggio.

Domande frequenti

Perché il mio web scraper restituisce un body vuoto?

Perché la pagina renderizza il suo contenuto con JavaScript dopo il caricamento dell'HTML, e il tuo client HTTP non esegue JavaScript. Ricevi lo scheletro vuoto inviato dal server. Conferma confrontando la risposta grezza (tramite cURL) con ciò che mostra DevTools — se il browser ha dati che l'HTML grezzo manca, è rendering lato client.

Come faccio a fare scraping di una pagina renderizzata con JavaScript senza un browser?

Due modi per evitare completamente un browser. Trova l'endpoint JSON interno che la pagina chiama nella scheda Network e richiedilo direttamente, oppure estrai lo stato di idratazione incorporato nell'HTML (ad esempio il tag script __NEXT_DATA__). Entrambi ti danno dati strutturati più velocemente e in modo più affidabile rispetto al rendering.

BeautifulSoup o Cheerio eseguono JavaScript?

No. Entrambi analizzano la stringa HTML che gli dai e nessuno esegue JavaScript. Su una pagina React, Vue o Next.js vedono solo lo scheletro pre-render, motivo per cui il tuo selettore restituisce un array vuoto. Hai bisogno di un DOM renderizzato (Playwright/Puppeteer) o del JSON sottostante per ottenere i dati.

Perché ricevo un messaggio 'JavaScript è disabilitato' quando faccio scraping?

Il sito serve un fallback per i client che non eseguono JavaScript, e il tuo scraper sembra uno di questi. È spesso abbinato al rilevamento dei bot. Eseguire la pagina in un browser headless su un IP residenziale pulito lo risolve; se si ripresenta, il target sta fingerprintando la richiesta, non solo controllando per JavaScript.

Una pagina vuota è una diagnosi, non un vicolo cieco. Conferma che sia lato client, cerca prima l'API nascosta o il JSON di idratazione, e rendi solo quando i dati non hanno altro posto dove vivere. Scriverai meno codice, userai una frazione della larghezza di banda e sarai bloccato molto meno spesso. Se preferisci non mantenere nulla di tutto ciò, la Scraper API gestisce il rendering e la rotazione per te.

Prova la QuantumProxies Scraper API