Scraper Geeft Een Lege Pagina Terug? Het Is JavaScript. Zo Los Je Het Op

De body-tag is leeg in je scraper maar vol in DevTools. Dat is geen bug — het is client-side rendering. Hier is hoe je het herkent, drie manieren om de data te krijgen, en hoe je het opstarten van een browser die je niet nodig hebt kunt vermijden.

Je haalt de pagina op, print de HTML, en het deel dat je wilde is er niet — een lege <body>, een lege knooppuntlijst, of een melding die zegt "JavaScript is vereist." Ondertussen toont de browserinspecteur de data precies daar. Dit is geen scraper-bug. Het is een pagina die zijn inhoud client-side met JavaScript rendert, en je HTTP-verzoek krijgt alleen de lege omhulsel die draait voordat de JavaScript dat doet. Hier is hoe je dat bevestigt, drie manieren om de data toch te krijgen, en hoe je het opstarten van een headless browser die je eigenlijk niet nodig hebt kunt vermijden.

Waarom de body leeg is

Een ruwe HTTP-client — Python requests, Node fetch, cURL — downloadt de HTML die de server stuurt en stopt. Het voert geen JavaScript uit. Sites gebouwd op React, Vue, Angular of Next.js leveren een bijna lege HTML-skelet plus een pakket dat data ophaalt en de DOM in de browser bouwt. Parsers zoals BeautifulSoup of Cheerio zien het skelet, dus een selector die perfect werkt in DevTools geeft een lege array terug in je code. Cheerio die [] teruggeeft op een React-pagina is het klassieke symptoom; het voert geen JavaScript uit, en dat zal het nooit doen.

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

Bevestig het in tien seconden

Voordat je naar een browser grijpt, bewijs de diagnose. Twee snelle controles vertellen je precies waar je mee te maken hebt: vergelijk de ruwe respons met wat de browser rendert, en tel elementen in de console. Als de console knooppunten vindt die de ruwe HTML niet bevat, is de inhoud client-side. Als de ruwe HTML het al heeft, zijn je selector of je headers het probleem, niet de 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

Nog een tak om uit te sluiten: een lege body is niet altijd JavaScript. Soms houdt de server de inhoud achter totdat je de juiste headers of cookies stuurt — een sessiecookie, een referer, een Accept-Language, of een User-Agent die niet "script" schreeuwt. Als cURL een korte pagina teruggeeft maar je browser (die een volledige set headers stuurt) een volledige, probeer dan de headers van het browserverzoek te herhalen voordat je aanneemt dat het rendering is. Het is een test van vijf minuten die je kan besparen van het opstarten van een browser die je nooit nodig had.

Checklistdiagram dat statische HTML-pagina's onderscheidt van JavaScript-gerenderde pagina's door tekenen zoals lege body-tags, ingebedde state JSON, en XHR-oproepen
Lege body, een framework state blob in de ruwe HTML, of XHR-oproepen in het Netwerk-tabblad: drie betrouwbare aanwijzingen dat de pagina client-side rendert.

Oplossing 1: gebruik de verborgen JSON API (snelst)

Hier is wat de meeste mensen missen: als een pagina zichzelf tekent met JavaScript, komt de data ergens vandaan — meestal een interne JSON-endpoint die de frontend na het laden aanroept. Open het Netwerk-tabblad, filter op Fetch/XHR, herlaad, en zoek naar het verzoek dat je data teruggeeft. Dat endpoint direct aanroepen is sneller, lichter en veel minder kwetsbaar dan HTML parsen, omdat je de hele browser overslaat en direct gestructureerde JSON krijgt. Soms heeft het endpoint een token nodig dat de initiële HTML insluit — haal het van de pagina en roep dan de API aan.

// 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];

Oplossing 2: haal de hydration JSON op

Zelfs wanneer er geen aparte API-aanroep is, embedden frameworks de initiële staat direct in de HTML zodat de client kan "hydrateren" zonder een round trip. Next.js levert het in een <script id="__NEXT_DATA__">-tag; andere stacks gebruiken window.__INITIAL_STATE__ of een vergelijkbare blob. Die JSON zit in de ruwe respons die je al hebt gedownload — geen rendering vereist. Parse de script-tag, en je krijgt vaak de exacte gestructureerde data die de pagina zou hebben weergegeven, schoner dan het scrapen van de gerenderde DOM.

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
}

Tussen de verborgen API en hydration mining worden de meeste "lege pagina" problemen opgelost zonder ooit een browser op te starten. Onze gids voor het scrapen van JavaScript-zware sites gaat dieper in op het betrouwbaar vinden van deze endpoints.

Een waarschuwing voor beide: verborgen endpoints en state blobs zijn onofficiële oppervlakken, dus ze veranderen zonder waarschuwing. Omhul de parse in een controle die luid faalt wanneer de vorm verandert — een ontbrekende sleutel of een array van nul lengte zou een fout moeten geven, niet stilletjes niets teruggeven. Op die manier toont een site die zijn API herstructureert zich als een duidelijke fout in je logs in plaats van een lege dataset die je een week lang niet opmerkt. Pin de User-Agent en headers die je gebruikte toen het werkte ook; sommige van deze endpoints zijn stilletjes afhankelijk van hen.

Oplossing 3: render — maar behandel het als laatste redmiddel

Wanneer de data echt in de DOM wordt opgebouwd zonder blootgestelde JSON, moet je de JavaScript uitvoeren. Playwright of Puppeteer stuurt een echte browser aan, wacht op de inhoud, en geeft je de voltooide HTML. Het werkt overal, maar het is de dure optie: rendering kost ongeveer 10-50x de bandbreedte en tijd van een JSON-aanroep, en een headless browser op een datacenter-IP wordt snel geblokkeerd. Routeer het via een residentiële proxy en wacht op het juiste signaal, niet een vaste slaap.

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();

Render en roteer in één oproep met Scraper API

Stroomdiagram van de oplossingsladder voor JavaScript-gerenderde pagina's: verborgen JSON API, hydration mining, render met proxy, dan Scraper API
Klim de ladder af: probeer eerst de verborgen API en hydration JSON, en render alleen als beide falen.

Wanneer het renderen zelf niet meer de moeite waard is

Je eigen browservloot draaien betekent het beheren van headless detectie, proxyrotatie, geheugenlekken en wachtlogica voor elk doelwit. Zodra je dat onderhoudt, is een Scraper API die JavaScript rendert, residentiële IP's roteert en schone HTML, JSON of markdown in één verzoek teruggeeft meestal minder code en een hogere succeskans — en het kan AI-extractie uitvoeren op de gerenderde pagina zodat je selectors helemaal kunt overslaan. Onze analyse van renderkosten versus gewone verzoeken laat zien wanneer je die switch moet maken.

Veelgestelde vragen

Waarom geeft mijn web scraper een lege body terug?

Omdat de pagina zijn inhoud met JavaScript rendert nadat de HTML is geladen, en je HTTP-client geen JavaScript uitvoert. Je ontvangt het lege skelet dat de server stuurt. Bevestig door de ruwe respons (via cURL) te vergelijken met wat DevTools toont — als de browser data heeft die de ruwe HTML mist, is het client-side rendering.

Hoe scrape ik een JavaScript-gerenderde pagina zonder een browser?

Twee manieren om een browser volledig te vermijden. Zoek het interne JSON-endpoint dat de pagina aanroept in het Netwerk-tabblad en vraag het direct op, of haal de hydration state op die in de HTML is ingebed (bijvoorbeeld de __NEXT_DATA__ script-tag). Beide geven je gestructureerde data sneller en betrouwbaarder dan rendering.

Voert BeautifulSoup of Cheerio JavaScript uit?

Nee. Beide parseren de HTML-string die je ze geeft en voeren geen JavaScript uit. Op een React-, Vue- of Next.js-pagina zien ze alleen het pre-render skelet, daarom geeft je selector een lege array terug. Je hebt een gerenderde DOM (Playwright/Puppeteer) of de onderliggende JSON nodig om de data te krijgen.

Waarom krijg ik een 'JavaScript is uitgeschakeld' bericht bij het scrapen?

De site levert een fallback voor clients die geen JavaScript uitvoeren, en je scraper lijkt er een te zijn. Het gaat vaak gepaard met botdetectie. Het uitvoeren van de pagina in een headless browser op een schone residentiële IP lost het op; als het terugkomt, fingerprint de target het verzoek, niet alleen controleren op JavaScript.

Een lege pagina is een diagnose, geen doodlopende weg. Bevestig dat het client-side is, ga eerst voor de verborgen API of hydration JSON, en render alleen wanneer de data nergens anders kan leven. Je schrijft minder code, gebruikt een fractie van de bandbreedte, en wordt veel minder vaak geblokkeerd. Als je liever niets van dit alles onderhoudt, de Scraper API behandelt rendering en rotatie voor je.

Probeer de QuantumProxies Scraper API