Скрейпер возвращает пустую страницу? Это JavaScript. Вот как это исправить
Тег body пуст в вашем скрейпере, но полон в DevTools. Это не ошибка — это клиентская отрисовка. Вот как это определить, три способа получить данные и как избежать запуска ненужного браузера.
Вы загружаете страницу, выводите HTML, и нужная часть отсутствует — пустой <body>, пустой список узлов или заглушка с надписью «Требуется JavaScript». Между тем, инспектор браузера показывает данные прямо там. Это не ошибка скрейпера. Это страница, которая отрисовывает свой контент на клиентской стороне с помощью JavaScript, и ваш HTTP-запрос получает только пустую оболочку, которая запускается до выполнения JavaScript. Вот как это подтвердить, три способа получить данные в любом случае и как избежать запуска ненужного безголового браузера.
Почему body пустой
Сырой HTTP-клиент — Python requests, Node fetch, cURL — загружает HTML, который отправляет сервер, и останавливается. Он не выполняет JavaScript. Сайты, построенные на React, Vue, Angular или Next.js, отправляют почти пустой HTML-скелет плюс пакет, который загружает данные и строит DOM в браузере. Парсеры, такие как BeautifulSoup или Cheerio, видят скелет, поэтому селектор, который идеально работает в DevTools, возвращает пустой массив в вашем коде. Cheerio, возвращающий [] на странице React, является каноническим симптомом; он не выполняет JavaScript и никогда не будет.
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
Подтвердите это за десять секунд
Прежде чем обращаться к браузеру, докажите диагноз. Два быстрых теста точно покажут, с чем вы имеете дело: сравните сырой ответ с тем, что отображает браузер, и посчитайте элементы в консоли. Если консоль находит узлы, которых нет в сыром HTML, контент клиентский. Если сырой HTML уже содержит его, проблема в вашем селекторе или заголовках, а не в отрисовке.
# 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
Еще одна ветвь для исключения: пустой body не всегда связан с JavaScript. Иногда сервер удерживает контент, пока вы не отправите правильные заголовки или куки — сессионный куки, referer, Accept-Language или User-Agent, который не кричит «скрипт». Если cURL возвращает короткую страницу, но ваш браузер (который отправляет полный набор заголовков) возвращает полную, попробуйте воспроизвести заголовки запроса браузера, прежде чем предполагать отрисовку. Это пятиминутный тест, который может спасти вас от запуска ненужного браузера.

Исправление 1: используйте скрытый JSON API (самый быстрый)
Вот что большинство людей упускает: если страница рисует себя с помощью JavaScript, данные поступают откуда-то — обычно из внутреннего JSON-эндпоинта, который фронтенд вызывает после загрузки. Откройте вкладку Network, отфильтруйте по Fetch/XHR, перезагрузите и найдите запрос, который возвращает ваши данные. Прямой вызов этого эндпоинта быстрее, легче и гораздо менее хрупок, чем парсинг HTML, потому что вы пропускаете весь браузер и сразу получаете структурированный JSON. Иногда эндпоинт требует токен, который встраивается в начальный HTML — извлеките его со страницы, затем вызовите 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];
Исправление 2: извлеките JSON гидратации
Даже когда нет отдельного вызова API, фреймворки встраивают начальное состояние прямо в HTML, чтобы клиент мог «гидратировать» без обратного запроса. Next.js отправляет его в теге <script id="__NEXT_DATA__">; другие стеки используют window.__INITIAL_STATE__ или подобный объект. Этот JSON уже в сыром ответе, который вы загрузили — отрисовка не требуется. Разберите тег script, и вы часто получите точно структурированные данные, которые страница бы отобразила, чище, чем скрейпинг отрисованного 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
}
Между скрытым API и гидратацией большинство проблем «пустой страницы» решаются без запуска браузера. Наш гид по скрейпингу сайтов с большим количеством JavaScript углубляется в надежный поиск этих эндпоинтов.
Предостережение по обоим методам: скрытые эндпоинты и объекты состояния — неофициальные поверхности, поэтому они меняются без предупреждения. Оберните разбор в проверку, которая громко сигнализирует об ошибке, когда структура меняется — отсутствующий ключ или массив нулевой длины должны вызывать ошибку, а не тихо возвращать ничего. Таким образом, реструктуризация API сайта проявится как явная ошибка в ваших логах, а не как пустой набор данных, который вы не заметите неделю. Закрепите User-Agent и заголовки, которые вы использовали, когда это работало; некоторые из этих эндпоинтов тихо зависят от них.
Исправление 3: отрисуйте — но рассматривайте это как последний вариант
Когда данные действительно строятся в DOM без открытого JSON, вам нужно выполнить JavaScript. Playwright или Puppeteer управляют реальным браузером, ждут контента и передают вам готовый HTML. Это работает везде, но это дорогой вариант: отрисовка стоит примерно в 10-50 раз больше пропускной способности и времени, чем вызов JSON, и безголовый браузер на IP-адресе дата-центра быстро блокируется. Направьте его через резидентный прокси и ждите правильного сигнала, а не фиксированного ожидания.
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();
Отрисуйте и вращайте в одном вызове с Scraper API

Когда отрисовка самостоятельно перестает быть оправданной
Управление собственным флотом браузеров означает управление обнаружением безголовости, ротацией прокси, утечками памяти и логикой ожидания для каждой цели. Когда вы уже поддерживаете это, Scraper API, который отрисовывает JavaScript, вращает резидентные IP и возвращает чистый HTML, JSON или markdown в одном запросе, обычно требует меньше кода и имеет более высокий уровень успеха — и он может выполнять извлечение AI на отрисованной странице, чтобы вы полностью избегали селекторов. Наш анализ стоимости отрисовки по сравнению с простыми запросами показывает, когда стоит переключиться.
Часто задаваемые вопросы
Почему мой веб-скрейпер возвращает пустой body?
Потому что страница отрисовывает свой контент с помощью JavaScript после загрузки HTML, и ваш HTTP-клиент не выполняет JavaScript. Вы получаете пустой скелет, который отправляет сервер. Подтвердите это, сравнив сырой ответ (через cURL) с тем, что показывает DevTools — если в браузере есть данные, которых нет в сыром HTML, это клиентская отрисовка.
Как скрейпить страницу, отрисованную JavaScript, без браузера?
Два способа полностью избежать браузера. Найдите внутренний JSON-эндпоинт, который страница вызывает на вкладке Network, и запросите его напрямую, или извлеките состояние гидратации, встроенное в HTML (например, тег скрипта __NEXT_DATA__). Оба способа дают вам структурированные данные быстрее и надежнее, чем отрисовка.
Выполняют ли BeautifulSoup или Cheerio JavaScript?
Нет. Оба парсят строку HTML, которую вы им передаете, и ни один из них не выполняет JavaScript. На странице React, Vue или Next.js они видят только предрендерный скелет, поэтому ваш селектор возвращает пустой массив. Вам нужен отрисованный DOM (Playwright/Puppeteer) или исходный JSON, чтобы получить данные.
Почему я получаю сообщение «JavaScript отключен» при скрейпинге?
Сайт предоставляет запасной вариант для клиентов, которые не выполняют JavaScript, и ваш скрейпер выглядит как один из них. Это часто сопровождается обнаружением ботов. Выполнение страницы в безголовом браузере на чистом резидентном IP устраняет это; если это повторяется, цель определяет отпечаток запроса, а не просто проверяет наличие JavaScript.
Пустая страница — это диагноз, а не тупик. Подтвердите, что это клиентская отрисовка, сначала обратитесь к скрытому API или JSON гидратации, и отрисовывайте только тогда, когда данные больше нигде не могут существовать. Вы напишете меньше кода, используете меньшую часть пропускной способности и будете блокироваться гораздо реже. Если вы предпочитаете не поддерживать все это, Scraper API справляется с отрисовкой и ротацией за вас.