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

Como extrair tabelas HTML usando Python

Como extrair tabelas HTML usando Python
Resumo: A maioria das tabelas HTML pode ser extraída com uma única linha de pandas.read_html. Quando a tabela estiver paginada, renderizada em JavaScript ou tiver cabeçalhos combinados, mude para Requests + BeautifulSoup ou um navegador headless como o Playwright. Este guia fornece-lhe uma matriz de decisão, código funcional para as três abordagens e os passos de limpeza que transformam as linhas extraídas em dados prontos para o pipeline.

Os dados tabulares estão por toda a parte na web pública, desde caixas de informação da Wikipédia e filtros de ações até estatísticas governamentais, estatísticas desportivas e páginas de comparação de produtos. Se souber como extrair tabelas HTML usando Python, pode transformar essas linhas em DataFrames limpos, documentos JSON ou linhas na sua própria base de dados em poucos minutos.

O problema é que a tabela HTML é uma categoria aparentemente muito ampla. Algumas tabelas encontram-se organizadas dentro de <table> marcação que o pandas consegue analisar com uma única linha. Outras são grelhas criadas manualmente <div>, paginadas em dezenas de páginas, ou que só são preenchidas após a execução de JavaScript no navegador. Um método que funciona na perfeição na Wikipédia pode silenciosamente devolver zero linhas numa aplicação de página única.

Este guia apresenta três abordagens em Python e estrutura todo o artigo em torno de duas questões práticas: qual o método que deve utilizar e como manter o seu scraper a funcionar quando o site alterar a sua marcação no próximo trimestre?

Como extrair tabelas HTML usando Python: uma matriz de decisão rápida

Antes de escrever uma linha de código, decida qual a ferramenta que se adequa à tabela que tem à sua frente. Escolher mal é a razão mais comum pela qual os tutoriais não sobrevivem ao contacto com sites reais. Use a matriz abaixo para fazer a sua escolha.

Critério

pandas.read_html

Requests + BeautifulSoup

Playwright (ou Selenium)

Ideal quando

A tabela estiver no HTML inicial e bem formada

Precisa de controlo ou filtragem por célula

A tabela é renderizada por JavaScript

Linhas de código

~3

30 a 80

40 a 100

Velocidade por página

Rápida

Rápida

Lenta (navegador completo)

Suporta JS

Não

Não

Sim

Paginação

Loop manual

Loop manual ou API oculta

Clicar e rolar

Resiliência à rotatividade de marcação

Média

Alta (você escreve seletores)

Alta

Consumo de memória

Baixa

Baixo

Elevado

Três regras gerais:

  • Se pd.read_html(url) retornar as linhas que esperas, pára por aí. O código de uma linha é o código mais fácil de manter que alguma vez irás escrever.
  • Se a tabela estiver no HTML, mas precisar de filtrar, juntar ou normalizar células antes de elas chegarem a um DataFrame, recorra ao Requests + BeautifulSoup.
  • Se a opção «Ver código-fonte da página» mostrar um <div id="grid"> e os dados só aparecem depois de a página carregar, precisas do Playwright ou de um endpoint JSON oculto.

O resto deste artigo mostra como extrair tabelas HTML usando Python em cada um desses cenários, além dos casos extremos que podem comprometer um código que, de outra forma, funcionaria.

Anatomia de uma tabela HTML (e o que torna a extração complicada)

Uma tabela HTML clássica tem este aspeto:

<table id="employees" class="stripe">
  <thead><tr><th>Name</th><th>Position</th><th>Salary</th></tr></thead>
  <tbody>
    <tr><td>Ada Lovelace</td><td>Engineer</td><td>$120,000</td></tr>
    <tr><td>Alan Turing</td><td>Researcher</td><td>$135,000</td></tr>
  </tbody>
</table>

Cinco tags fazem a maior parte do trabalho: <table> é o contêiner, <thead> e <tbody> agrupa linhas, <tr> é uma linha, e <th> ou <td> são células de cabeçalho e de dados, respetivamente. Dois atributos complicam as coisas: colspan faz com que uma célula ocupe várias colunas, e rowspan faz com que ela se estenda por várias linhas. Ambos são muito utilizados em tabelas financeiras e desportivas.

Na prática, metade destas convenções é ignorada. Muitas páginas omitem <thead> e <tbody>, deixam de fora as tags de fechamento ou renderizam tabelas como <div> que nenhum analisador reconhecerá como uma tabela. A extração de dados no mundo real consiste principalmente em lidar com essa variação, razão pela qual o pandas por si só não é suficiente em todos os sites.

Método 1: pandas.read_html, a linha única

pandas.read_html é uma função de conveniência na biblioteca de manipulação de dados pandas que recebe um URL ou uma string HTML e devolve uma lista de DataFrames, um por <table> que consiga encontrar. De acordo com a documentação do pandas, requer lxml, html5lib, ou bs4 por baixo do capô, e identifica tabelas procurando por elementos de tabela padrão.

Todo o apelo reside no facto de poder escrever três linhas de código e obter um DataFrame tipado e pesquisável:

import pandas as pd

tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue")
df = tables[0]
print(df.head())

O problema é que read_html só vê o que já está no corpo da resposta. Se a tabela for preenchida por JavaScript após o carregamento da página, a função gera um erro ValueError: No tables found mesmo que a tabela esteja claramente visível no seu navegador. Saber dessa limitação desde o início poupa muito trabalho de depuração.

Configurar o seu ambiente Python

Podes executar todos os exemplos deste guia com um ambiente virtual novo e três pacotes:

python -m venv .venv
source .venv/bin/activate
pip install pandas requests beautifulsoup4 lxml html5lib playwright
playwright install chromium

lxml é o analisador HTML mais rápido disponível para Python e é o que a maioria dos profissionais usa por padrão. html5lib é mais lento, mas segue o algoritmo de análise do WHATWG, o que o torna a escolha mais tolerante em relação a marcação incorreta. Instale ambos para poder trocar de analisador quando um falhar.

Um guia completo do pandas.read_html

Vamos extrair uma tabela real e bem formada: a lista de países por PIB na Wikipédia. O fluxo de trabalho completo tem quatro linhas.

import pandas as pd

url = "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)"
tables = pd.read_html(url)
print(f"Found {len(tables)} tables on the page")

gdp = tables[2]            # pick the right one by index
gdp.columns = [c[1] if isinstance(c, tuple) else c for c in gdp.columns]
print(gdp.head())

Três coisas a ter em conta. Primeiro, read_html retornou uma lista, por isso indexa-se nela. Segundo, as tabelas da Wikipédia têm frequentemente cabeçalhos de vários níveis, que o pandas expõe como um MultiIndex. A compreensão de lista achata-a mantendo o nível inferior. Terceiro, não há iteração manual de linhas: cada célula já reside numa coluna tipada que pode chamar .sort_values, .groupbyou .to_csv .

Quando só precisas dos dados para uma análise rápida, este é realmente todo o código que deves escrever.

Resolução de problemas com pandas.read_html: Erros comuns

pd.read_html falha de formas previsíveis. Memorize estes quatro e resolverá a maioria dos problemas em menos de um minuto.

  1. ValueError: No tables found. A página é renderizada em JavaScript ou está protegida por uma página de login. Passe diretamente para a secção do Playwright.
  2. HTTP 403 ou 429 devolvido pelo fetcher interno do pandas. O urllib agente de utilizador está a ser bloqueado. Obtenha o HTML por conta própria com o Requests e passe a string para read_html:
import requests, pandas as pd
headers = {"User-Agent": "Mozilla/5.0 (compatible; analytics-bot/1.0)"}
html = requests.get(url, headers=headers, timeout=15).text
tables = pd.read_html(html)
  1. Índice de tabela incorreto. Use match= para filtrar por uma string que apareça dentro da tabela de destino, por exemplo pd.read_html(html, match="Population"). Isto é muito mais estável do que confiar em tables[3].
  2. Caracteres ilegíveis em conteúdo não ASCII. Força uma codificação lendo os bytes explicitamente: response = requests.get(url); response.encoding = "utf-8"; tables = pd.read_html(response.text).

Se ainda estiver a deparar-se com dificuldades após estas correções, a tabela quase certamente precisa do Requests + BeautifulSoup ou de um navegador sem interface gráfica, e não de mais read_html soluções alternativas.

Método 2: Requests + BeautifulSoup, quando precisa de controlo

pandas.read_html é ótimo quando se quer que cada célula fique exatamente como aparece no HTML. No momento em que precisar filtrar linhas durante a extração, juntar valores de duas colunas, remover símbolos de moeda na hora ou extrair href de uma célula com link, deixa de ser a ferramenta certa.

É aí que entra o Requests + BeautifulSoup. O Requests lida com a camada HTTP (cabeçalhos, cookies, sessões, tentativas de recarga), e o BeautifulSoup fornece-lhe uma árvore de análise que pode percorrer com seletores CSS, correspondência de atributos ou navegação entre elementos irmãos. Se é novo no BeautifulSoup, o nosso artigo aprofundado sobre a extração e análise de dados da Web com Python e BeautifulSoup percorre a superfície da API em detalhe. Esta combinação é também aquela por que a maioria dos scrapers de produção acaba por optar, porque cada passo (buscar, analisar, extrair, transformar) é algo que controlas.

As próximas três secções mostram como extrair tabelas HTML usando Python com esta pilha: um pedido educado, um seletor robusto para a tabela e um loop de linhas que não falha quando uma coluna é adicionada.

Envio de Pedidos HTTP Educados e Realistas

As defesas anti-bot baseiam-se em alguns sinais simples: um User-Agent ausente ou padrão, sem Accept-Language, sem cookies e tráfego que esgota uma sessão num segundo. Imite um navegador real e reutilize uma ligação:

import requests
from bs4 import BeautifulSoup

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})

response = session.get("https://example.com/employees", timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")

Três pequenos hábitos são importantes. Session() mantém os cookies e o pool de ligações entre chamadas. raise_for_status() transforma respostas 4xx/5xx silenciosas em exceções que pode tentar novamente. E passar "lxml" como o analisador é cerca de cinco a dez vezes mais rápido do que o html.parser em páginas grandes.

Localizar a tabela certa na página

Depois de ter um BeautifulSoup objeto, o próximo problema é identificar a tabela certa <table>. As páginas têm normalmente entre oito a quinze delas (pense em: tabelas de layout, widgets da barra lateral, controlos de paginação ocultos). Experimente os seletores nesta ordem de estabilidade:

# 1. By stable id (best)
table = soup.find("table", id="employees")

# 2. By a class that's specific to this table
table = soup.find("table", class_="data-grid")

# 3. By a CSS selector
table = soup.select_one("section#payroll table.stripe")

# 4. By the heading that precedes it (when classes are dynamic)
heading = soup.find(["h2", "h3"], string=lambda s: s and "Employees" in s)
table = heading.find_next("table") if heading else None

Quando os nomes de classe são gerados automaticamente e mudam a cada implementação (um padrão comum do React), prefira o XPath via lxml, uma vez que permite expressar «a terceira tabela dentro da secção cujo texto do cabeçalho contém “Empregados”» numa única expressão. Temos um guia separado sobre XPath versus seletores CSS que aprofunda esta questão.

Iterar linhas e extrair células de forma segura

A maioria dos tutoriais de scraping mostra loops de linhas que indexam células posicionalmente: cells[0] é nome, cells[1] é a posição, cells[2] é o salário. Esse código deixa de funcionar no momento em que alguém adiciona uma coluna «Departamento». O padrão robusto consiste em ler os cabeçalhos uma vez e associá-los a cada linha.

# Read headers from <thead> if present, else from the first row
header_cells = table.select("thead th") or table.select("tr:first-of-type th, tr:first-of-type td")
headers = [th.get_text(strip=True) for th in header_cells]

rows = []
for tr in table.select("tbody tr") or table.select("tr")[1:]:
    cells = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])]
    if not cells:
        continue
    rows.append(dict(zip(headers, cells)))

print(f"Extracted {len(rows)} rows with {len(headers)} columns")

Isto oferece-lhe três vantagens. As novas colunas são incorporadas automaticamente porque as chaves provêm dos cabeçalhos, e não de índices. As linhas vazias (frequentemente utilizadas como separadores visuais) são ignoradas. E todas as células passam pelo get_text(strip=True), o que comprime os espaços em branco e remove os \n caracteres que assombram as chamadas cell.text . Este é o loop de linhas que deve copiar para todos os projetos BeautifulSoup.

Guardar linhas extraídas em JSON, CSV ou Parquet

Assim que tiver uma lista de dicionários, guardá-los é uma linha de código por formato:

import json
import pandas as pd

# JSON, human-readable, UTF-8 safe
with open("employees.json", "w", encoding="utf-8") as f:
    json.dump(rows, f, indent=2, ensure_ascii=False)

# CSV via pandas (handles quoting, encoding, and missing keys)
df = pd.DataFrame(rows)
df.to_csv("employees.csv", index=False, encoding="utf-8")

# Parquet for analytical pipelines (smaller files, typed columns)
df.to_parquet("employees.parquet", index=False)

Opte por JSON quando o consumidor for um script ou um frontend, CSV quando um utilizador for abri-lo no Excel ou no BigQuery, e Parquet quando o conjunto de dados ultrapassar algumas centenas de milhares de linhas ou alimentar o Spark, o Snowflake ou o DuckDB. Os ficheiros Parquet são normalmente 5 a 10 vezes mais pequenos do que os CSV equivalentes e preservam os tipos de dados. Para qualquer coisa destinada a uma base de dados relacional, salte diretamente para df.to_sql para que possa ignorar completamente o ficheiro intermédio.

Lidar com cabeçalhos complexos com colspan e rowspan

Cabeçalhos de duas linhas são comuns em finanças, estatísticas governamentais e tabelas desportivas. A linha superior agrupa colunas ("1.º trimestre de 2024", "2.º trimestre de 2024"), e a linha inferior identifica-as ("Receita", "Lucro"). Codificar nomes de colunas como ["Name", "Position", "Contact"] funciona uma vez e falha para sempre. Aqui está um algoritmo genérico que respeita colspan e rowspan.

def expand_header(table):
    # Return a flat list of column labels from a multi-row <thead>
    rows = table.select("thead tr")
    if not rows:
        return [th.get_text(strip=True) for th in table.select("tr:first-of-type th")]

    grid = []  # grid[row_index] = list of column labels at that row
    for r, tr in enumerate(rows):
        while len(grid) <= r:
            grid.append([])
        col = 0
        for th in tr.find_all(["th", "td"]):
            # skip already-filled slots from previous rowspans
            while col < len(grid[r]) and grid[r][col] is not None:
                col += 1
            text = th.get_text(strip=True)
            colspan = int(th.get("colspan", 1))
            rowspan = int(th.get("rowspan", 1))
            for dr in range(rowspan):
                while len(grid) <= r + dr:
                    grid.append([])
                row_buf = grid[r + dr]
                # pad
                while len(row_buf) < col + colspan:
                    row_buf.append(None)
                for dc in range(colspan):
                    row_buf[col + dc] = text
            col += colspan

    # Combine the columns of each row, top-down, into a single label per column
    n_cols = max(len(r) for r in grid)
    flat = []
    for c in range(n_cols):
        parts = [grid[r][c] for r in range(len(grid)) if c < len(grid[r]) and grid[r][c]]
        # de-dup adjacent identical strings: ['Q1 2024', 'Q1 2024', 'Revenue'] -> 'Q1 2024 Revenue'
        seen = []
        for p in parts:
            if not seen or seen[-1] != p:
                seen.append(p)
        flat.append(" ".join(seen))
    return flat

Combine isto com o mesmo zip(headers, cells) do ciclo de linhas anterior e terá uma análise de cabeçalhos que resiste a qualquer combinação de células fundidas. A mesma ideia (uma grelha 2D que preenche colspan por colspan) estende-se ao corpo quando os rowspans repetem valores ao longo das colunas: acompanhe quais os espaços já ocupados e ignore-os nas <tr> .

Extracção de tabelas HTML paginadas (três estratégias)

A paginação é a parte mais subestimada de como extrair tabelas HTML usando Python. A maioria dos tutoriais mostra apenas «clique no botão seguinte num navegador sem interface gráfica», que é a abordagem mais lenta e frágil. Experimente estas três primeiro, por ordem de preferência.

1. Aumente o parâmetro de consulta page-size. Muitas tabelas aceitam ?per_page=500 ou ?length=1000. Uma solicitação, todas as linhas, sem loops. Inspecione a URL quando clicar no menu suspenso de tamanho da página e, muitas vezes, encontrará isso gratuitamente.

2. Aceda à API JSON subjacente. Abra o DevTools, mude para o separador Rede, filtre por Fetch/XHRe clique na página seguinte. Quase todas as tabelas de dados modernas são suportadas por um endpoint que devolve JSON. Ao chamá-lo diretamente, salta-se completamente a análise do HTML:

import requests
url = "https://example.com/api/employees"
all_rows = []
for page in range(1, 20):
    payload = requests.get(url, params={"page": page, "size": 100}, timeout=15).json()
    if not payload["items"]:
        break
    all_rows.extend(payload["items"])

3. Percorra as cadeias de consulta da página. Quando o URL contiver o número da página (?page=2, &start=20), itere-o explicitamente e pare quando a tabela voltar vazia. Isto é mais fiável do que utilizar um navegador, porque não há nada para clicar nem animações para esperar.

Um navegador sem interface gráfica é o seu último recurso, não o primeiro. Guarde-o para tabelas em que o link da página seguinte esteja vinculado a um manipulador JavaScript sem alteração de URL.

Método 3: Playwright para tabelas renderizadas em JavaScript

Quando a tabela só aparece após a página ser carregada, é necessário algo que execute JavaScript. O Playwright é a escolha moderna: inclui ligações oficiais para Python, executa o Chromium, o Firefox ou o WebKit e possui um comportamento de espera automática sólido. Aqui está o modelo completo de como extrair tabelas HTML usando Python que dependem de JS:

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import pandas as pd

URL = "https://example.com/dashboard"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(user_agent="Mozilla/5.0 ... Chrome/124.0 Safari/537.36")
    page.goto(URL, wait_until="domcontentloaded")

    # Wait for the actual data, not just the page load
    page.wait_for_selector("table#grid tbody tr", timeout=15000)

    html = page.content()
    browser.close()

# Hand the rendered HTML off to your existing parser
soup = BeautifulSoup(html, "lxml")
table = soup.find("table", id="grid")
# ... use the same row loop from earlier ...

# Or, when the table is well-formed, skip BeautifulSoup entirely:
df = pd.read_html(html, match="Department")[0]
print(df.head())

O padrão é sempre o mesmo: navegar, esperar pelos dados (não apenas load), recolher page.content()e, em seguida, insira essa string no mesmo código de análise que usaria para HTML estático. Consulte a documentação do Playwright para Python para obter informações sobre instalação, APIs assíncronas e rastreamento.

O Selenium e o Pyppeteer são alternativas válidas. O Selenium tem um ecossistema mais vasto e é a escolha segura se a sua equipa já o utiliza para testes de ponta a ponta, e o nosso tutorial passo a passo do Selenium abrange a configuração equivalente. O Pyppeteer é mais leve, mas tem uma manutenção menos ativa. Para uma comparação mais completa das ferramentas headless, consulte o nosso guia de extração de dados da Web com o Playwright. Para novos projetos, o Playwright tende a ser o mais ergonómico.

Escolher um analisador HTML e lidar com células vazias

O BeautifulSoup é um wrapper. A análise propriamente dita é delegada a um de três backends, e a escolha é mais importante do que a maioria dos tutoriais admite.

Analisador

Velocidade

Tolerância a HTML incorreto

Instalação

html.parser

Lenta

Médio (integrado no Python)

Nenhuma

lxml

Rápido

Rigoroso, mas pragmático

pip install lxml

html5lib

Mais lento

Mais rigoroso, segue o WHATWG

pip install html5lib

Padrão para lxml. Mude para html5lib apenas quando lxml retornar uma árvore parcial numa página com marcação incorreta (falta de </td>, <tr>, < ). Pode verificar rapidamente:

import time
from bs4 import BeautifulSoup

for parser in ["lxml", "html.parser", "html5lib"]:
    t0 = time.perf_counter()
    soup = BeautifulSoup(html, parser)
    rows = soup.select("tbody tr")
    print(f"{parser:10} {len(rows):4} rows in {time.perf_counter()-t0:.3f}s")

Para células vazias, escreva um helper que retorne um valor padrão sensato em vez de falhar:

def cell_text(cell, default=""):
    if cell is None:
        return default
    text = cell.get_text(" ", strip=True)
    return text if text else default

Use-o sempre que indexar uma linha. None verificações em cada local de chamada sobrecarregam o ciclo e não detectam o caso em que a célula existe, mas contém apenas &nbsp;. Este auxiliar lida com ambos os casos.

Evitar bloqueios: cabeçalhos, sessões e proxies

Um código de estado 200 significa que o pedido foi aceite. Qualquer outra coisa (especialmente 403, 429 ou 503) significa normalmente que o site detetou o seu scraper. Suba esta escada por ordem, parando no primeiro degrau que funcionar.

  1. Cabeçalhos realistas. Defina User-Agent, Accept-Languagee Referer para valores que uma sessão real do Chrome enviaria. Só isto resolve um número surpreendente de bloqueios.
  2. Sessões persistentes. Use requests.Session() para que os cookies definidos pela página inicial sejam enviados nas chamadas subsequentes. Muitos sites emitem um cookie de sessão na primeira visita e rejeitam pedidos que não o possuam.
  3. Retardamento exponencial nos códigos 429 e 503. Aguarde 2 ** attempt segundos e tente novamente até cinco vezes. Respeite Retry-After os cabeçalhos quando o servidor os fornecer.
  4. Proxies de centro de dados. Baratos, rápidos e suficientes para a maioria dos sites estáticos. Alterne os IPs no seu conjunto de trabalhadores.
  5. Proxies residenciais. IPs residenciais reais de 195 países, usados quando os intervalos de endereços de centros de dados já estão bloqueados. Mais lentos, mas mais difíceis de detectar.
  6. APIs de scraping geridas. Quando quiser concentrar-se na análise em vez de na infraestrutura, serviços como a nossa API Scraper na WebScrapingAPI tratam da rotação de proxies, geração de cabeçalhos e novas tentativas por trás de um único ponto de extremidade, para que o mesmo código BeautifulSoup ou pandas continue a funcionar.

A maioria dos projetos necessita dos passos um a três. Para uma lista de verificação mais extensa de sinais de deteção, o nosso guia sobre por que razão os scrapers são bloqueados ou banidos por IP aprofunda a identificação de impressões digitais TLS, a ordem dos cabeçalhos e a matemática do limite de taxa. Se estiver a ser bloqueado num artigo da Wikipédia, há algo de errado.

Limpeza, Coerção de Tipos e Exportação para Produção

As tabelas extraídas quase nunca estão prontas para análise. Símbolos monetários, sinais de percentagem, marcadores de notas de rodapé e espaços em branco finais aparecem todos como cadeias de caracteres. Corrija-os numa única passagem antes de guardar:

import pandas as pd

df = pd.DataFrame(rows)

# 1. Strip whitespace on every text column
str_cols = df.select_dtypes(include="object").columns
df[str_cols] = df[str_cols].apply(lambda s: s.str.strip())

# 2. Coerce numeric columns (errors='coerce' turns junk into NaN)
df["salary"] = pd.to_numeric(df["salary"].str.replace(r"[^0-9.\-]", "", regex=True),
                             errors="coerce")
df["growth_pct"] = pd.to_numeric(df["growth_pct"].str.rstrip("%"), errors="coerce")

# 3. Coerce dates
df["hired_at"] = pd.to_datetime(df["hired_at"], errors="coerce")

# 4. Drop rows where the primary key failed to parse
df = df.dropna(subset=["employee_id"])

# 5. Persist
df.to_parquet("employees.parquet", index=False)
df.to_sql("employees", con=engine, if_exists="replace", index=False)

A errors="coerce" flag é o herói subestimado deste pipeline: as células incorretas tornam-se NaN em vez de gerarem erros, e pode investigá-las mais tarde com df[df["salary"].isna()]. Para pipelines de produção, escreva em Parquet para armazenamento e use to_sql para colocar os dados limpos no Postgres ou no seu armazém de dados de eleição.

Diretrizes legais e éticas

Estas são orientações para redução de riscos, não aconselhamento jurídico. Consulte um advogado antes de extrair qualquer informação sensível.

  • Leia o robots.txt. Ele expressa a preferência do proprietário do site, não uma regra legal, mas ignorá-lo é uma maneira rápida de ser bloqueado. A especificação está documentada na RFC 9309.
  • Leia os Termos de Serviço. A extração de dados após o início de sessão, em particular, viola frequentemente os Termos de Serviço, mesmo quando o robots.txt não se pronuncia.
  • Limite a sua própria taxa. Uma solicitação por segundo é um padrão razoável para projetos pequenos. Adicione jitter para não parecer um relógio.
  • Evite dados pessoais, a menos que tenha uma base legal. O RGPD e leis semelhantes aplicam-se mesmo quando os dados são tecnicamente públicos.
  • Indique a fonte ao republicar. Cite o URL da fonte e a data da recolha.

Saber como extrair tabelas HTML usando Python é metade técnico, metade ético. A parte técnica falha uma vez; a parte ética pode arruinar a sua empresa.

Pontos-chave

  • Escolha a ferramenta mais simples que funcione. pandas.read_html Para tabelas estáticas limpas, use Requests + BeautifulSoup; para controlo, use Playwright; para tabelas renderizadas em JS ou orientadas por interação, use Playwright.
  • Cabeçalhos, não índices. Comprime o texto do cabeçalho com o texto da célula para que o teu scraper sobreviva a uma coluna adicionada. Código cells[0], cells[1] é dívida técnica.
  • A paginação tem três camadas. Tente per_page=500, depois uma API JSON oculta e, por fim, loops de números de página. Um navegador sem interface gráfica é o último recurso.
  • Limpe antes de guardar. pd.to_numeric, pd.to_datetime, e errors="coerce" transforme linhas sujas extraídas num DataFrame tipado pronto para análise.
  • Respeite o site. Respeite o robots.txt, limite as solicitações e evite dados pessoais, a menos que tenha uma base legal clara.

Perguntas frequentes

Qual é a diferença entre pandas.read_html e BeautifulSoup para extrair tabelas?

pandas.read_html é um atalho de alto nível: retorna DataFrames diretamente, mas lida apenas com tabelas já presentes na resposta HTML. O BeautifulSoup é um analisador HTML de baixo nível que lhe dá controlo total sobre quais as células que mantém, como as transforma e como navegar em marcação não padrão. Use read_html para dados prontos para análise, e o BeautifulSoup quando as regras de que necessita não puderem ser expressas como «dá-me a tabela N».

Como extraio uma tabela HTML que só aparece depois de o JavaScript ser executado?

Primeiro, confirme se é realmente renderizada por JavaScript: veja o código-fonte da página (Ctrl+U), procure por uma palavra da tabela e, se ela estiver ausente, a tabela é dinamizada no lado do cliente. A solução mais rápida é encontrar o endpoint JSON subjacente na guia Rede do DevTools e chamá-lo diretamente. Se isso não for viável, utilize um navegador headless como o Playwright, aguarde um seletor de linha e, em seguida, passe page.content() para o seu analisador habitual.

O que devo fazer quando uma tabela tem células unidas (rowspan ou colspan)?

Trate a tabela como uma grelha 2D que preenche célula a célula, respeitando colspan e rowspan , em vez de uma lista de linhas. Para cada <th> ou <td>, repita o seu valor em todas as posições que o seu span abrange e ignore as posições já preenchidas por um rowspan anterior. Isto produz uma matriz retangular que pode passar para pd.DataFrame sem incompatibilidades no número de colunas.

Como faço para manter as colunas numéricas e de data com o tipo correto após extrair uma tabela?

Remova caracteres não numéricos com uma expressão regular (str.replace(r"[^0-9.\-]", "", regex=True)), depois chame pd.to_numeric(series, errors="coerce") para que os valores não analisáveis se tornem NaN em vez de gerarem erros. Para datas, pd.to_datetime(series, errors="coerce", format="%Y-%m-%d") é o equivalente. Adicionar o format argumento torna a análise cerca de 10 vezes mais rápida em colunas grandes e evita falsos positivos de cadeias ambíguas.

Posso executar pandas.read_html num ficheiro HTML local ou numa cadeia de caracteres HTML bruta?

Sim. pd.read_html Aceita um URL, um caminho para um ficheiro local ou uma string HTML bruta. Passe pd.read_html(open("page.html").read()) para fornecer uma string, ou pd.read_html("page.html") para um caminho de ficheiro. Isto é útil para testes unitários (submeter um fixture HTML comprovadamente válido) e para separar a recuperação da análise em scrapers de produção.

Conclusão

Saber como extrair tabelas HTML usando Python tem principalmente a ver com escolher a ferramenta certa para a tabela. Comece pandas.read_html primeiro, passe para Requests + BeautifulSoup quando precisar de controlo ao nível das células e só recorra ao Playwright quando o JavaScript renderizar os dados. Adicione loops de linhas sensíveis a cabeçalhos, análise genérica de colspan/rowspan, paginação inteligente e uma passagem de limpeza com pandas, e terá um scraper que sobrevive a alterações na marcação em vez de falhar na próxima implementação.

Quando ultrapassar a rotação de proxies «faça você mesmo» e a renderização JavaScript, a WebScrapingAPI oferece uma API de Scraper que gere a camada de pedidos por trás de um único ponto de extremidade, para que o seu código de análise continue a funcionar. A partir daqui, explore os nossos guias mais aprofundados sobre tabelas JavaScript e sobre como evitar bloqueios.

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.