Scraper zwraca pustą stronę? To JavaScript. Oto jak to naprawić
Tag body jest pusty w twoim scraperze, ale pełny w DevTools. To nie błąd — to renderowanie po stronie klienta. Oto jak to rozpoznać, trzy sposoby na zdobycie danych i jak uniknąć uruchamiania przeglądarki, której nie potrzebujesz.
Pobierasz stronę, drukujesz HTML, a część, której chciałeś, brakuje — pusty <body>, pusta lista węzłów lub komunikat „JavaScript jest wymagany.” Tymczasem inspektor przeglądarki pokazuje dane dokładnie tam. To nie jest błąd scrapera. To strona, która renderuje swoją zawartość po stronie klienta za pomocą JavaScript, a twoje żądanie HTTP otrzymuje tylko pustą powłokę, która działa przed JavaScript. Oto jak to potwierdzić, trzy sposoby na zdobycie danych i jak uniknąć uruchamiania przeglądarki, której faktycznie nie potrzebujesz.
Dlaczego body jest puste
Surowy klient HTTP — Python requests, Node fetch, cURL — pobiera HTML, który wysyła serwer i zatrzymuje się. Nie wykonuje JavaScript. Strony zbudowane na React, Vue, Angular lub Next.js dostarczają niemal pusty szkielet HTML plus pakiet, który pobiera dane i buduje DOM w przeglądarce. Parsery takie jak BeautifulSoup lub Cheerio widzą szkielet, więc selektor, który działa perfekcyjnie w DevTools, zwraca pustą tablicę w twoim kodzie. Cheerio zwracający [] na stronie React to kanoniczny objaw; nie wykonuje JavaScript i nigdy nie będzie.
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
Potwierdź to w dziesięć sekund
Zanim sięgniesz po przeglądarkę, potwierdź diagnozę. Dwa szybkie testy powiedzą ci dokładnie, z czym masz do czynienia: porównaj surową odpowiedź z tym, co renderuje przeglądarka, i policz elementy w konsoli. Jeśli konsola znajdzie węzły, których surowy HTML nie zawiera, zawartość jest po stronie klienta. Jeśli surowy HTML już to ma, problemem jest twój selektor lub nagłówki, a nie renderowanie.
# 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
Jeszcze jedna gałąź do wykluczenia: pusty body nie jest zawsze JavaScript. Czasami serwer wstrzymuje zawartość, dopóki nie wyślesz odpowiednich nagłówków lub ciasteczek — ciasteczko sesji, referer, Accept-Language lub User-Agent, który nie krzyczy „skrypt.” Jeśli cURL zwraca krótką stronę, ale twoja przeglądarka (która wysyła pełny zestaw nagłówków) zwraca pełną, spróbuj odtworzyć nagłówki żądań przeglądarki, zanim założysz renderowanie. To pięciominutowy test, który może cię uchronić przed uruchamianiem przeglądarki, której nigdy nie potrzebowałeś.

Naprawa 1: uderz w ukryte API JSON (najszybsze)
Oto co większość ludzi pomija: jeśli strona rysuje się za pomocą JavaScript, dane pochodzą skądś — zazwyczaj z wewnętrznego punktu końcowego JSON, który frontend wywołuje po załadowaniu. Otwórz zakładkę Sieć, przefiltruj do Fetch/XHR, odśwież i poszukaj żądania, które zwraca twoje dane. Bezpośrednie wywołanie tego punktu końcowego jest szybsze, lżejsze i znacznie mniej kruche niż parsowanie HTML, ponieważ pomijasz całą przeglądarkę i od razu otrzymujesz strukturalny JSON. Czasami punkt końcowy potrzebuje tokenu, który początkowy HTML osadza — pobierz go ze strony, a następnie wywołaj 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];
Naprawa 2: wydobądź JSON hydracji
Nawet gdy nie ma osobnego wywołania API, frameworki osadzają początkowy stan bezpośrednio w HTML, aby klient mógł „nawodnić” bez podróży w obie strony. Next.js dostarcza go w tagu <script id="__NEXT_DATA__">; inne stosy używają window.__INITIAL_STATE__ lub podobnego bloba. Ten JSON znajduje się w surowej odpowiedzi, którą już pobrałeś — nie jest wymagane renderowanie. Przeanalizuj tag skryptu, a często otrzymasz dokładnie strukturalne dane, które strona by wyświetliła, czystsze niż skrobanie renderowanego 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
}
Między ukrytym API a wydobywaniem hydracji, większość problemów z „pustą stroną” jest rozwiązana bez uruchamiania przeglądarki. Nasz przewodnik po skrobaniu stron ciężkich w JavaScript zagłębia się w znajdowanie tych punktów końcowych niezawodnie.
Słowo ostrzeżenia dla obu: ukryte punkty końcowe i bloby stanu to nieoficjalne powierzchnie, więc zmieniają się bez ostrzeżenia. Zawiń analizę w kontrolę, która głośno zawiedzie, gdy kształt się zmieni — brakujący klucz lub tablica o zerowej długości powinny podnieść alarm, a nie cicho zwrócić nic. W ten sposób restrukturyzacja API na stronie pojawia się jako wyraźny błąd w twoich logach, a nie pusty zestaw danych, którego nie zauważasz przez tydzień. Przypnij User-Agent i nagłówki, których używałeś, gdy to działało; niektóre z tych punktów końcowych cicho się na nich opierają.
Naprawa 3: renderuj — ale traktuj to jako ostateczność
Gdy dane są rzeczywiście budowane w DOM bez ujawnionego JSON, musisz uruchomić JavaScript. Playwright lub Puppeteer prowadzi prawdziwą przeglądarkę, czeka na zawartość i przekazuje ci gotowy HTML. Działa wszędzie, ale to droga opcja: renderowanie kosztuje około 10-50x więcej przepustowości i czasu niż wywołanie JSON, a przeglądarka bez głowy na adresie IP centrum danych jest szybko blokowana. Przekieruj ją przez proxy rezydencyjne i czekaj na właściwy sygnał, a nie na stały czas oczekiwania.
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();
Renderuj i rotuj w jednym wywołaniu z Scraper API

Kiedy samodzielne renderowanie przestaje być opłacalne
Prowadzenie własnej floty przeglądarek oznacza zarządzanie wykrywaniem bez głowy, rotacją proxy, wyciekami pamięci i logiką oczekiwania dla każdego celu. Gdy już to utrzymujesz, Scraper API, które renderuje JavaScript, rotuje IP rezydencyjne i zwraca czysty HTML, JSON lub markdown w jednym żądaniu, zwykle oznacza mniej kodu i wyższy wskaźnik sukcesu — i może uruchomić ekstrakcję AI na renderowanej stronie, więc pomijasz selektory całkowicie. Nasze zestawienie kosztów renderowania w porównaniu do zwykłych żądań pokazuje, kiedy dokonać tej zmiany.
Często zadawane pytania
Dlaczego mój scraper internetowy zwraca puste body?
Ponieważ strona renderuje swoją zawartość za pomocą JavaScript po załadowaniu HTML, a twój klient HTTP nie wykonuje JavaScript. Otrzymujesz pusty szkielet, który wysyła serwer. Potwierdź, porównując surową odpowiedź (przez cURL) z tym, co pokazuje DevTools — jeśli przeglądarka ma dane, których brakuje w surowym HTML, to renderowanie po stronie klienta.
Jak skrobać stronę renderowaną przez JavaScript bez przeglądarki?
Dwa sposoby, aby całkowicie uniknąć przeglądarki. Znajdź wewnętrzny punkt końcowy JSON, który strona wywołuje w zakładce Sieć i zażądaj go bezpośrednio, lub wyodrębnij stan hydracji osadzony w HTML (na przykład tag skryptu __NEXT_DATA__). Oba dają ci strukturalne dane szybciej i bardziej niezawodnie niż renderowanie.
Czy BeautifulSoup lub Cheerio wykonuje JavaScript?
Nie. Oba analizują ciąg HTML, który im podajesz, i żaden nie wykonuje JavaScript. Na stronie React, Vue lub Next.js widzą tylko szkielet przed renderowaniem, dlatego twój selektor zwraca pustą tablicę. Potrzebujesz renderowanego DOM (Playwright/Puppeteer) lub podstawowego JSON, aby uzyskać dane.
Dlaczego dostaję komunikat 'JavaScript jest wyłączony' podczas skrobania?
Strona serwuje wersję zapasową dla klientów, które nie uruchamiają JavaScript, a twój scraper wygląda jak jeden z nich. Często jest to połączone z wykrywaniem botów. Wykonanie strony w przeglądarce bez głowy na czystym IP rezydencyjnym to usuwa; jeśli się powtarza, cel odciska palce na żądaniu, a nie tylko sprawdza JavaScript.
Pusta strona to diagnoza, nie ślepy zaułek. Potwierdź, że to po stronie klienta, sięgnij najpierw po ukryte API lub JSON hydracji, a renderuj tylko wtedy, gdy dane nie mają innego miejsca do życia. Napiszesz mniej kodu, zużyjesz ułamek przepustowości i będziesz blokowany znacznie rzadziej. Jeśli wolisz tego nie utrzymywać, Scraper API zajmuje się renderowaniem i rotacją za ciebie.