Os sites modernos raramente entregam o HTML já pronto com os dados. O catálogo de produtos, o feed, os preços, as avaliações — quase tudo costuma ser carregado depois que a página já abriu, via JavaScript. Para um scraper, isso significa que uma simples requisição GET da página e o parsing posterior do HTML vão devolver um esqueleto vazio, sem dados.
Há duas formas radicalmente diferentes de resolver esse problema:
- Interceptar as requisições à API — localizar as requisições com as quais o próprio navegador obtém os dados e repeti-las diretamente, sem navegador.
- Emulação completa do navegador — iniciar um navegador real (ou headless), deixar que ele renderize o JS e operar sobre a página como um usuário: clicar, rolar, arrastar elementos.
A primeira abordagem é mais rápida e consome menos recursos; a segunda é mais universal e mais robusta diante de lógicas fora do comum. Na prática, é frequente combinar as duas.
Abordagem 1. Interceptar e emular as requisições à API
A ideia
Quando uma página «completa» os seus dados na hora, quase sempre dispara requisições HTTP em segundo plano (XHR/fetch) a uma API interna que devolve JSON. Se você localizar esse endpoint e reproduzir a requisição com os cabeçalhos, cookies e tokens corretos, poderá obter os dados diretamente, sem passar pela renderização. É dezenas de vezes mais rápido e não exige navegador.
Como localizar a API
- Abra o DevTools (
F12) → aba Network. - Filtre por Fetch/XHR.
- Role a página, clique em «Mostrar mais», troque de categoria — enfim, provoque o carregamento de dados.
- Localize a requisição em cuja resposta esteja o JSON que interessa (produtos, preços etc.).
- Examine-a: URL, método, parâmetros da requisição, cabeçalhos, corpo e cookies.
- Clique com o botão direito → Copy → Copy as cURL — um ótimo ponto de partida: dá para importá-lo no Postman ou traduzi-lo direto para código.
Tokens: CSRF, sessões e autenticação
A principal dificuldade dessa abordagem é que a requisição quase nunca vai «nua». O servidor espera um conjunto de dados de validação e, sem eles, devolve 401, 403 ou 419.
Token CSRF (Cross-Site Request Forgery). Proteção contra a falsificação de requisições entre sites. O servidor emite um token aleatório que o cliente precisa devolver nas requisições que alteram dados (e, às vezes, também nas de leitura). Onde costuma ficar:
- em
<meta name="csrf-token" content="...">, dentro do HTML da página; - em um campo oculto do formulário
<input type="hidden" name="_token" value="...">; - em um cookie (muitas vezes
XSRF-TOKEN) que depois precisa ser duplicado no cabeçalhoX-CSRF-TokenouX-XSRF-TOKEN.
O esquema é o seguinte: primeiro carrega-se a página normal, extraem-se o token e os cookies de sessão e depois se incorporam eles à requisição à API.
Cookies de sessão. No primeiro acesso, o servidor envia Set-Cookie (por exemplo, sessionid, PHPSESSID, laravel_session). É preciso preservá-los entre requisições; para isso usa-se um objeto de sessão (requests.Session, httpx.Client), que faz isso automaticamente.
Autenticação (Bearer / JWT / chave de API). Se os dados estão atrás de um login, o cabeçalho costuma levar Authorization: Bearer <token>. Os tokens JWT são obtidos pelo endpoint de login e depois anexados a cada requisição.
Outros campos de proteção. X-Requested-With: XMLHttpRequest (muitas vezes obrigatório nos endpoints AJAX), Referer, Origin e, às vezes, parâmetros assinados (signature, nonce, timestamp) gerados pelo JavaScript do frontend.
Quando essa abordagem falha. Se o token ou a assinatura da requisição forem gerados por um JavaScript ofuscado no próprio navegador (ou em WASM), reproduzi-lo do lado do servidor fica muito difícil. É o sinal de que convém passar para a segunda abordagem — a emulação do navegador, onde o JS se executa sozinho.
Exemplo: Python + requests (com CSRF e paginação)
import re
import requests
session = requests.Session()
BASE = "https://example-shop.com"
# 1. Carregamos a página para obter o token CSRF e os cookies de sessão
resp = session.get(f"{BASE}/catalog", headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
})
# O CSRF pode estar em uma tag meta...
m = re.search(r'name="csrf-token"\s+content="([^"]+)"', resp.text)
csrf = m.group(1) if m else session.cookies.get("XSRF-TOKEN")
headers = {
"X-CSRF-Token": csrf,
"X-Requested-With": "XMLHttpRequest",
"Referer": f"{BASE}/catalog",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept": "application/json",
}
# 2. Consultamos a API interna página a página
all_items = []
page = 1
while True:
r = session.get(
f"{BASE}/api/products",
params={"category": "phones", "page": page, "per_page": 48},
headers=headers,
)
r.raise_for_status()
payload = r.json()
items = payload.get("items", [])
if not items:
break
all_items.extend(items)
page += 1
for it in all_items:
print(it["title"], it["price"])Exemplo: Python + httpx (async, mais rápido com grandes volumes)
import asyncio
import httpx
async def fetch_page(client, page):
r = await client.get("/api/products", params={"page": page, "per_page": 48})
return r.json().get("items", [])
async def main():
async with httpx.AsyncClient(base_url="https://example-shop.com",
headers={"X-Requested-With": "XMLHttpRequest"}) as client:
tasks = [fetch_page(client, p) for p in range(1, 11)]
results = await asyncio.gather(*tasks)
items = [x for chunk in results for x in chunk]
print(len(items))
asyncio.run(main())Exemplo: Node.js + fetch
const csrf = "..."; // extraído do HTML/cookies de antemão
const res = await fetch("https://example-shop.com/api/products?page=1&per_page=48", {
headers: {
"X-CSRF-Token": csrf,
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json",
"Cookie": "sessionid=abc123; XSRF-TOKEN=" + csrf,
},
});
const data = await res.json();
data.items.forEach(item => console.log(item.title, item.price));Abordagem 2. Emulação completa do navegador
A ideia
Iniciamos um motor real (Chromium, Firefox, WebKit): ele baixa a página, executa todo o JS e renderiza o DOM. A partir daí, operamos sobre a página igual a uma pessoa: esperamos os elementos aparecerem, clicamos, rolamos, arrastamos controles deslizantes. Todos os tokens, as assinaturas e os scripts anti-bot se executam sozinhos — não precisamos reproduzi-los.
Desvantagens: é uma ordem de grandeza mais lento, voraz em CPU/RAM e mais fácil de ser detectado pelos sistemas anti-bot (embora isso se combata com «modos stealth» específicos).
Com o que emular
- Selenium — o padrão mais veterano; suporta Python, Java, C#, JavaScript e Ruby. Controla navegadores reais por meio do WebDriver.
- Playwright — um framework moderno da Microsoft. Python, JavaScript/TS, .NET, Java. Chromium, Firefox e WebKit «prontos para usar», autoespera inteligente de elementos e uma prática interceptação de requisições de rede.
- Puppeteer — Node.js; na origem, só Chromium (há suporte experimental para Firefox). Muito rápido e maduro para o Chrome.
Ações sobre a página
A seguir — as mesmas quatro ações (clique, rolagem, rolagem até um elemento e manter o botão do mouse pressionado + movimento) em diferentes tecnologias. Manter pressionado + mover é a base do drag-and-drop, dos controles deslizantes e dos captchas de slider.
Playwright (Python)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example-shop.com/catalog")
# CLIQUE
page.click("button.load-more")
# ROLAGEM com a roda do mouse
page.mouse.wheel(0, 2000)
# ROLAGEM até um elemento específico
page.locator("footer").scroll_into_view_if_needed()
# MANTER pressionado + MOVIMENTO (drag / control deslizante)
box = page.locator(".slider-handle").bounding_box()
start_x = box["x"] + box["width"] / 2
start_y = box["y"] + box["height"] / 2
page.mouse.move(start_x, start_y)
page.mouse.down() # botão pressionado
page.mouse.move(start_x + 200, start_y, steps=25) # deslocamento suave (25 passos)
page.mouse.up() # soltamos
browser.close()Playwright (JavaScript/Node)
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example-shop.com/catalog');
// CLIQUE
await page.click('button.load-more');
// ROLAGEM
await page.mouse.wheel(0, 2000);
// ROLAGEM até o elemento
await page.locator('footer').scrollIntoViewIfNeeded();
// MANTER PRESSIONADO + MOVIMENTO
const box = await page.locator('.slider-handle').boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 200, box.y, { steps: 25 });
await page.mouse.up();
await browser.close();
})();Selenium (Python) — com ActionChains
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example-shop.com/catalog")
wait = WebDriverWait(driver, 10)
# CLIQUE (esperando que fique clicável)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.load-more")))
btn.click()
# ROLAGEM
driver.execute_script("window.scrollBy(0, 2000)")
# ROLAGEM até o elemento
footer = driver.find_element(By.CSS_SELECTOR, "footer")
driver.execute_script("arguments[0].scrollIntoView({block:'center'})", footer)
# MANTER PRESSIONADO + MOVIMENTO
handle = driver.find_element(By.CSS_SELECTOR, ".slider-handle")
(ActionChains(driver)
.click_and_hold(handle) # botão pressionado
.move_by_offset(200, 0) # deslocamos 200 px para a direita
.pause(0.3)
.release() # soltamos
.perform())
driver.quit()Selenium (Java)
WebDriver driver = new ChromeDriver();
driver.get("https://example-shop.com/catalog");
// CLIQUE
driver.findElement(By.cssSelector("button.load-more")).click();
// ROLAGEM
((JavascriptExecutor) driver).executeScript("window.scrollBy(0, 2000)");
// MANTER PRESSIONADO + MOVIMENTO
WebElement handle = driver.findElement(By.cssSelector(".slider-handle"));
new Actions(driver)
.clickAndHold(handle)
.moveByOffset(200, 0)
.release()
.perform();Puppeteer (Node.js)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('https://example-shop.com/catalog');
// CLIQUE
await page.click('button.load-more');
// ROLAGEM
await page.evaluate(() => window.scrollBy(0, 2000));
// ROLAGEM até o elemento
await page.$eval('footer', el => el.scrollIntoView());
// MANTER PRESSIONADO + MOVIMENTO
const handle = await page.$('.slider-handle');
const box = await handle.boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 200, box.y, { steps: 25 });
await page.mouse.up();
await browser.close();
})();Modos de mascaramento (stealth): escapar da detecção da automação
Um navegador headless executado «como está» se distingue com facilidade de um real. Os sistemas anti-bot (Cloudflare, DataDome, PerimeterX/HUMAN, Akamai etc.) verificam dezenas de sinais e, se pelo menos parte deles denunciar a automação, chega o captcha, o challenge ou o bloqueio. O modo stealth é um conjunto de patches e técnicas que mascaram esses indícios.
Por quais indícios detectam
navigator.webdriver === true— a flag mais evidente, que o navegador controlado por WebDriver/CDP ativa automaticamente.- Artefatos de headless. Ausência de
window.chrome, lista de plugins vazia (navigator.plugins), valores atípicos denavigator.languages, um renderizador WebGL do tipoSwiftShader/Google Inc.em vez de uma placa de vídeo real. - Fingerprinting. Canvas, WebGL, AudioContext e o conjunto de fontes geram uma «impressão digital» estável do ambiente; em um headless padrão, ela fica suspeitamente genérica.
- Impressão digital TLS/JA3. No nível da própria conexão HTTP, o «aperto de mãos» de um cliente Python ou Node difere do do Chrome — e isso é detectado antes mesmo de o JS ser executado (também vale para a abordagem 1).
- Comportamento. Cliques instantâneos sem movimento do mouse, tempos perfeitamente regulares, saltar direto para uma página interna sem navegação prévia — nada disso é humano.
- Reputação do IP. As faixas de data centers (AWS, Hetzner etc.) estão marcadas; os endereços residenciais e móveis levantam menos suspeitas.
Ferramentas já disponíveis
puppeteer-extra+puppeteer-extra-plugin-stealth(Node) — o pacote mais conhecido; ocultanavigator.webdriver, corrige WebGL/plugins/languages e dezenas de outros «vazamentos».playwright-extracom o mesmo plugin stealth — o equivalente para o Playwright em Node.undetected-chromedriver(Python, sobre o Selenium) — um ChromeDriver com patches que supera muitas verificações do Cloudflare. Sua evolução é onodriver(sem o protocolo webdriver, puramente via CDP).SeleniumBaseno modo UC (--uc) — um invólucro sobre o Selenium com antidetecção integrada.rebrowser-patches— patches de baixo nível para o runtime do Puppeteer/Playwright que fecham vazamentos de CDP mais sutis.
Importante: nenhum plugin stealth oferece garantias. Os sistemas anti-bot se atualizam o tempo todo, e o que passava ontem pode ser detectado amanhã. É uma «corrida armamentista», não uma configuração de uma única vez.
Exemplos
Python — undetected-chromedriver:
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.add_argument("--lang=pt-BR")
# para a antidetecção normalmente NÃO se usa headless, ou se usa o modo novo:
# options.add_argument("--headless=new")
driver = uc.Chrome(options=options)
driver.get("https://example-shop.com/catalog")
print(driver.title)
driver.quit()Node — puppeteer-extra + stealth:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
// UA verossímil e cabeçalhos coerentes
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
);
await page.goto('https://example-shop.com/catalog');
await browser.close();
})();Node — playwright-extra + stealth:
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();
chromium.use(stealth);
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example-shop.com/catalog');
await browser.close();
})();Técnicas manuais (além dos plugins ou no lugar deles)
- Ocultar
webdrivere ajustar o ambiente. Via CDP/init scriptantes de a página carregar:
# Playwright (Python): executa em cada documento novo ANTES dos scripts do site
page.add_init_script(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
)- Novo modo headless. Nas versões recentes do Chrome, a flag
--headless=newfica mais perto de um navegador normal do que o headless antigo; às vezes sai melhor rodar direto no modo não headless sob uma tela virtual (Xvfb). - Perfil persistente. Iniciar com
user_data_dirconserva os cookies e o «aquecimento» da sessão entre execuções — fica menos parecido com um bot recém-criado. - Comportamento humano. Pausas aleatórias, movimento do mouse seguindo uma curva (Bézier), rolagem em pequenos trechos em vez de um único salto. Para gerar trajetórias existem bibliotecas como
pyautogui/bezierou o parâmetrostepsembutido nomouse.move. - Proxies. Os proxies residenciais e móveis com rotação reduzem bastante a proporção de challenges frente aos IPs de data centers.
Mascaramento (stealth) para a abordagem 1 (sem navegador)
A interceptação da API também é detectada — pela impressão digital TLS do cliente HTTP. Para que a requisição pareça, já no «aperto de mãos», um Chrome real, usam-se clientes que falseiam a impressão digital TLS:
# curl_cffi sabe imitar o TLS/JA3 de um navegador específico
from curl_cffi import requests
r = requests.get(
"https://example-shop.com/api/products?page=1",
impersonate="chrome124", # falseamos o aperto de mãos do Chrome 124
)
print(r.json())Alternativas: tls-client (Python/Go), curl-impersonate (um binário do sistema). Isso costuma resolver o problema quando um requests «nu» recebe um 403 enquanto no navegador essa mesma página abre.
O que cada biblioteca sabe fazer
A linha divisória principal: se a ferramenta executa JavaScript e se sabe imitar as ações do mouse. Os clientes HTTP e os parsers de HTML não fazem nenhuma das duas coisas — só servem para sites estáticos ou para a primeira abordagem (a emulação da API).
| Biblioteca | Linguagem | Renderização de JS | Ações (clique/scroll/drag) | Uso |
|---|---|---|---|---|
| requests / httpx | Python | Não | Não | Cliente HTTP |
| aiohttp | Python | Não | Não | HTTP assíncrono |
| BeautifulSoup / lxml | Python | Não | Não | Parsing de HTML |
| Scrapy | Python | Não (requer o plugin Splash/Playwright) | Não | Framework de crawling |
| Selenium | Python/Java/C#/JS/Ruby | Sim | Sim | Controle do navegador |
| Playwright | Python/JS/.NET/Java | Sim | Sim | Controle do navegador |
| Puppeteer | Node.js | Sim (Chromium) | Sim | Controle do navegador |
| Cypress | JS | Sim | Sim (mas voltado a testes e2e) | Testes |
| axios / fetch / got | Node.js | Não | Não | Cliente HTTP |
| cheerio | Node.js | Não | Não | Parsing de HTML (estilo jQuery) |
| Colly | Go | Não | Não | Framework de crawling |
| chromedp / rod | Go | Sim | Sim | Controle do navegador |
| HtmlUnit | Java | Parcial/instável | Limitadas | Navegador headless |
| jsoup | Java | Não | Não | Parsing de HTML |
Conclusão em resumo:
- Só precisa repetir uma requisição à API →
requests/httpx(Python),fetch/got(Node),Colly(Go). - Fazer o parsing de um HTML já obtido →
BeautifulSoup/lxml,cheerio,jsoup. - Precisa de renderização de JS e ações com o mouse →
Playwright,Selenium,Puppeteer,chromedp/rod. - HtmlUnit dá conta de um pouco de JS, mas tropeça com frequência nas SPAs modernas — para uma renderização séria, recorre-se ao Playwright/Selenium.
Caso prático: monitoramento de lojas online e rolagem infinita
É, provavelmente, a tarefa prática mais comum. Nos catálogos, os produtos raramente aparecem todos de uma vez: aplica-se lazy loading / rolagem infinita — as novas fichas são carregadas à medida que a página é rolada (ou ao clicar em «Mostrar mais»). Uma simples requisição do HTML só devolve a primeira «leva».
Há dois caminhos, e ambos são muito usados no monitoramento de preços e sortimento:
Caminho A (preferencial): interceptar a API de paginação
Ao rolar, a loja quase sempre chama algo parecido com /api/catalog?page=2&offset=48. Se for esse o caso — esqueça o navegador e colete os dados página a página de forma direta (veja a abordagem 1). É rápido, estável e escala para milhares de produtos. É assim que se constrói a maioria dos monitoramentos industriais: o navegador é usado uma única vez — para explorar a estrutura da API e os tokens, e a coleta em si é feita com um cliente HTTP.
Caminho B: renderização + rolagem quando a API está fechada
Se o endpoint estiver protegido por uma assinatura fora do trivial ou os dados forem gerados exclusivamente no cliente, não resta senão rolar com o navegador e recolher as fichas do DOM. O algoritmo: rolamos para baixo → esperamos o carregamento → contamos as fichas → repetimos enquanto a quantidade crescer.
from playwright.sync_api import sync_playwright
def scrape_catalog(url):
products = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url)
prev_count = -1
stable_rounds = 0
while stable_rounds < 2: # 2 rolagens «vazias» seguidas = chegamos ao fim
# rolamos até o fundo
page.mouse.wheel(0, 4000)
page.wait_for_timeout(1500) # damos tempo para carregar
cards = page.locator(".product-card")
count = cards.count()
if count == prev_count:
stable_rounds += 1
else:
stable_rounds = 0
prev_count = count
# recolhemos todas as fichas depois da rolagem completa
cards = page.locator(".product-card")
for i in range(cards.count()):
card = cards.nth(i)
products.append({
"title": card.locator(".title").inner_text(),
"price": card.locator(".price").inner_text(),
"url": card.locator("a").get_attribute("href"),
})
browser.close()
return products
data = scrape_catalog("https://example-shop.com/catalog/phones")
print(f"Produtos coletados: {len(data)}")A mesma técnica para o botão «Mostrar mais» — clicamos enquanto o botão existir:
while page.locator("button.load-more").count() > 0:
page.click("button.load-more")
page.wait_for_timeout(1200)Híbrido: o melhor dos dois mundos
Playwright e Puppeteer sabem escutar o tráfego de rede. Dá para abrir a página no navegador (para que passem todos os tokens e as verificações anti-bot), mas obter os dados não do DOM, e sim das respostas dessa mesma API que o navegador chama ao rolar:
def handle_response(response):
if "/api/products" in response.url:
data = response.json()
# guardamos o JSON já pronto: não é preciso fazer o parsing do HTML
save(data["items"])
page.on("response", handle_response)
page.goto("https://example-shop.com/catalog")
# depois é só rolar: os dados «chegam» sozinhos ao manipuladorCostuma ser a opção ideal para o monitoramento: a robustez da emulação de navegador + um JSON limpo e estruturado em vez da frágil análise do layout.
Dicas práticas para o monitoramento
- Espere pelos dados, não pelo relógio. Em vez de «dormir 1,5 segundo», use a espera pelo surgimento de um elemento (
wait_for_selector) ou pela rede entrar em silêncio (wait_for_load_state("networkidle")) — é mais confiável e, muitas vezes, mais rápido. - Deduplicação. Com a rolagem infinita, algumas fichas podem ser lidas em duplicidade — colete por um
id/urlúnico. - Limite a frequência. Requisições agressivas demais sobrecarregam o site e provocam um bloqueio rápido. Adicione atrasos e use os pools de proxies com cuidado e dentro da lei.
- Coloque a exploração prévia em cache. Descubra a estrutura da API e os tokens uma única vez; em produção, rode uma coleta HTTP leve e mantenha o navegador pesado como reserva.
Como escolher a abordagem
| Critério | Interceptar a API | Emulação de navegador |
|---|---|---|
| Velocidade | Muito alta | Baixa |
| Consumo de recursos | Mínimo | Alto (CPU/RAM) |
| Complexidade de configuração | Maior (engenharia reversa de tokens) | Menor (tudo «como um usuário») |
| Robustez diante de mudanças de layout | Alta (depende da API) | Média (depende dos seletores) |
| Superar tokens/assinaturas do cliente | Difícil | Automático |
| Escala por volume | Excelente | Limitada |
Regra prática: verifique sempre primeiro se basta interceptar a API — é mais rápido, mais barato e mais estável. Passe para a emulação de navegador só quando a API estiver oculta atrás de criptografia do lado do cliente, protegida por uma lógica anti-bot complexa, ou quando for preciso reproduzir uma interação fora do trivial (drag-and-drop, controles deslizantes, formulários em etapas).