Quase todo mundo que começa no scraping passa pelo mesmo momento: produtos, preços e avaliações estão ali no navegador, mas, quando você busca o mesmo endereço no Python com requests, os seletores não encontram nada. Olhando o HTML da página, você vê um texto "Carregando..." e algumas tags <script> em vez de uma lista de produtos. O problema não é o seu código; a página é dinâmica, e o JavaScript que roda no navegador busca o conteúdo depois.
Neste artigo, explicamos a diferença entre páginas estáticas e dinâmicas, como saber em poucos minutos se uma página é dinâmica e o que é um navegador headless. A parte em que mais nos concentramos é como chegar aos dados sem navegador headless na maioria dos casos: encontrando a requisição de API que a página faz em segundo plano e lendo o JSON embutido no HTML. Quando um navegador é realmente necessário, também mostramos as estratégias de espera certas no Playwright e formas de reduzir o custo. Rodamos os exemplos contra uma página de teste local que carrega o conteúdo com JavaScript.
O que é uma página estática?
Em uma página estática, tudo o que o navegador mostra está no HTML da primeira resposta do servidor. O servidor pode ler esse HTML de um arquivo pronto ou gerá-lo a partir de um banco de dados a cada requisição; o que importa para o scraping é que o conteúdo já está dentro do HTML quando chega ao navegador.
Você não precisa de navegador para obter dados de uma página estática:
import requests
from bs4 import BeautifulSoup
html = requests.get("https://example.com/deals", timeout=20).text
cards = BeautifulSoup(html, "html.parser").select("div.product")
print("Cards de produto encontrados:", len(cards))Sites de notícias, blogs, muitos sites corporativos e lojas online renderizadas no servidor fazem parte desse grupo.
O que é uma página dinâmica (carregada com JavaScript)?
Em uma página dinâmica, a primeira resposta do servidor é um esqueleto: o layout da página, um contêiner de lista vazio e arquivos JavaScript. O conteúdo chega depois, nestas etapas:
- O navegador recebe o esqueleto HTML e desenha na tela um placeholder como "Carregando...".
- Os arquivos JavaScript são baixados e executados. Muitas vezes um framework como React ou Vue.
- O JavaScript envia uma requisição de API. Por exemplo, com
fetchpara/api/products?category=headphones&page=1. - O servidor devolve os dados em JSON. Nomes de produto, preços, informações de estoque.
- O JavaScript monta o HTML a partir desse JSON e o insere na página. Os cards de produto só aparecem nesta etapa.
- Rolar ou clicar dispara novas requisições. Rolagem infinita, um botão "Ver mais", filtros.
Um cliente HTTP como o requests do Python só faz a primeira etapa; ele não executa JavaScript. Por isso o HTML recebido não tem cards de produto. Na nossa página de teste local, o código requests acima encontrou zero cards; a mesma página mostrava dois produtos no navegador.
Existe também uma estrutura híbrida: frameworks como Next.js e Nuxt renderizam no servidor o primeiro estado da página e também embutem os mesmos dados no HTML como um bloco JSON para o JavaScript usar. Nessas páginas, os dados estão no HTML sem executar JavaScript, mas às vezes dentro de uma tag <script>, e não nos cards visíveis.
Como saber se uma página é dinâmica?
Alguns minutos de diagnóstico antes de montar um navegador headless ajudam a escolher o caminho certo:
- Veja o código-fonte.
Ctrl + Una página (Cmd + Option + Uno macOS) mostra o HTML bruto que o servidor enviou. Procure ali um nome de produto ou um preço que aparece na página. Se encontrar, a página provavelmente é estática. - Compare com o painel Elements. O painel Elements das ferramentas do desenvolvedor mostra a página depois que o JavaScript rodou. Se o texto está em Elements, mas não no código-fonte, o conteúdo veio por JavaScript.
- Desative o JavaScript e recarregue. Nas ferramentas do desenvolvedor do Chrome, abra o menu de comandos com
Ctrl + Shift + P, digite "Disable JavaScript" e recarregue a página. Se o conteúdo sumir, a página é dinâmica. - Verifique com código. Procure no HTML obtido com
requestsum texto que você espera. Se não estiver lá e o HTML for muito curto, você recebeu um esqueleto. - Olhe o filtro Fetch/XHR no painel Network. Se, ao recarregar a página, aparecerem respostas JSON nesse filtro, os dados provavelmente estão em uma dessas requisições.
A etapa quatro tem uma armadilha: se o conteúdo não está no HTML, o motivo nem sempre é JavaScript. O site pode ter devolvido uma página de verificação ou outro conteúdo. Confira o código de status e o conteúdo da resposta; explicamos esse diagnóstico em detalhe em Como fazer web scraping sem ser bloqueado.
O que é um navegador headless?
Um navegador headless é um navegador de verdade que roda sem abrir janela na tela. O mecanismo do Chromium, do Firefox ou do WebKit carrega a página como um navegador normal, executa JavaScript, envia requisições de API e monta a página; você lê essa página com código. As ferramentas mais comuns são Playwright, Puppeteer e Selenium.
Um navegador headless faz mais do que um cliente HTTP, mas também custa mais:
- CPU e memória. Cada aba de navegador usa muitas vezes os recursos de uma requisição HTTP. O número de páginas que você roda ao mesmo tempo na mesma máquina é limitado por núcleos e memória, não pela rede.
- Tempo. Baixar e executar todo o JavaScript de uma página pode levar segundos.
- Número de requisições e banda. Uma única página gera dezenas de requisições extras de imagens, fontes, folhas de estilo, scripts de analytics e anúncios. Isso aumenta tanto o seu tráfego quanto a carga do site de destino.
- Manutenção. Versões de navegador, drivers e lógica de espera adicionam complexidade.
Por isso o navegador headless deve ser o último recurso, não a primeira escolha. Para comparar as ferramentas, veja Web scraping: JavaScript ou Python?, e para configurar o Selenium, Como usar proxy com Selenium.
Primeiro, encontre a requisição de API/XHR
Os dados de uma página dinâmica chegam ao navegador de algum lugar em JSON. Se você encontrar essa requisição, obtém os dados direto, sem renderizar a página. A referência do painel Network do Chrome explica os filtros e as opções de cópia em detalhe; o caminho curto é:
- Abra as ferramentas do desenvolvedor (F12) e vá para a aba Network.
- Marque Fetch/XHR na barra de filtros.
- Recarregue a página ou faça a ação que carrega os dados (rolar a página, escolher um filtro, ir para a próxima página).
- Clique nas requisições cujo nome contém palavras como
api,graphql,searchouproductse veja o JSON na aba Response. - Ao encontrar a requisição com os dados de que precisa, anote o endereço, os parâmetros e os cabeçalhos necessários na aba Headers. Dá para testá-la na linha de comando clicando com o botão direito na requisição e escolhendo Copy > Copy as cURL.
Repetir no Python a requisição encontrada costuma levar poucas linhas:
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "ExamplePriceBot/1.0 (+https://example.com/about-our-bot)",
"Accept": "application/json",
})
response = session.get(
"https://example.com/api/products",
params={"category": "headphones", "page": 1},
timeout=20,
)
response.raise_for_status()
for product in response.json()["products"]:
print(product["name"], product["price"])As vantagens desse caminho são grandes: os dados chegam estruturados, você não precisa de seletores HTML, o seu código não quebra quando o design da página muda e uma única requisição é uma fração pequena da carga de uma página inteira. A paginação geralmente é feita com um parâmetro page, offset ou cursor.
Pontos de atenção:
- Autenticação e tokens. Algumas requisições de API precisam de cookies, tokens de sessão ou assinaturas de curta duração. Copiar isso à mão funciona por um tempo e depois quebra.
- Termos do site. Uma API interna que o site usa no próprio front-end não é uma API pública. Aplique também a essas requisições os termos de uso e as regras de robots.txt do site; se houver uma API oficial, prefira-a. Para o enquadramento legal, veja Web scraping é legal?.
- Velocidade. Como as requisições de API são leves, dá para enviá-las muito rápido, o que também facilita ultrapassar os limites de um site. Aplique as mesmas regras de velocidade.
Ler o JSON embutido no HTML
Em sites feitos com Next.js, Nuxt e frameworks parecidos, os dados costumam estar prontos em uma tag <script> dentro do HTML. Veja o código-fonte e procure __NEXT_DATA__, __NUXT__, __INITIAL_STATE__ ou application/ld+json.
import json
import requests
from bs4 import BeautifulSoup
html = requests.get("https://example.com/deals", timeout=20).text
soup = BeautifulSoup(html, "html.parser")
data = json.loads(soup.select_one("script#__NEXT_DATA__").string)
for product in data["props"]["pageProps"]["products"]:
print(product["name"], product["price"])Os blocos application/ld+json trazem dados estruturados como nome do produto, preço, estoque e avaliação no formato schema.org e, como são preparados para os buscadores, costumam ser estáveis. Tratamos de como tornar seletores robustos contra mudanças de design em Seletor CSS ou XPath.
Estratégias de espera no Playwright
Se os dados não estão nem em uma API nem em JSON embutido, ou se chegar ao conteúdo exige interações como clicar, rolar e preencher formulários, usa-se um navegador headless. O erro mais comum nesse caso é o momento de ler: se você lê assim que a página abre, o conteúdo ainda não chegou; se espera um tempo fixo, ou perde tempo ou, com uma resposta lenta, recebe um resultado vazio do mesmo jeito.
As ferramentas de espera certas do Playwright são:
- A espera automática dos locators. Operações como
page.locator("div.product h2").inner_text()esperam o elemento aparecer na página, até o timeout padrão. A documentação de verificações de ação do Playwright lista o que é esperado. - Esperar um elemento específico.
page.locator("div.product").first.wait_for()espera o primeiro card de produto aparecer. - Esperar uma resposta específica.
page.expect_response(...)espera a requisição de dados da página voltar e entrega o JSON direto. - Não usar esperas fixas.
page.wait_for_timeout(5000)é útil em testes, mas pouco confiável em produção.
A documentação do Playwright desaconselha o estado de carregamento networkidle, ou seja, esperar o tráfego de rede parar por um tempo; em páginas cujos scripts de analytics e anúncios continuam enviando requisições, esse estado pode nunca chegar.
O exemplo abaixo abre a página por um proxy, bloqueia imagens e fontes para reduzir a carga, captura a resposta da requisição de dados e depois lê os cards renderizados:
from playwright.sync_api import sync_playwright
PROXY = {"server": "http://pr.proxynet.io:8000", "username": "user", "password": "pass"}
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY)
page = browser.new_page()
# Não baixar imagens e fontes, para economizar tempo e tráfego
page.route("**/*.{png,jpg,jpeg,webp,gif,woff2}", lambda route: route.abort())
with page.expect_response(lambda r: "/api/products" in r.url and r.ok) as response:
page.goto("https://example.com/deals")
api_data = response.value.json() # pegar o JSON direto
page.locator("div.product").first.wait_for() # ou esperar os cards renderizados
names = page.locator("div.product h2").all_inner_texts()
print(len(api_data["products"]), names)
browser.close()Neste exemplo, o JSON capturado com expect_response muitas vezes basta sozinho; nem é preciso ler os cards. No fim, você usa o navegador só para fazer a página enviar essa requisição com os parâmetros e cookies certos.
No Playwright, os dados do proxy são informados ao iniciar o navegador, com usuário e senha em campos separados. Para fluxos em que o mesmo IP precisa ser mantido durante a sessão, pode-se usar um endereço de saída fixo; para muitas páginas independentes, um Proxies rotativos, que dá um IP diferente a cada conexão. Explicamos por que telas de verificação aparecem na automação de navegador em Puppeteer e CAPTCHA.
Custo: quando o navegador headless é desnecessário?
| Abordagem | Quando funciona | Velocidade e recursos | Fragilidade | Atenção a |
|---|---|---|---|---|
| Cliente HTTP + HTML | A página é estática, o conteúdo está no código-fonte | Muito rápida, muito leve | Sensível a mudanças de design | Ancorar seletores em atributos estáveis |
| JSON embutido no HTML | Páginas híbridas como Next.js, Nuxt | Muito rápida, leve | A estrutura de dados pode mudar | Tratamento de erro que confira o caminho do JSON |
| Requisição de API em segundo plano | O conteúdo chega em JSON | Muito rápida, a mais leve | Independe do design, a API pode mudar | Termos do site, tokens, limites de taxa |
| Navegador headless | Há interação, os dados não são acessíveis de outro jeito | Lenta, pesada | Sensível à lógica de espera | Bloquear imagens, manter a concorrência baixa |
Casos típicos em que o navegador headless é desnecessário:
- O conteúdo já está no código-fonte da página.
- Os dados vêm de uma requisição JSON que pode ser repetida com parâmetros simples.
- Os dados estão em um bloco
__NEXT_DATA__ou JSON-LD. - O JavaScript só é usado para navegar entre páginas, e o conteúdo vem do servidor em cada página.
Casos em que o navegador headless é realmente necessário:
- O conteúdo só carrega com rolagem, clique ou interação com formulário, e a requisição de API correspondente não pode ser repetida.
- As requisições precisam de assinaturas ou tokens de curta duração gerados pelo JavaScript da página.
- A saída visual da página (uma captura de tela, uma verificação de layout) é o próprio trabalho.
Se usar navegador headless, ajuste o número de páginas simultâneas aos recursos da máquina; tratamos disso em Concorrência e paralelismo.
Casos de uso
- Monitoramento de preços no e-commerce: a página de categoria é dinâmica, mas a lista de produtos vem de uma única requisição
/api/products. A requisição de API em vez do navegador headless; para ver preços em países diferentes, um Proxies residenciais que sai por conexões domésticas reais. A configuração está na nossa página de solução de proxy para e-commerce. - Um site de classificados feito com Next.js: os dados estão em
__NEXT_DATA__. Um cliente HTTP e a análise do JSON bastam. - Uma lista de avaliações com rolagem infinita: encontra-se a requisição de paginação enviada ao rolar e percorre-se em loop com o parâmetro
cursor. - Um painel de conta que exige interação (a sua própria conta): login, escolha de filtros e download de relatórios com navegador headless; o mesmo IP durante a sessão. A configuração geral está na nossa página de solução de extração de dados.
Erros comuns
- Partir primeiro para o navegador headless. A requisição JSON no painel Network muitas vezes entrega os mesmos dados de forma muito mais barata.
- Esperar um tempo fixo.
time.sleep(5)desperdiça tempo em respostas rápidas e ainda dá resultado vazio nas lentas. - Esperar por
networkidle. Pode nunca chegar em páginas que continuam enviando requisições. - Baixar todos os recursos. Imagens, fontes e vídeos não são necessários para os dados; bloqueá-los economiza tempo e tráfego.
- Confundir resultado vazio com página dinâmica. Uma página de verificação ou um bloqueio também dá resultado vazio; confira o código de status e o conteúdo.
- Usar uma API interna como se fosse pública. Os termos e os limites de taxa do site também valem para essas requisições.
- Recorrer a ferramentas de evasão de detecção. Plugins que tentam fazer o navegador "parecer humano" escondem a origem do problema; revisar velocidade, escopo e permissão é um caminho mais sólido. Discutimos por que essas ferramentas são problemáticas em Como usar o undetected ChromeDriver para web scraping.
Guia de decisão
| Resultado do diagnóstico | Recomendação |
|---|---|
| O texto está no código-fonte da página | Cliente HTTP + análise de HTML |
O código-fonte tem __NEXT_DATA__ ou JSON-LD | Cliente HTTP + análise de JSON |
| O painel Network tem uma requisição JSON com os dados | Repetir a requisição diretamente |
| A requisição JSON precisa de assinatura de curta duração | Navegador headless + expect_response |
| O conteúdo só chega com interação | Navegador headless + espera com locators |
| O navegador headless está lento e pesado | Bloquear imagens, reduzir a concorrência |
| Sem conteúdo no HTML e código de status 403/429 | Não é dinâmica, é bloqueio; diagnostique |
Perguntas frequentes
Por que o requests não traz o conteúdo que vejo no navegador?
Porque o requests só busca o primeiro HTML que o servidor envia e não executa JavaScript. Se o conteúdo da página é carregado depois com JavaScript, esse HTML só contém o esqueleto. Procure no painel Network a requisição de onde vêm os dados, ou JSON embutido no HTML.
Qual é a diferença entre navegador headless e navegador normal?
O mecanismo é o mesmo; a diferença é que no modo headless nenhuma janela é desenhada na tela. O carregamento da página, a execução de JavaScript e as requisições de rede funcionam como em um navegador normal. Alguns sites conseguem distinguir navegadores headless por pequenas diferenças.
Playwright ou Selenium?
Os dois rodam páginas dinâmicas. O Playwright oferece em uma única API recursos que ajudam no scraping, como espera automática, captura de respostas de rede e bloqueio de requisições; o Selenium é mais antigo, multilinguagem e tem um ecossistema amplo. Continuar com o que o seu projeto atual já usa costuma ser o mais prático.
Usar uma requisição de API escondida é legal?
Uma requisição ser tecnicamente pública não torna o uso dela irrestrito. Considere os termos de uso do site, as regras do robots.txt e as leis de proteção de dados pessoais; se houver API oficial, prefira-a. Se tiver dúvida, busque orientação jurídica.
Como usar navegador headless com proxy?
No Playwright, o proxy é informado com o parâmetro proxy ao iniciar o navegador; endereço do servidor, usuário e senha são campos separados. No Puppeteer e no Selenium, ele é passado ao navegador como argumento de inicialização, e a autenticação é tratada separadamente.
Como obter dados de páginas com rolagem infinita?
Primeiro, encontre no painel Network a requisição de paginação enviada ao rolar. Ela geralmente traz um número de página ou um valor cursor e pode ser repetida em loop. Se a requisição não puder ser repetida, role a página aos poucos em um navegador headless e espere, a cada passo, a chegada de novos cards.
Em resumo
Em uma página estática, o conteúdo está pronto no HTML e uma requisição HTTP simples basta; em uma página dinâmica, o conteúdo chega depois por JavaScript e uma requisição HTTP devolve um esqueleto. Antes de partir para um navegador headless, veja o código-fonte, procure no painel Network a requisição JSON com os dados e confira se há dados embutidos no HTML. Quando o navegador for realmente necessário, use a espera com locators e expect_response em vez de esperas fixas, bloqueie imagens e mantenha a concorrência baixa. Tipos de proxy adequados ao seu trabalho de coleta de dados estão nos nossos serviços de proxy.




