Scraper Retorna uma Página Vazia? É o JavaScript. Veja Como Corrigir

A tag body está vazia no seu scraper, mas cheia no DevTools. Isso não é um bug — é renderização no lado do cliente. Veja como identificar, três maneiras de obter os dados e como evitar lançar um navegador desnecessário.

Você busca a página, imprime o HTML e a parte que você queria está faltando — um <body> vazio, uma lista de nós vazia ou um aviso dizendo "JavaScript é necessário." Enquanto isso, o inspetor do navegador mostra os dados bem ali. Isso não é um bug do scraper. É uma página que renderiza seu conteúdo no lado do cliente com JavaScript, e sua solicitação HTTP só recebe a casca vazia que roda antes do JavaScript. Veja como confirmar isso, três maneiras de obter os dados de qualquer forma e como evitar iniciar um navegador headless que você realmente não precisa.

Por que o body está vazio

Um cliente HTTP bruto — Python requests, Node fetch, cURL — baixa o HTML que o servidor envia e para. Ele não executa JavaScript. Sites construídos em React, Vue, Angular ou Next.js enviam um esqueleto HTML quase vazio mais um pacote que busca dados e constrói o DOM no navegador. Analisadores como BeautifulSoup ou Cheerio veem o esqueleto, então um seletor que funciona perfeitamente no DevTools retorna um array vazio no seu código. Cheerio retornando [] em uma página React é o sintoma clássico; ele não executa JavaScript, e nunca o fará.

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

Confirme em dez segundos

Antes de recorrer a um navegador, prove o diagnóstico. Duas verificações rápidas dizem exatamente com o que você está lidando: compare a resposta bruta com o que o navegador renderiza e conte os elementos no console. Se o console encontrar nós que o HTML bruto não contém, o conteúdo é do lado do cliente. Se o HTML bruto já o tiver, seu seletor ou seus cabeçalhos são o problema, não a renderização.

# 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

Mais uma ramificação para descartar: um body vazio nem sempre é JavaScript. Às vezes, o servidor retém o conteúdo até você enviar os cabeçalhos ou cookies corretos — um cookie de sessão, um referer, um Accept-Language ou um User-Agent que não grite "script." Se o cURL retornar uma página curta, mas seu navegador (que envia um conjunto completo de cabeçalhos) retornar uma completa, tente reproduzir os cabeçalhos da solicitação do navegador antes de assumir que é renderização. É um teste de cinco minutos que pode poupá-lo de lançar um navegador que você nunca precisou.

Diagrama de checklist distinguindo páginas HTML estáticas de páginas renderizadas por JavaScript por sinais como tags body vazias, JSON de estado embutido e chamadas XHR
Body vazio, um blob de estado de framework no HTML bruto ou chamadas XHR na aba Network: três sinais confiáveis de que a página é renderizada no lado do cliente.

Correção 1: acesse o JSON API oculto (mais rápido)

Aqui está o que a maioria das pessoas perde: se uma página se desenha com JavaScript, os dados vêm de algum lugar — geralmente um endpoint JSON interno que o frontend chama após o carregamento. Abra a aba Network, filtre para Fetch/XHR, recarregue e procure a solicitação que retorna seus dados. Chamar esse endpoint diretamente é mais rápido, leve e muito menos frágil do que analisar HTML, porque você pula todo o navegador e obtém JSON estruturado imediatamente. Às vezes, o endpoint precisa de um token que o HTML inicial incorpora — pegue-o da página e, em seguida, chame a 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];

Correção 2: extraia o JSON de hidratação

Mesmo quando não há uma chamada de API separada, frameworks incorporam o estado inicial diretamente no HTML para que o cliente possa "hidratar" sem uma ida e volta. Next.js o envia em uma tag <script id="__NEXT_DATA__">; outras pilhas usam window.__INITIAL_STATE__ ou um blob semelhante. Esse JSON está na resposta bruta que você já baixou — sem necessidade de renderização. Analise a tag script e você frequentemente obtém os dados estruturados exatos que a página exibiria, mais limpos do que scraping do DOM renderizado.

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 a API oculta e a mineração de hidratação, a maioria dos problemas de "página vazia" são resolvidos sem nunca lançar um navegador. Nosso guia para scraping de sites pesados em JavaScript aprofunda-se em encontrar esses endpoints de forma confiável.

Uma palavra de cautela sobre ambos: endpoints ocultos e blobs de estado são superfícies não oficiais, então mudam sem aviso. Envolva a análise em uma verificação que falhe ruidosamente quando a forma mudar — uma chave ausente ou um array de comprimento zero deve levantar um erro, não retornar silenciosamente nada. Dessa forma, uma reestruturação do site em sua API aparece como um erro claro em seus logs, em vez de um conjunto de dados vazio que você não percebe por uma semana. Fixe o User-Agent e os cabeçalhos que você usou quando funcionou, também; alguns desses endpoints silenciosamente dependem deles.

Correção 3: renderize — mas trate como último recurso

Quando os dados são genuinamente construídos no DOM sem JSON exposto, você tem que executar o JavaScript. Playwright ou Puppeteer dirigem um navegador real, esperam pelo conteúdo e entregam o HTML finalizado. Funciona em qualquer lugar, mas é a opção cara: a renderização custa aproximadamente 10-50x a largura de banda e o tempo de uma chamada JSON, e um navegador headless em um IP de datacenter é bloqueado rapidamente. Direcione-o através de um proxy residencial e espere pelo sinal certo, não por um sono fixo.

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

Renderize e rotacione em uma chamada com Scraper API

Diagrama de fluxo da escada de correção para páginas renderizadas por JavaScript: JSON API oculta, mineração de hidratação, renderização com proxy, depois Scraper API
Desça a escada: tente primeiro a API oculta e o JSON de hidratação, e só renderize quando ambos falharem.

Quando renderizar por conta própria deixa de valer a pena

Executar sua própria frota de navegadores significa gerenciar detecção headless, rotação de proxy, vazamentos de memória e lógica de espera para cada alvo. Uma vez que você está mantendo isso, uma Scraper API que renderiza JavaScript, rotaciona IPs residenciais e retorna HTML, JSON ou markdown limpos em uma única solicitação geralmente é menos código e tem uma taxa de sucesso mais alta — e pode executar extração de IA na página renderizada para que você pule os seletores completamente. Nossa análise de custo de renderização versus solicitações simples mostra quando fazer essa mudança.

Perguntas frequentes

Por que meu web scraper está retornando um body vazio?

Porque a página renderiza seu conteúdo com JavaScript após o carregamento do HTML, e seu cliente HTTP não executa JavaScript. Você recebe o esqueleto vazio que o servidor envia. Confirme comparando a resposta bruta (via cURL) com o que o DevTools mostra — se o navegador tiver dados que o HTML bruto não tem, é renderização no lado do cliente.

Como faço para scrape uma página renderizada por JavaScript sem um navegador?

Duas maneiras evitam um navegador completamente. Encontre o endpoint JSON interno que a página chama na aba Network e solicite-o diretamente, ou extraia o estado de hidratação embutido no HTML (por exemplo, a tag script __NEXT_DATA__). Ambos fornecem dados estruturados mais rápido e de forma mais confiável do que renderizar.

BeautifulSoup ou Cheerio executam JavaScript?

Não. Ambos analisam a string HTML que você fornece e nenhum executa JavaScript. Em uma página React, Vue ou Next.js, eles só veem o esqueleto pré-renderizado, por isso seu seletor retorna um array vazio. Você precisa de um DOM renderizado (Playwright/Puppeteer) ou do JSON subjacente para obter os dados.

Por que recebo uma mensagem 'JavaScript está desativado' ao fazer scraping?

O site serve um fallback para clientes que não executam JavaScript, e seu scraper parece um. Isso geralmente é combinado com detecção de bot. Executar a página em um navegador headless em um IP residencial limpo resolve isso; se voltar a ocorrer, o alvo está identificando a solicitação, não apenas verificando o JavaScript.

Uma página vazia é um diagnóstico, não um beco sem saída. Confirme que é do lado do cliente, busque primeiro a API oculta ou o JSON de hidratação e só renderize quando os dados não tiverem outro lugar para viver. Você escreverá menos código, usará uma fração da largura de banda e será bloqueado com muito menos frequência. Se preferir não manter nada disso, a Scraper API lida com renderização e rotação para você.

Experimente o QuantumProxies Scraper API