Le Scraper Renvoie une Page Vide ? C'est JavaScript. Voici Comment le Résoudre
La balise body est vide dans votre scraper mais pleine dans DevTools. Ce n'est pas un bug — c'est le rendu côté client. Voici comment le détecter, trois façons d'obtenir les données, et comment éviter de lancer un navigateur dont vous n'avez pas besoin.
Vous récupérez la page, imprimez le HTML, et la partie que vous vouliez est manquante — une <body> vide, une liste de nœuds vide, ou un message indiquant "JavaScript est requis." Pendant ce temps, l'inspecteur du navigateur montre les données juste là. Ce n'est pas un bug du scraper. C'est une page qui rend son contenu côté client avec JavaScript, et votre requête HTTP ne reçoit que la coquille vide qui s'exécute avant le JavaScript. Voici comment le confirmer, trois façons d'obtenir les données quand même, et comment éviter de lancer un navigateur sans nécessité.
Pourquoi le corps est vide
Un client HTTP brut — Python requests, Node fetch, cURL — télécharge le HTML envoyé par le serveur et s'arrête. Il n'exécute pas JavaScript. Les sites construits sur React, Vue, Angular ou Next.js envoient un squelette HTML presque vide plus un bundle qui récupère les données et construit le DOM dans le navigateur. Les parseurs comme BeautifulSoup ou Cheerio voient le squelette, donc un sélecteur qui fonctionne parfaitement dans DevTools renvoie un tableau vide dans votre code. Cheerio renvoyant [] sur une page React est le symptôme canonique ; il n'exécute pas JavaScript, et ne le fera jamais.
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
Confirmez-le en dix secondes
Avant de vous tourner vers un navigateur, prouvez le diagnostic. Deux vérifications rapides vous indiquent exactement à quoi vous avez affaire : comparez la réponse brute à ce que le navigateur rend, et comptez les éléments dans la console. Si la console trouve des nœuds que le HTML brut ne contient pas, le contenu est côté client. Si le HTML brut les contient déjà, votre sélecteur ou vos en-têtes sont le problème, pas le rendu.
# 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
Une autre branche à écarter : un corps vide n'est pas toujours JavaScript. Parfois, le serveur retient le contenu jusqu'à ce que vous envoyiez les bons en-têtes ou cookies — un cookie de session, un referer, un Accept-Language, ou un User-Agent qui ne crie pas "script." Si cURL renvoie une page courte mais que votre navigateur (qui envoie un ensemble complet d'en-têtes) renvoie une page complète, essayez de rejouer les en-têtes de requête du navigateur avant de supposer un problème de rendu. C'est un test de cinq minutes qui peut vous éviter de lancer un navigateur dont vous n'aviez pas besoin.

Solution 1 : accédez à l'API JSON cachée (la plus rapide)
Voici ce que la plupart des gens manquent : si une page se dessine avec JavaScript, les données arrivent de quelque part — généralement un point de terminaison JSON interne que le frontend appelle après le chargement. Ouvrez l'onglet Réseau, filtrez par Fetch/XHR, rechargez, et cherchez la requête qui renvoie vos données. Appeler directement ce point de terminaison est plus rapide, plus léger et bien moins fragile que de parser le HTML, car vous sautez tout le navigateur et obtenez directement un JSON structuré. Parfois, le point de terminaison nécessite un jeton que le HTML initial intègre — récupérez-le sur la page, puis appelez 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];
Solution 2 : exploitez le JSON d'hydratation
Même lorsqu'il n'y a pas d'appel API séparé, les frameworks intègrent l'état initial directement dans le HTML pour que le client puisse "hydrater" sans aller-retour. Next.js l'expédie dans une balise <script id="__NEXT_DATA__"> ; d'autres stacks utilisent window.__INITIAL_STATE__ ou un blob similaire. Ce JSON est dans la réponse brute que vous avez déjà téléchargée — aucun rendu requis. Parsez la balise script, et vous obtenez souvent les données structurées exactes que la page aurait affichées, plus proprement que de scraper le DOM rendu.
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
}
Entre l'API cachée et l'exploitation de l'hydratation, la plupart des problèmes de "page vide" sont résolus sans jamais lancer un navigateur. Notre guide pour scraper les sites lourds en JavaScript approfondit la recherche de ces points de terminaison de manière fiable.
Un mot de prudence sur les deux : les points de terminaison cachés et les blobs d'état sont des surfaces non officielles, donc ils changent sans avertissement. Enveloppez le parse dans une vérification qui échoue bruyamment lorsque la forme change — une clé manquante ou un tableau de longueur zéro devrait lever une erreur, pas retourner silencieusement rien. De cette façon, une restructuration du site de son API apparaît comme une erreur claire dans vos journaux au lieu d'un ensemble de données vide que vous ne remarquez pas pendant une semaine. Épinglez le User-Agent et les en-têtes que vous avez utilisés lorsqu'il fonctionnait, aussi ; certains de ces points de terminaison se basent discrètement sur eux.
Solution 3 : rendez — mais considérez-le comme le dernier recours
Lorsque les données sont réellement construites dans le DOM sans JSON exposé, vous devez exécuter le JavaScript. Playwright ou Puppeteer pilote un vrai navigateur, attend le contenu, et vous remet le HTML fini. Cela fonctionne partout, mais c'est l'option coûteuse : le rendu coûte environ 10 à 50 fois la bande passante et le temps d'un appel JSON, et un navigateur sans tête sur une IP de centre de données est rapidement bloqué. Routez-le via un proxy résidentiel et attendez le bon signal, pas un sommeil fixe.
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();
Rendez et faites tourner en un seul appel avec Scraper API

Quand le rendu par vous-même cesse d'en valoir la peine
Gérer votre propre flotte de navigateurs signifie gérer la détection sans tête, la rotation des proxies, les fuites de mémoire et la logique d'attente pour chaque cible. Une fois que vous maintenez cela, une Scraper API qui rend JavaScript, fait tourner les IPs résidentielles et renvoie du HTML propre, du JSON ou du markdown en une seule requête est généralement moins de code et un taux de succès plus élevé — et elle peut exécuter l'extraction AI sur la page rendue pour que vous sautiez complètement les sélecteurs. Notre analyse de le coût du rendu par rapport aux requêtes simples montre quand faire ce changement.
Questions fréquemment posées
Pourquoi mon scraper web renvoie-t-il un corps vide ?
Parce que la page rend son contenu avec JavaScript après le chargement du HTML, et votre client HTTP n'exécute pas JavaScript. Vous recevez le squelette vide que le serveur envoie. Confirmez en comparant la réponse brute (via cURL) à ce que DevTools montre — si le navigateur a des données que le HTML brut n'a pas, c'est un rendu côté client.
Comment scraper une page rendue par JavaScript sans navigateur ?
Deux façons d'éviter complètement un navigateur. Trouvez le point de terminaison JSON interne que la page appelle dans l'onglet Réseau et demandez-le directement, ou extrayez l'état d'hydratation intégré dans le HTML (par exemple la balise script __NEXT_DATA__). Les deux vous donnent des données structurées plus rapidement et plus fiablement que le rendu.
Est-ce que BeautifulSoup ou Cheerio exécute JavaScript ?
Non. Les deux analysent la chaîne HTML que vous leur donnez et aucun n'exécute JavaScript. Sur une page React, Vue ou Next.js, ils ne voient que le squelette pré-rendu, c'est pourquoi votre sélecteur renvoie un tableau vide. Vous avez besoin d'un DOM rendu (Playwright/Puppeteer) ou du JSON sous-jacent pour obtenir les données.
Pourquoi ai-je un message 'JavaScript est désactivé' lors du scraping ?
Le site sert un fallback pour les clients qui n'exécutent pas JavaScript, et votre scraper en a l'air. C'est souvent associé à la détection de bot. Exécuter la page dans un navigateur sans tête sur une IP résidentielle propre le résout ; si cela se reproduit, la cible fingerprint la requête, pas seulement vérifier le JavaScript.
Une page vide est un diagnostic, pas une impasse. Confirmez que c'est côté client, optez d'abord pour l'API cachée ou le JSON d'hydratation, et ne rendez que lorsque les données n'ont nulle part ailleurs où vivre. Vous écrirez moins de code, utiliserez une fraction de la bande passante, et serez bloqué beaucoup moins souvent. Si vous préférez ne rien maintenir de tout cela, la Scraper API gère le rendu et la rotation pour vous.