scrape dynamic web pages

Aprender a fazer scraping de páginas web dinâmicas significa lidar com conteúdo que não existe no HTML que a sua primeira requisição baixa. Os sites modernos constroem as grades de produtos, os preços, os comentários e os feeds no navegador usando JavaScript, então uma simples requisição HTTP retorna uma casca vazia onde os dados deveriam estar.

Este guia percorre o fluxo de trabalho completo em Python. Começa por detectar conteúdo dinâmico comparando a resposta bruta com o DOM renderizado e depois ordena as opções de extração da mais leve à mais pesada: encontrar a API JSON oculta, controlar Playwright ou Selenium, lidar com scroll infinito e botões de carregar mais, reduzir a largura de banda do proxy, adicionar retentativas, fazer scraping de forma concorrente e exportar JSON e CSV limpos.

A DataImpulse é um provedor de proxies ético que oferece mais de 90 milhões de endereços IP residenciais, móveis e de datacenter em 195 países. Usa um modelo de pagamento conforme o uso a partir de 1 dólar por GB com tráfego que não expira, e é usada para web scraping, verificação de anúncios, monitoramento de preços, pesquisa de mercado e gestão de múltiplas contas.

Fatos principais

  • Scraping dinâmico: o conteúdo construído por JavaScript não está no HTML bruto, então você chama a API JSON subjacente que a página consulta ou renderiza a página em um navegador headless.
  • Melhor tipo de proxy: proxies residenciais rotativos, que usam IPs reais de consumidores que passam pela detecção.
  • Preço: a partir de 1 dólar por GB, pagamento conforme o uso, com tráfego que não expira e sem assinatura.
  • Cobertura: mais de 90M de IPs de origem ética em 195 países.
  • Confiabilidade: 99,51% de taxa de sucesso, avaliada em 4,8 de 5 no G2.
  • Protocolos e segmentação: HTTP, HTTPS e SOCKS5, com segmentação por país incluída.
Prioridade de métodos para fazer scraping de páginas web dinâmicas

O que torna uma página web dinâmica?

Uma página é dinâmica quando o conteúdo relevante é gerado por JavaScript no navegador em vez de ser entregue na resposta HTML inicial. O servidor envia um esqueleto leve e, em seguida, o código do lado do cliente busca os dados e os injeta no DOM depois que a página abre.

A verificação manual mais rápida é comparar duas visões da mesma página. Clique com o botão direito e escolha Ver código-fonte para ver o HTML bruto que o servidor retornou; depois abra as ferramentas de desenvolvedor e inspecione o painel Elements, que mostra o DOM ao vivo depois que os scripts rodaram. Se o preço ou a listagem que você quer aparece no painel Elements mas está ausente em Ver código-fonte, o conteúdo é dinâmico e uma única requisição não o capturará.

  • O conteúdo estático está presente em Ver código-fonte e pode ser parseado diretamente de uma resposta HTTP.
  • O conteúdo dinâmico aparece apenas no DOM renderizado e é carregado por meio de requisições em segundo plano.

Você pode automatizar esse diagnóstico em código. Primeiro, conte os elementos-alvo no HTML bruto que um cliente HTTP normal baixa:

import requests
from bs4 import BeautifulSoup

url = "https://example.com/products"
headers = {"User-Agent": "Mozilla/5.0"}

resp = requests.get(url, headers=headers, timeout=30)
soup = BeautifulSoup(resp.text, "html.parser")

raw_cards = soup.select("div.product-card")
print("Cards in raw HTML:", len(raw_cards))

# If this prints 0 but the page clearly shows products in a browser,
# the content is injected by JavaScript and the page is dynamic.

Depois, renderize a mesma URL em um navegador headless e conte de novo. A diferença entre os dois números é a prova:

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.com/products", wait_until="networkidle")
    rendered_cards = page.query_selector_all("div.product-card")
    print("Cards after JavaScript runs:", len(rendered_cards))
    browser.close()

# Raw HTML 0, rendered DOM 48 means the page builds its content dynamically.

Antes de escrever um scraper completo, também vale confirmar se o alvo permite acesso automatizado. Nosso guia sobre como verificar se um site permite scraping aborda a leitura das regras de robots e dos termos de serviço.

Qual método você deve usar para fazer scraping de páginas dinâmicas?

Escolha o método mais leve que retorne os dados de forma confiável: chame uma API JSON oculta quando existir uma e recorra a um navegador headless somente quando não houver. Cada opção equilibra velocidade e custo em relação a quanto da página consegue reproduzir.

A tabela abaixo compara as quatro abordagens comuns para que você possa escolher antes de escrever código. Na prática, a maioria dos projetos combina duas delas, usando uma API oculta para a carga em massa e um navegador para as páginas que resistem.

Método Velocidade Custo Melhor para Limitação principal
API JSON oculta A mais rápida O mais baixo Dados por trás de uma chamada XHR limpa O endpoint pode mudar ou exigir tokens
Playwright Moderada Mais alto Renderização completa, controle assíncrono moderno Usa CPU, memória e largura de banda
Selenium Moderada Mais alto Stacks legados, amplo suporte a linguagens Configuração mais lenta, esperas mais verbosas
requests-html Lenta Baixo JavaScript leve em páginas simples Praticamente sem manutenção, renderização frágil

O custo importa porque um navegador headless roteado por um proxy medido baixa muito mais bytes do que uma única chamada de API. Se você faz scraping em escala, o método que escolher muda a conta tanto quanto o código. Para a disciplina mais ampla de ritmo e cabeçalhos, veja nossas melhores práticas de web scraping.

Como encontrar a API oculta por trás de uma página dinâmica?

Procure a requisição em segundo plano que a página faz para buscar seus dados, porque esse endpoint normalmente retorna JSON limpo que você pode chamar diretamente. É a abordagem mais rápida e confiável, e evita completamente executar um navegador.

Abra as ferramentas de desenvolvedor, vá para a aba Network, filtre por Fetch/XHR e recarregue a página. Conforme o conteúdo aparece, observe as requisições que retornam JSON com os valores que você quer. Inspecione a URL da requisição, o método, os parâmetros de consulta e quaisquer cabeçalhos ou tokens que ela envia; depois clique com o botão direito e copie como cURL para ver o formato completo. Assim que puder reproduzir essa chamada com um cliente HTTP, você pula a renderização por completo e obtém dados estruturados por uma fração do custo.

Aqui está um exemplo mínimo que chama um endpoint JSON através de um proxy e lê os campos diretamente:

import requests

proxy = "http://USERNAME:[email protected]:823"
proxies = {"http": proxy, "https": proxy}

headers = {
    "User-Agent": "Mozilla/5.0",
    "Accept": "application/json",
    "Referer": "https://example.com/products",
}
params = {"category": "laptops", "page": 1}

r = requests.get(
    "https://example.com/api/v2/products",
    headers=headers,
    params=params,
    proxies=proxies,
    timeout=30,
)
r.raise_for_status()
data = r.json()

for item in data["items"]:
    print(item["name"], item["price"])

Se o endpoint for paginado, incremente o parâmetro de página ou siga o próximo cursor que a resposta retorna. Quando uma requisição precisa de um token, capture onde a página o emite pela primeira vez e reproduza-o dentro da mesma sessão. Rotear a chamada pela DataImpulse faz o endpoint ver um IP residencial em vez do endereço do seu servidor, o que importa quando a API limita a taxa por origem. Se você esbarrar em uma barreira de autenticação no próprio proxy, nossa nota sobre autenticação de proxy explica o formato das credenciais.

Como fazer scraping de páginas dinâmicas com Playwright?

Quando não há uma API limpa para chamar, controle um navegador headless como o Playwright para que o JavaScript execute exatamente como executaria para um usuário e, então, leia o DOM finalizado. O Playwright é o padrão moderno porque traz espera automática confiável, suporte assíncrono de primeira classe e uma opção de proxy simples.

Primeiro instale-o e baixe um binário do navegador:

pip install playwright
playwright install chromium

A disciplina central é esperar pelo elemento específico de que você precisa em vez de adivinhar com sleeps fixos. O exemplo abaixo inicia o Chromium, roteia-o através de um proxy usando a forma de dicionário, espera pela grade de produtos e extrai os valores renderizados:

from playwright.sync_api import sync_playwright

PROXY = {
    "server": "http://gw.dataimpulse.com:823",
    "username": "USERNAME",
    "password": "PASSWORD",
}

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True, proxy=PROXY)
    context = browser.new_context(user_agent="Mozilla/5.0")
    page = context.new_page()
    page.goto("https://example.com/products", wait_until="domcontentloaded")

    page.wait_for_selector("div.product-card", timeout=15000)

    cards = page.query_selector_all("div.product-card")
    for card in cards:
        name = card.query_selector(".title").inner_text()
        price = card.query_selector(".price").inner_text()
        print(name, price)

    browser.close()

A espera é onde a maioria dos scrapers dinâmicos tem sucesso ou falha, então ajuda conhecer as quatro estratégias e quando cada uma se encaixa. Prefira esperar por um seletor ou condição concreta em vez de networkidle, que pode travar em páginas que mantêm conexões de longa duração abertas:

# Wait for a specific element (most reliable)
page.wait_for_selector("div.product-card", timeout=15000)

# Wait for the initial DOM to be built, before every script finishes
page.goto(url, wait_until="domcontentloaded")

# Wait until there are no network connections for 500 ms
page.goto(url, wait_until="networkidle")

# Wait for a custom condition, such as a minimum item count
page.wait_for_function(
    "document.querySelectorAll('div.product-card').length > 20"
)

Se o seu alvo depende fortemente de scripts executados no navegador, nosso artigo complementar sobre web scraping com JavaScript aprofunda a renderização do lado do cliente que torna essas páginas difíceis.

Como lidar com scroll infinito e botões de carregar mais?

Dispare os mesmos eventos que a página usa para carregar mais conteúdo e depois espere cada novo lote renderizar antes de lê-lo. O scroll infinito e os botões de carregar mais só buscam dados conforme o usuário age, então um único carregamento de página captura apenas a primeira tela.

Antes de recorrer ao navegador, verifique novamente a aba Network, porque o scroll infinito quase sempre é alimentado por uma chamada XHR paginada, e iterar sobre esse endpoint com a abordagem de requests acima é muito mais barato do que rolar. Se você precisar rolar, faça-o em um laço controlado que pare assim que nenhum item novo aparecer:

def scroll_until_stable(page, item_selector, max_rounds=30):
    seen = 0
    for _ in range(max_rounds):
        page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        page.wait_for_timeout(1500)
        count = len(page.query_selector_all(item_selector))
        if count == seen:
            break            # no new items loaded, stop
        seen = count
    return seen

Páginas que usam um botão explícito em vez de scroll precisam de um laço de cliques. Leia a contagem de itens antes de cada clique, clique no botão e espere a contagem crescer para nunca ficar à frente da renderização:

def click_load_more(page, button_selector, item_selector, max_clicks=50):
    for _ in range(max_clicks):
        button = page.query_selector(button_selector)
        if button is None or not button.is_visible():
            break
        before = len(page.query_selector_all(item_selector))
        button.click()
        page.wait_for_function(
            "([sel, n]) => document.querySelectorAll(sel).length > n",
            arg=[item_selector, before],
        )
    return len(page.query_selector_all(item_selector))

Remova duplicatas conforme coleta, já que reler os mesmos nós a cada passagem é comum. Mantenha um conjunto de IDs ou URLs únicos e adicione apenas os registros que você ainda não viu.

Você pode usar Selenium em vez de Playwright?

Sim, o Selenium controla um navegador real da mesma forma e é uma opção sólida quando sua equipe já o usa ou precisa do seu amplo suporte a linguagens. O padrão espelha o do Playwright: carregue a página, espere por uma condição com WebDriverWait e depois leia os elementos do DOM renderizado.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = Options()
options.add_argument("--headless=new")
options.add_argument("--proxy-server=http://gw.dataimpulse.com:823")

driver = webdriver.Chrome(options=options)
driver.get("https://example.com/products")

WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "div.product-card"))
)

for card in driver.find_elements(By.CSS_SELECTOR, "div.product-card"):
    name = card.find_element(By.CSS_SELECTOR, ".title").text
    price = card.find_element(By.CSS_SELECTOR, ".price").text
    print(name, price)

driver.quit()

Uma ressalva: o simples flag –proxy-server não aceita usuário e senha, então um proxy autenticado precisa de um auxiliar. A biblioteca Selenium Wire injeta as credenciais de forma limpa, e nosso passo a passo sobre a configuração do proxy com Selenium Wire mostra a configuração exata. O Selenium é mais pesado de configurar que o Playwright e suas esperas são mais verbosas, então projetos novos costumam começar com Playwright, a menos que haja um motivo para ficar no Selenium.

Como reduzir a largura de banda do proxy ao renderizar páginas?

Bloqueie os recursos de que você não precisa para que o navegador pare de baixar imagens, fontes e mídia que você nunca faz parse. Um navegador headless busca todos os recursos por padrão, e cada um desses bytes custa dinheiro quando roteado por um proxy cobrado por gigabyte.

O Playwright permite interceptar requisições e abortar as que são irrelevantes para a sua extração. Bloquear imagens, mídia e fontes pode reduzir drasticamente o tráfego do proxy sem mexer no JSON e no HTML que você realmente lê:

BLOCK = {"image", "media", "font"}

def block_heavy(route):
    if route.request.resource_type in BLOCK:
        route.abort()
    else:
        route.continue_()

context = browser.new_context()
context.route("**/*", block_heavy)
page = context.new_page()
page.goto("https://example.com/products", wait_until="domcontentloaded")

Você pode estender o mesmo manipulador para pular os domínios de analytics e de anúncios que adicionam carga sem adicionar dados. Com o tráfego da DataImpulse cobrado conforme o uso a partir de 1 dólar por GB e sem expirar, cortar bytes desperdiçados reduz diretamente quanto custa cada scrape. Reutilizar um mesmo contexto de navegador entre páginas relacionadas também evita o custo repetido de inicialização, e a requisição mais barata continua sendo aquela que você evita ao recorrer a uma API oculta.

Como adicionar tratamento de erros e retentativas?

Envolva cada navegação em um laço de retentativas com backoff exponencial para que um único carregamento lento ou bloqueio transitório não arruíne toda a execução. Páginas dinâmicas falham de forma intermitente, e um scraper que desiste no primeiro timeout perderá uma grande parcela dos registros em escala.

Capture o timeout que o Playwright lança, espere um intervalo crescente e tente novamente um número limitado de vezes antes de registrar a falha e seguir em frente:

import time
from playwright.sync_api import TimeoutError as PlaywrightTimeout

def fetch_with_retries(page, url, selector, retries=3, backoff=2):
    for attempt in range(1, retries + 1):
        try:
            page.goto(url, wait_until="domcontentloaded", timeout=30000)
            page.wait_for_selector(selector, timeout=15000)
            return page.query_selector_all(selector)
        except PlaywrightTimeout:
            wait = backoff ** attempt
            print(f"Attempt {attempt} timed out, retrying in {wait}s")
            time.sleep(wait)
    raise RuntimeError(f"Failed to load {url} after {retries} attempts")

Combine as retentativas com a rotação de proxies para que cada tentativa saia por uma IP nova. Se uma saída está com limite de taxa, a próxima requisição chega a um endereço diferente e muitas vezes tem sucesso. Um pool rotativo de proxies residenciais ou proxies móveis atribui uma IP nova por requisição automaticamente, o que transforma muitos bloqueios difíceis em uma simples retentativa. Para lidar especificamente com falhas a nível de proxy, nossa nota sobre o erro HTTP 407 aborda a resposta de autenticação mais comum.

Como fazer scraping de muitas páginas dinâmicas de forma concorrente?

Use a API assíncrona do Playwright com um semáforo do asyncio para que várias páginas rendam ao mesmo tempo sem sobrecarregar sua máquina ou o alvo. A concorrência é onde o scraping dinâmico recupera sua vazão, já que cada página do navegador passa a maior parte do tempo esperando pela rede.

O semáforo limita quantas páginas rodam em paralelo. Compartilhe um mesmo navegador e contexto entre as tarefas, abra uma página por URL e reúna os resultados:

import asyncio
from playwright.async_api import async_playwright

PROXY = {
    "server": "http://gw.dataimpulse.com:823",
    "username": "USERNAME",
    "password": "PASSWORD",
}

async def scrape_one(context, url, sem):
    async with sem:
        page = await context.new_page()
        try:
            await page.goto(url, wait_until="domcontentloaded")
            await page.wait_for_selector("div.product-card", timeout=15000)
            cards = await page.query_selector_all("div.product-card")
            return [await c.inner_text() for c in cards]
        finally:
            await page.close()

async def main(urls):
    sem = asyncio.Semaphore(5)   # at most 5 pages at once
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True, proxy=PROXY)
        context = await browser.new_context()
        tasks = [scrape_one(context, u, sem) for u in urls]
        results = await asyncio.gather(*tasks)
        await browser.close()
    return results

urls = ["https://example.com/products?page=%d" % i for i in range(1, 21)]
data = asyncio.run(main(urls))

Mantenha o limite de concorrência modesto. Páginas paralelas em excesso esgotam a memória e disparam os limites de taxa, o que custa mais retentativas do que o paralelismo economiza. Comece em torno de cinco e aumente apenas enquanto as taxas de sucesso se mantiverem altas.

Como exportar os dados extraídos para JSON e CSV?

Reúna os resultados de cada página em uma lista de dicionários e depois grave essa lista em JSON para dados aninhados ou em CSV para tabelas planas e amigáveis a planilhas. Manter a extração e a exportação separadas torna o scraper mais fácil de testar e reexecutar.

import json
import csv

records = [
    {"name": "Laptop A", "price": "999"},
    {"name": "Laptop B", "price": "1299"},
]

# Export to JSON
with open("products.json", "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)

# Export to CSV
with open("products.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "price"])
    writer.writeheader()
    writer.writerows(records)

Escolha JSON quando os registros contêm listas ou objetos aninhados, e CSV quando cada registro compartilha os mesmos campos planos e os analistas vão abri-lo em uma planilha. Normalize seus campos antes de gravar para que cada registro tenha as mesmas chaves, e o passo de exportação permanece trivial não importa como a página dinâmica foi renderizada. Daqui, o mesmo JSON alimenta uma carga em banco de dados ou um trabalho de analytics sem parsing adicional.

Estratégias de espera para conteúdo que carrega tarde

Perguntas frequentes

Como saber se uma página é dinâmica antes de escrever um scraper?

Compare Ver código-fonte com o painel Elements nas ferramentas de desenvolvedor, ou conte um elemento-alvo no HTML bruto versus o DOM renderizado. Se os dados aparecem apenas depois que o JavaScript roda, a página é dinâmica e precisa de uma chamada de API ou de um navegador headless.

É melhor chamar a API oculta ou usar um navegador headless?

Chame a API oculta quando conseguir encontrá-la, porque ela retorna JSON limpo por uma fração do custo e com mais velocidade que a renderização. Use um navegador headless apenas quando não existir uma API reproduzível ou os dados dependerem de scripts complexos do lado do cliente.

Devo usar Playwright ou Selenium para scraping dinâmico?

O Playwright é o padrão moderno graças à espera automática confiável e ao suporte assíncrono integrado, então projetos novos costumam começar por ele. O Selenium se encaixa bem quando seu stack já o usa ou você precisa do seu suporte a mais linguagens.

Como reduzo a largura de banda do proxy ao usar um navegador headless?

Intercepte as requisições e aborte imagens, fontes, mídia e analytics que você não faz parse. Isso pode reduzir o tráfego significativamente, já que o custo do proxy é medido por gigabyte, e também acelera cada carregamento de página.

Como faço scraping de conteúdo que carrega em scroll infinito?

Primeiro verifique na aba Network a chamada XHR paginada por trás do scroll e itere sobre esse endpoint diretamente. Se você precisar rolar, faça-o em um laço controlado e espere cada novo lote renderizar antes de lê-lo.

Quando a DataImpulse não é a escolha certa?

Se você precisa de proxies ISP estáticos, de uma API de scraping totalmente gerenciada ou de acesso a sites bancários e governamentais, a DataImpulse não é a ferramenta certa. Ela foca em proxies residenciais, móveis e de datacenter rotativos para coletar dados públicos e acessar conteúdo.

Faça scraping de páginas dinâmicas com proxies confiáveis

Se o seu scraper precisa de IPs residenciais, móveis ou de datacenter que se comportam como visitantes reais, você pode começar com tráfego de pagamento conforme o uso a partir de 1 dólar por GB. Crie uma conta na DataImpulse e roteie seu scraping dinâmico por sessões rotativas ou sticky.


Share article: