Voltar ao blogue
Guias
Andrei OgiolanLast updated on May 7, 202613 min read

Web Scraping de Tabelas JavaScript em Python: De APIs ocultas a Playwright

Web Scraping de Tabelas JavaScript em Python: De APIs ocultas a Playwright
Resumo: A extração de tabelas JavaScript na Web em Python raramente requer um navegador sem interface gráfica. Abra o DevTools, localize o endpoint JSON que preenche a tabela, reproduza-o com requests, pagine-o e recorra ao Playwright apenas quando a chamada de rede for assinada, encriptada ou de outra forma bloqueada.

Escreveu o código óbvio. requests.get(url), passa o HTML para o BeautifulSoup, extrai as linhas do <table>. O script é executado, o ficheiro é guardado no disco e o CSV está vazio. Bem-vindo à extração de tabelas JavaScript da Web, onde as linhas que vê no seu navegador não existem no documento que o servidor realmente devolveu.

As tabelas estáticas enviam os dados dentro do HTML inicial. As tabelas dinâmicas (também chamadas de tabelas AJAX ou renderizadas por JavaScript) enviam uma estrutura quase vazia; em seguida, um script na página chama um endpoint JSON e injeta linhas no DOM após o carregamento. Se não executar esse script, não verá essas linhas. Iniciar um navegador completo para resolver isto é uma resposta pesada para o que geralmente é um problema pequeno.

Este guia segue o caminho mais curto. Começaremos com uma árvore de decisão para que deixe de adivinhar se deve recorrer a requests ou um motor de navegador, e depois vamos percorrer o processo de encontrar o ponto de extremidade JSON subjacente no DevTools, reproduzi-lo em Python com paginação e autenticação, analisá-lo em linhas limpas e exportá-lo para CSV, JSON Lines ou SQLite. O Playwright está aqui como um verdadeiro recurso de emergência para sites que ocultam a chamada de rede, não como a ferramenta padrão. No final, terá um script que poderá executar novamente no próximo trimestre sem precisar de o reescrever do zero.

Por que as tabelas JavaScript prejudicam os scrapers padrão

Quando chamas requests.get() numa página com uma tabela JavaScript, o que é devolvido é o documento que o servidor enviou antes de qualquer código do navegador ser executado. Esse documento contém o layout, a navegação, o contentor de grelha vazio e um conjunto de JavaScript. As linhas ainda não estão lá. O navegador executa o script, o script obtém uma carga JSON e só então a tabela é preenchida.

O BeautifulSoup analisa fielmente o que lhe foi fornecido, que é um <table> sem <tr> filhos. O seu seletor não corresponde a nada, o seu loop é executado zero vezes e o gravador produz um CSV com cabeçalhos e sem dados. A extração de tabelas JavaScript falha aqui, silenciosamente, porque todas as camadas funcionaram tecnicamente.

Escolha um caminho de extração antes de escrever código

Antes de abrir um editor, execute uma escada de decisão de um minuto. A classificação é importante porque cada passo custa mais para manter do que o que está acima dele.

  1. API oficial ou exportação CSV. Muitos painéis apresentam um botão de download ou um endpoint documentado. Use-o. Não vai extrair o que pode simplesmente solicitar com uma chave.
  2. XHR oculto ou JSON Fetch. A maioria das grelhas modernas é alimentada por uma chamada JSON que pode ver no DevTools. Esta deve ser a sua opção padrão para a extração de dados de tabelas JavaScript. A carga útil é estruturada, o esquema é estável e salta toda a camada de renderização.
  3. Estático <table> já na fonte. Se as linhas estiverem presentes em view-source: (sem necessidade de script), analise o HTML com pandas.read_html() para um resultado rápido ou requests além do BeautifulSoup com lxml para produção.
  4. Renderização com navegador headless. Recorra ao Playwright apenas quando o caminho de rede for assinado, GraphQL com verificações rigorosas de origem, alimentado por WebSocket ou de outra forma inacessível a partir de um cliente HTTP simples.

A maioria dos artigos ensina primeiro a opção 4. Isso é errado. Um endpoint JSON oculto, quando existe, oferece dados mais limpos e uma área de falha menor do que qualquer navegador headless jamais oferecerá.

Localize o endpoint JSON oculto com o DevTools

A forma mais rápida de confirmar se uma tabela é preenchida por JavaScript é verificar o código-fonte bruto da página, não o DOM renderizado. Clique com o botão direito do rato na página, escolha «Ver código-fonte» e procure um valor de amostra visível na tabela (um nome, um salário, um ID único). Se a pesquisa não devolver nada, a linha foi inserida após o carregamento e está a ver uma grelha renderizada por JavaScript.

Agora, encontre o pedido que entregou os dados. O exemplo de referência utilizado ao longo deste guia é a demonstração pública do DataTables AJAX em datatables.net/examples/data_sources/ajax.html. Abra o DevTools, mude para o separador Rede e filtre por Fetch/XHR. Recarregue a página para capturar todo o tráfego e, em seguida, acione uma alteração de ordenação ou paginação. Essa segunda ação é o truque: a maior carga útil após uma alteração de ordenação é quase sempre aquela que transporta as linhas.

Clique na chamada, abra a Resposta e confirme o formato JSON que esperava. Verifique os Cabeçalhos quanto ao método de solicitação, parâmetros de consulta, cookies e quaisquer tokens personalizados (X-CSRF-Token, Authorization). Para alvos complexos, clique com o botão direito na solicitação e selecione «Copiar como cURL». Isso preserva os cabeçalhos, os cookies e o corpo exato, para que possa colá-lo num conversor e inicializar o seu código Python sem precisar de digitar nada manualmente. Filtre agressivamente: uma única caixa de pesquisa digitada pode disparar dez solicitações de autocompletar antes da solicitação real.

Reproduza a solicitação capturada em Python

Assim que tiver a URL e os cabeçalhos, a parte do Python é pequena. Comece com o mínimo absoluto e adicione cabeçalhos apenas quando o servidor reclamar.

import requests

URL = "https://datatables.net/examples/ajax/data/objects.txt"

headers = {
    "User-Agent": "Mozilla/5.0 (compatible; tables-scraper/1.0)",
    "Accept": "application/json, text/javascript, */*; q=0.01",
}

response = requests.get(URL, headers=headers, timeout=15)
response.raise_for_status()
payload = response.json()

Há duas coisas a destacar. Primeiro, raise_for_status() é imprescindível porque os sistemas anti-bot frequentemente devolvem HTML com HTTP 200, e uma verificação de estado em falta transforma um bloqueio suave em dados corrompidos. Segundo, resista à tentação de colar o seu cookie de sessão pessoal do DevTools. Esse cookie expira, vaza contexto pessoal para o seu repositório e vincula o script a um único utilizador. Prefira cabeçalhos públicos e, em seguida, adicione um fluxo de login real com um requests.Session se o endpoint realmente precisar de autenticação.

Para fluxos de trabalho em que necessite de fan-out assíncrono em muitos endpoints, o HTTPX é uma alternativa imediata com uma API síncrona quase idêntica e suporte assíncrono de primeira classe. Considere isso como uma opção em vez de uma recomendação rígida; requests continua a ser um padrão perfeitamente aceitável em 2026.

Analise a carga JSON em linhas organizadas

O exemplo do DataTables retorna um dicionário de nível superior com uma data chave que contém uma lista de listas. As APIs reais variam: algumas devolvem uma lista de objetos, outras agrupam as linhas em results ou items, outras enterram-nas a dois níveis de profundidade sob payload.table.rows. Analise a estrutura uma vez e, em seguida, escreva código defensivo.

rows = payload.get("data", [])
records = []
for r in rows:
    records.append({
        "name":       r[0],
        "position":   r[1],
        "office":     r[2],
        "extn":       r[3],
        "start_date": r[4],
        "salary":     r[5],
    })

Se o endpoint devolver uma lista de objetos em vez de matrizes posicionais, troque os índices por r.get("name"), r.get("position"), e assim por diante. Usar .get() em vez de r["name"] poupa-lhe um KeyError no dia em que o backend adicionar ou renomear um campo. Faça este mapeamento uma vez, num único local, para que o resto do pipeline comunique com um esquema interno estável em vez de com o que quer que a API a montante tenha decidido enviar esta semana.

Lide com paginação, parâmetros de consulta e autenticação

Os endpoints reais raramente fornecem todas as linhas numa única chamada. O protocolo do lado do servidor do DataTables utiliza draw, start, length, order[0][column], e search[value]; a lista canónica de parâmetros encontra-se no manual de processamento do lado do servidor do DataTables. Outros backends utilizam paginação por cursor (?cursor=eyJ...), paginação por deslocamento (?page=3&per_page=100) ou um next_url campo incorporado na resposta.

import time

session = requests.Session()
session.headers.update(headers)

start, length, rows = 0, 100, []
while True:
    r = session.get(URL, params={"draw": 1, "start": start, "length": length}, timeout=15)
    if r.status_code == 429:
        time.sleep(2 ** (start // length))  # crude exponential backoff
        continue
    r.raise_for_status()
    page = r.json().get("data", [])
    if not page:
        break
    rows.extend(page)
    start += length

Se o endpoint estiver protegido por um login, efetue primeiro o login com session.post() e deixe que o cookie gerencie a sessão. Para POSTs protegidos contra CSRF, extraia o token de um campo oculto ou de um XSRF-TOKEN cookie e reenvie-o como um cabeçalho. Nunca cole uma string de cookie estática. Ela expira durante a noite e interrompe todas as execuções do cron a partir daí.

Exporte linhas para CSV, JSON Lines ou SQLite

Escolha o formato de saída que as suas ferramentas a jusante realmente utilizam. O CSV é adequado para folhas de cálculo, o JSON Lines é mais adequado para ingestão em streaming e pipelines LLM ou RAG, e o SQLite é a opção mais leve e intuitiva para analistas que sobrevive a um reinício do sistema.

import csv, json, sqlite3

# CSV with named headers (clearer than raw csv.writer)
with open("rows.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)

# JSON Lines
with open("rows.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(r, ensure_ascii=False) + "\n")

# SQLite
con = sqlite3.connect("rows.db")
con.execute("CREATE TABLE IF NOT EXISTS staff (name TEXT, position TEXT, office TEXT, extn TEXT, start_date TEXT, salary TEXT)")
con.executemany("INSERT INTO staff VALUES (:name, :position, :office, :extn, :start_date, :salary)", records)
con.commit(); con.close()

csv.DictWriter Vale a pena as poucas linhas extra porque a linha de cabeçalho permanece sincronizada com as chaves do dicionário; ninguém tem de se lembrar qual a coluna que era o índice 3. A mesma records lista alimenta os três gravadores, pelo que trocar de formato é uma alteração de uma linha em produção.

Solução alternativa: renderize a tabela com o Playwright quando a rede estiver indisponível

Alguns sites genuinamente não permitem que se aproxime do JSON. URLs assinadas que expiram em segundos, pontos finais GraphQL com verificações rigorosas Origin , grelhas alimentadas por WebSocket e um punhado de configurações personalizadas levam-no a renderizar a página num navegador real. O Playwright para Python é uma excelente opção moderna padrão para essa tarefa, embora o Selenium continue a ser uma escolha razoável em pilhas legadas.

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/grid", wait_until="networkidle")
    page.wait_for_selector("table.grid tbody tr")
    rows = page.locator("table.grid tbody tr").all_text_contents()
    browser.close()

Uma armadilha a ter em conta em qualquer alternativa de web scraping de tabelas JavaScript: bibliotecas de grelhas do lado do cliente, como DataTables, AG Grid e TanStack Table, virtualizam frequentemente a renderização, o que significa que apenas as linhas atualmente visíveis na janela de visualização são montadas no DOM num determinado momento. A contagem exata de linhas depende do tamanho da janela de visualização e da configuração da biblioteca, por isso não confie em tr para capturar tudo. Percorra o contentor num ciclo, aguarde novas linhas com um MutationObserverou chame a API de paginação da própria biblioteca até que o total de linhas pare de crescer.

Armadilhas comuns na extração de tabelas JavaScript

A maioria das falhas na extração de dados de tabelas JavaScript na web é silenciosa. O script é executado, o ficheiro é gravado e ninguém percebe que os dados estão errados até que um painel de controlo o indique. Esteja atento a estas situações:

  • Selecionar tabelas por índice. tables[2] falha no momento em que o marketing adiciona um widget de comparação acima da grelha. Em vez disso, faça a correspondência pelo texto da legenda, ID ou um cabeçalho único.
  • Grelhas virtualizadas. Uma extração simplista em DataTables, AG Grid ou TanStack Table pode capturar apenas as linhas visíveis na janela de visualização, enquanto milhares ficam por extrair. Confirme os totais de linhas com base numa contagem da API ou num pedido paginado.
  • Números formatados por localidade. 1.000,50 é europeu para 1000.50, mas o Python float() lê-o como 1.0. Normalize a string antes de converter.
  • Fusos horários nas datas. "2025-04-01" Analisadas sem zona, assumem silenciosamente a meia-noite UTC, deslocando os agregados diários numa linha.
  • Símbolos monetários e separadores de milhares. "$1,234" não será convertido para um float. Remova primeiro os caracteres não numéricos.
  • Cookies expirados. Um cookie de sessão colado funciona durante um dia, depois devolve silenciosamente 401s que alguns servidores disfarçam como HTML HTTP 200.
  • 200s anti-bot. Um WAF pode devolver uma página de desafio captcha com o estado 200. r.json() Lança uma exceção, mas apenas se se lembrar de a chamar.

Valide e monitore o pipeline de extração

Uma extração não está concluída quando o «CSV é criado». Está concluída quando confiar no ficheiro amanhã. Adicione uma pequena camada de validação após o gravador: verifique se a contagem de linhas está dentro de uma faixa razoável da execução de ontem, falhe de forma evidente se qualquer coluna obrigatória tiver uma taxa de nulos acima de um limiar (1 a 5 por cento funciona) e compare o conjunto de colunas com um manifesto guardado, para que um campo renomeado sinalize um desvio do esquema em vez de prejudicar uma junção a jusante. Alerte separadamente sobre execuções com zero linhas. A maioria dos pipelines de tabelas JavaScript de web scraping falham devido a uma redução silenciosa, não a falhas evidentes.

Pontos-chave

  • O caminho padrão para tabelas JavaScript de web scraping é o endpoint JSON oculto, não um navegador headless. Use a árvore de decisão antes de escrever qualquer código.
  • O separador Rede do DevTools, juntamente com uma ação de ordenação ou paginação acionada, é a forma mais rápida de identificar a chamada que realmente transporta as linhas.
  • Reproduza o pedido sem estado: cabeçalhos públicos, raise_for_status(), uma sessão real para inícios de sessão e nunca um cookie pessoal colado manualmente.
  • Os padrões de paginação variam (DataTables draw/start/length, cursores, deslocamentos); trate o loop, e não a solicitação única, como a unidade de trabalho.
  • O Playwright é a ferramenta certa quando o caminho de rede é assinado, encriptado ou está ausente, e apenas nessa altura. Esteja atento a grelhas virtualizadas que montam apenas linhas da janela de visualização.
  • Um pipeline que possa ser executado novamente no próximo trimestre tem verificações de contagem de linhas, limites de taxa de nulos e um manifesto de colunas, não apenas um CSV funcional hoje.

Perguntas frequentes

Por que é que requests.get() devolve linhas vazias para uma tabela JavaScript?

Porque requests não executa JavaScript. Ele descarrega o documento que o servidor serviu inicialmente, que contém a estrutura da página e um pacote de scripts, mas sem linhas. As linhas são adicionadas posteriormente por código do lado do cliente que chama um ponto de extremidade JSON. O seu analisador vê a <table> e não retorna nada.

Preciso mesmo do Selenium ou do Playwright para extrair uma tabela dinâmica?

Normalmente não. Se o DevTools mostrar uma solicitação JSON que preenche a grade, reproduzir essa solicitação com requests ou httpx é mais rápido, mais barato e mais fiável do que um navegador. Recorra ao Playwright apenas quando a chamada for assinada, for GraphQL com verificações rigorosas de origem, for orientada por WebSocket ou for de outra forma inacessível a partir de um cliente HTTP simples.

Como faço para extrair uma tabela JavaScript que requer login ou um token CSRF?

Use um requests.Session para que os cookies persistam entre as chamadas. Envie as suas credenciais para o ponto final de login e, em seguida, leia o valor CSRF a partir de um campo de entrada oculto ou do XSRF-TOKEN cookie e reenvie-o como um cabeçalho na solicitação de dados. Nunca codifique um cookie de sessão copiado do seu próprio navegador.

E se a API oculta apenas devolver uma página de linhas de cada vez?

Faça um loop. Inspecione os parâmetros da solicitação (start, length, cursor, page, offset) e incremente-os até que a resposta retorne zero linhas ou um has_more: false sinalizador. Adicione um backoff exponencial para o erro HTTP 429 e um limite rígido de solicitações para que um bug do lado do servidor não transforme o seu scraper num loop infinito.

Conclusão

O scraping de tabelas JavaScript na Web deixa de ser assustador no momento em que deixas de tratar a página renderizada como a fonte da verdade. O navegador é um renderizador; o endpoint JSON por trás da grelha é a verdadeira fonte de dados. Encontra esse endpoint no DevTools, reproduz-o com requests, pagine-o corretamente, valide a saída e terá um script que sobrevive à próxima reformulação, em vez de um que silenciosamente enche o seu armazém com linhas vazias.

Guarde o navegador headless para os casos que realmente precisam dele. Sites com chamadas de rede assinadas, grelhas alimentadas por WebSocket ou proteção agressiva contra bots vão levá-lo a essa situação, e é exatamente aí que um caminho alternativo é importante. Quando recorrer a um navegador, seja cuidadoso com a renderização virtualizada, valide os totais das linhas e mantenha a sua camada de monitorização ativa.

Se preferir não gerir a rotação de proxies, as impressões digitais do navegador e os manipuladores de CAPTCHA, a WebScrapingAPI pode ser colocada à frente do seu requests e devolver HTML ou JSON limpo de sites que, de outra forma, bloqueiam o acesso direto, mantendo inalterada a lógica de análise e paginação acima. Seja qual for a rota que escolher, o manual é o mesmo: escolha o caminho de extração mais barato que funcione e torne o script suficientemente honesto para lhe dizer quando deixar de funcionar.

Sobre o autor
Andrei Ogiolan, Desenvolvedor Full Stack @ WebScrapingAPI
Andrei OgiolanDesenvolvedor Full Stack

Andrei Ogiolan é um programador Full Stack na WebScrapingAPI, contribuindo em todas as áreas do produto e ajudando a criar ferramentas e funcionalidades fiáveis para a plataforma.

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.