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

Como extrair tabelas HTML em Golang com Colly: Guia de ponta a ponta

Como extrair tabelas HTML em Golang com Colly: Guia de ponta a ponta
Resumo: Este guia mostra como extrair dados de tabelas HTML em Golang de ponta a ponta: escolha entre Colly, goquery e golang.org/x/net/html, selecione o <tbody>, modele as linhas como uma estrutura tipada e exporte JSON e CSV limpos. Também terá acesso a padrões de tabelas com paginação, anti-bloqueio e renderizadas em JavaScript.

Se já tentaste alimentar um HTML <table> num armazém Postgres ou num CSV para analistas, os dados estão ali mesmo no DOM, mas extraí-los de forma fiável é um pequeno projeto por si só. Este guia explica como extrair tabelas HTML em Golang de uma forma que funcione em páginas reais, e não apenas em tutoriais limpos.

Uma tabela HTML é uma grelha estruturada de linhas (<tr>) e células (<td> ou <th>). Extraí-la significa analisar a marcação, percorrer esses elementos e transformar cada linha num registo tipado que o seu código possa utilizar a jusante. Em Go, tem três opções sérias: Colly, goquery e o golang.org/x/net/html. Iremos abordar quando cada uma é adequada e, em seguida, construir um scraper funcional com base no Colly v2.

Irá aprender a inspecionar uma página no DevTools, escrever um seletor CSS preciso, modelar linhas como uma estrutura, exportar tanto para JSON como para CSV e lidar com paginação, renderização JavaScript e bloqueios anti-bot. No final, terá um padrão pronto a copiar e colar sobre como fazer scraping de tabelas HTML em Golang.

Por que vale a pena aprender a extrair tabelas HTML em Golang

Os dados tabulares aparecem em todo o lado: páginas de preços, estatísticas desportivas, relatórios financeiros, conjuntos de dados públicos que nunca tiveram uma API a sério. Se o seu pipeline começa com <table> marcação e termina num armazém ou num notebook, precisa de uma forma fiável de extrair esses dados. O Go compila para um único binário, lida bem com a concorrência e oferece um desempenho previsível em escala. Saber como extrair tabelas HTML em Golang significa enviar esse pipeline como um serviço autónomo, sem necessidade de um runtime Python.

Quando usar Colly vs. goquery vs. net/html

Escolha a biblioteca errada e passará mais tempo a lutar com a API do que a analisar linhas. Aqui está uma matriz de decisão rápida.

Biblioteca

Ideal para

Ignore quando

Colly v2 (github.com/gocolly/colly/v2)

Rastrear muitas páginas com callbacks de ciclo de vida (OnRequest, OnHTML, OnError), cookies, limitação de taxa, ganchos de proxy

Já tem uma string HTML na memória e não precisa de rede

goquery (github.com/PuerkitoBio/goquery)

Seleção CSS ao estilo jQuery num *goquery.Document que já tenha sido obtida

Também precisa de rastreamento, limitação de taxa e configuração de proxy

golang.org/x/net/html

Rastreamento de tokens e nós de baixo nível quando o CSS não é suficiente

Pode expressar o que deseja em CSS; o goquery tem três vezes menos código

O tópico de longa data no Stack Overflow sobre a análise de tabelas HTML em Go ainda aparece nos resultados para esta pesquisa, e as respostas mais votadas apontam para o goquery e x/net/html. Ambos são sólidos. O Colly junta-os com a ergonomia de rastreamento de que vai precisar assim que tiver mais do que uma página para visitar.

Configure o seu projeto Go e instale o Colly

Crie um módulo e obtenha o Colly v2:

mkdir html-golang-scraper && cd html-golang-scraper
go mod init github.com/yourname/html-golang-scraper
go get github.com/gocolly/colly/v2

Repara no /v2 . A importação original github.com/gocolly/colly importação é a linha v1, e a maioria dos tutoriais mais antigos ainda a referenciam. Os novos projetos devem usar a v2 para obter as correções de bugs atuais e o suporte a módulos Go.

Adicione uma verificação de integridade main.go:

package main

import "fmt"

func main() {
    fmt.Println("scraper booted")
}

Execute go run main.go. Se vir scraper booted, a cadeia de ferramentas está configurada e o Colly está em go.sum. A partir daqui, cada snippet substitui o corpo de main ou adiciona um tipo ao nível do pacote.

Inspecione a tabela de destino antes de escrever o código

Antes de escrever em Go, abra a página de destino no seu navegador e selecione a tabela que deseja. Usaremos a demonstração do DataTables em https://datatables.net/examples/styling/display.html como exemplo prático. Clique com o botão direito do rato na tabela, escolha Inspecionar e confirme três coisas:

  1. O seletor. Procure um id (a demonstração usa #example) ou única. Evite table sozinho, uma vez que as páginas frequentemente envolvem o layout em elementos de tabela aninhados.
  2. Estrutura do cabeçalho. Confirme <thead> e <tbody> estejam separados. Caso contrário, irá ignorar a primeira linha no código.
  3. Estático vs. dinâmico. Desative o JavaScript e recarregue. Se as linhas desaparecerem, a tabela é renderizada pelo cliente. Abordaremos essa vertente mais tarde.

Cinco minutos no DevTools valem mais do que uma hora a depurar uma fatia vazia. A nossa ficha de referência de seletores CSS apresenta os padrões mais utilizados pelos scrapers de tabelas.

Configurar o Collector e os Callbacks do Colly

O Collector é o objeto central: emite pedidos e despacha callbacks de ciclo de vida. Trate os quatro callbacks abaixo como código padrão que pode copiar para todos os projetos.

package main

import (
    "fmt"
    "log"

    "github.com/gocolly/colly/v2"
)

func main() {
    c := colly.NewCollector()

    c.OnRequest(func(r *colly.Request) {
        fmt.Println("visiting:", r.URL.String())
    })

    c.OnResponse(func(r *colly.Response) {
        fmt.Println("status:", r.StatusCode)
    })

    c.OnError(func(r *colly.Response, err error) {
        log.Printf("failed %s: %v", r.Request.URL, err)
    })

    if err := c.Visit("https://datatables.net/examples/styling/display.html"); err != nil {
        log.Fatal(err)
    }
}

OnRequest é disparado antes de cada chamada de rede, OnResponse quando o servidor responde e OnError intercepta respostas que não sejam 2xx e erros de transporte, que é onde a maioria dos scrapers de produção falha silenciosamente. A seguir, vamos adicionar OnHTML a seguir, a chamada de retorno onde ocorre a análise real da tabela.

Selecione a tabela com um seletor CSS preciso

Na demonstração do DataTables, executar document.querySelectorAll('table') no console do navegador retorna mais de uma correspondência, porque a marcação de layout noutros locais também usa elementos de tabela. Selecionar table por si só iria extrair as linhas erradas, por isso valide sempre os seletores no console antes de escrever em Go.

O seletor fiável aqui é table#example > tbody. Ele restringe a uma única tabela ao id e ignora o <thead> , pelo que não é necessário eliminar manualmente a linha de cabeçalho. O widget DataTables também insere linhas de cabeçalho e rodapé espelhadas; restringir a > tbody mantém-nas fora do seu conjunto de dados.

c.OnHTML("table#example > tbody", func(h *colly.HTMLElement) {
    // row loop goes here
})

OnHTML corresponde a elementos através do seletor CSS e chama o manipulador para cada correspondência. Substitua #example pelo que quer que o DevTools lhe mostre. Se estiver a ponderar CSS versus XPath, a nossa comparação entre seletores XPath e CSS aborda as vantagens e desvantagens.

Percorra as linhas e extraia cada célula

Dentro do OnHTML manipulador, chame h.ForEach("tr", ...) e extraia cada célula com el.ChildText("td:nth-child(N)"):

c.OnHTML("table#example > tbody", func(h *colly.HTMLElement) {
    h.ForEach("tr", func(_ int, el *colly.HTMLElement) {
        row := tableData{
            Name:      strings.TrimSpace(el.ChildText("td:nth-child(1)")),
            Position:  strings.TrimSpace(el.ChildText("td:nth-child(2)")),
            Office:    strings.TrimSpace(el.ChildText("td:nth-child(3)")),
            Age:       strings.TrimSpace(el.ChildText("td:nth-child(4)")),
            StartDate: strings.TrimSpace(el.ChildText("td:nth-child(5)")),
            Salary:    strings.TrimSpace(el.ChildText("td:nth-child(6)")),
        }
        employeeData = append(employeeData, row)
    })
})

As células de tabelas HTML quase nunca contêm class ou id , por isso nth-child(n) é a forma mais simples de lidar com as colunas. Se a página reorganizar as colunas, basta alterar um número por campo em vez de reescrever o seu analisador.

Um padrão mais resiliente é ler <thead> primeiro, criar um map[string]int índice de nomes de colunas e procurar as células pelo rótulo do cabeçalho. Vale a pena o código extra se a fonte reorganizar as colunas. Envolva sempre o texto em strings.TrimSpace e analise colunas de moeda ou data com strconv e time.Parse antes da serialização, para que os consumidores não recebam strings como "$320,800" quando esperavam números.

Modele a linha com uma estrutura Go e uma fatia

Defina o tipo da linha ao nível do pacote para que as tags JSON sejam transmitidas com ela:

type tableData struct {
    Name      string `json:"name"`
    Position  string `json:"position"`
    Office    string `json:"office"`
    Age       string `json:"age"`
    StartDate string `json:"start_date"`
    Salary    string `json:"salary"`
}

var employeeData []tableData

Porquê uma estrutura tipada em vez de map[string]string? Três razões:

  1. Chaves JSON estáveis. As tags da estrutura controlam os nomes dos campos e as maiúsculas/minúsculas na saída, em vez de herdar o que quer que tenha digitado durante a análise.
  2. Segurança em tempo de compilação. Erros de digitação impedem a compilação, em vez de produzirem silenciosamente valores vazios que o prejudicam na fase de teste.
  3. Refactoring fácil. Ao analisar números e datas, troque Age para int ou StartDate para time.Time e o compilador guia-o através de todas as correções.

Anexe cada row dentro employeeData dentro do loop de linha. A fatia está pronta para ser marshalizada assim que c.Visit retornar.

Exporte os resultados para JSON (e CSV como bónus)

JSON é o padrão adequado para APIs e serviços a jusante; CSV é o que as ferramentas de BI e os analistas desejam. Exportar ambos requer cerca de dez linhas adicionais.

import (
    "encoding/csv"
    "encoding/json"
    "log"
    "os"
)

content, err := json.MarshalIndent(employeeData, "", "  ")
if err != nil {
    log.Fatal(err)
}
if err := os.WriteFile("employees.json", content, 0644); err != nil {
    log.Fatal(err)
}

f, err := os.Create("employees.csv")
if err != nil {
    log.Fatal(err)
}
defer f.Close()
w := csv.NewWriter(f)
defer w.Flush()
_ = w.Write([]string{"Name", "Position", "Office", "Age", "StartDate", "Salary"})
for _, r := range employeeData {
    _ = w.Write([]string{r.Name, r.Position, r.Office, r.Age, r.StartDate, r.Salary})
}

Ambos os ficheiros ficam no seu diretório de trabalho. Manter ambos os formatos disponíveis para pipelines a jusante é um dos hábitos mais úteis ao aprender a extrair tabelas HTML em Golang.

Lidar com paginação e páginas múltiplas

A maioria das páginas com tabelas não cabe num único ecrã. Dois padrões cobrem a maioria dos casos.

Padrão A: Siga o link seguinte.

c.OnHTML("a.next", func(e *colly.HTMLElement) {
    if next := e.Request.AbsoluteURL(e.Attr("href")); next != "" {
        _ = e.Request.Visit(next)
    }
})

Padrão B: Repita um modelo de URL com o número da página.

for page := 1; page <= 20; page++ {
    _ = c.Visit(fmt.Sprintf("https://example.com/data?page=%d", page))
}

Combine qualquer um dos padrões com colly.LimitRule para limitar os pedidos e evitar sobrecarregar o servidor de origem:

_ = c.Limit(&colly.LimitRule{
    DomainGlob:  "*example.com*",
    Parallelism: 2,
    RandomDelay: 1500 * time.Millisecond,
})

Isso mantém o tráfego moderado e reduz a probabilidade de um erro 429 na página sete.

Evite ser bloqueado: proxies, cabeçalhos e novas tentativas

Assim que ultrapassar algumas centenas de pedidos, as defesas básicas anti-bot entram em ação. Uma lista de verificação independente de fornecedores sobre como extrair tabelas HTML em Golang em grande volume:

  1. Alterne os user agents. extensions.RandomUserAgent(c) Inserir um novo agente de utilizador em cada pedido.
  2. Limite o tráfego. colly.LimitRule com RandomDelay faz com que o tráfego pareça menos robótico.
  3. Repetir a tentativa em caso de erros transitórios. No OnError, verifique o código de estado e chame r.Request.Retry() para respostas 5xx e 429.
  4. Alterne os proxies. Passe uma lista para proxy.RoundRobinProxySwitcher e anexe-a via c.SetProxyFunc(...). Os conjuntos de IPs residenciais integram-se melhor do que os intervalos de centros de dados.
  5. Ajuste o transporte. Um http.Transport com um DialContext e MaxIdleConns reduz a rotatividade de conexões em alvos instáveis.
  6. Subcontrate quando deixar de ser divertido. Uma API de scraping gerida supera as horas de engenharia quando CAPTCHAs e fingerprinting se tornam o projeto. O nosso guia com dicas para evitar ser bloqueado durante o web scraping aprofunda este tema de uma perspetiva independente da linguagem.

E se a tabela for renderizada por JavaScript?

Abra a página com o JavaScript desativado. Se <tbody> estiver vazio na resposta HTML bruta, as linhas são inseridas pelo JS do lado do cliente e o Colly, por si só, não as verá. Duas opções:

  1. Navegador headless no processo. O chromedp controla uma instância real do Chrome a partir de Go, aguarda a renderização da tabela e entrega-lhe o DOM renderizado.
  2. API de renderização headless. Descarregue o navegador para um endpoint gerido que devolve HTML pós-JS e, em seguida, alimente esse HTML no Colly ou no goquery como habitualmente.

Juntando tudo: Scraper totalmente funcional

A versão mínima executável, pronta para um novo módulo:

package main

import (
    "encoding/csv"
    "encoding/json"
    "fmt"
    "log"
    "os"
    "strings"

    "github.com/gocolly/colly/v2"
)

type tableData struct {
    Name, Position, Office, Age, StartDate, Salary string
}

func main() {
    var rows []tableData
    c := colly.NewCollector()

    c.OnHTML("table#example > tbody", func(h *colly.HTMLElement) {
        h.ForEach("tr", func(_ int, el *colly.HTMLElement) {
            rows = append(rows, tableData{
                Name:      strings.TrimSpace(el.ChildText("td:nth-child(1)")),
                Position:  strings.TrimSpace(el.ChildText("td:nth-child(2)")),
                Office:    strings.TrimSpace(el.ChildText("td:nth-child(3)")),
                Age:       strings.TrimSpace(el.ChildText("td:nth-child(4)")),
                StartDate: strings.TrimSpace(el.ChildText("td:nth-child(5)")),
                Salary:    strings.TrimSpace(el.ChildText("td:nth-child(6)")),
            })
        })
    })

    if err := c.Visit("https://datatables.net/examples/styling/display.html"); err != nil {
        log.Fatal(err)
    }

    j, _ := json.MarshalIndent(rows, "", "  ")
    _ = os.WriteFile("employees.json", j, 0644)

    f, _ := os.Create("employees.csv")
    defer f.Close()
    w := csv.NewWriter(f)
    defer w.Flush()
    _ = w.Write([]string{"Name", "Position", "Office", "Age", "StartDate", "Salary"})
    for _, r := range rows {
        _ = w.Write([]string{r.Name, r.Position, r.Office, r.Age, r.StartDate, r.Salary})
    }
    fmt.Println("scraped:", len(rows), "rows")
}

Testado no Go 1.22 com o Colly v2 no momento da redação. Adicione o limite de taxa, o alternador de proxy e a extensão do agente do utilizador assim que passar da URL de demonstração. O nosso guia mais abrangente sobre web scraping com Go aborda a cadeia de ferramentas.

Conclusão e próximos passos

Agora tem o padrão completo de como fazer scraping de tabelas HTML em Golang: escolha a biblioteca certa, defina um seletor preciso, modele as linhas como uma estrutura, exporte para JSON e CSV e recorra ao chromedp ou à rotação de proxy apenas quando a página o exigir.

O próximo passo natural é a concorrência. Mude o seu coletor para o modo assíncrono com c.Async = true, levante Parallelism no seu colly.LimitRule, e chame c.Wait() depois do último c.Visit() para distribuir por várias páginas.

Quando o alvo se torna agressivo no bloqueio e preferes enviar o pipeline em vez de manter a infraestrutura de proxy, a nossa API Scraper na WebScrapingAPI devolve HTML renderizado por trás de um único endpoint, para que o código de análise Colly que escreveste hoje continue a funcionar.

Pontos-chave

  • Escolha a ferramenta adequada para a tarefa. O Colly v2 é a melhor opção para rastreamento e callbacks, o goquery é a escolha mais leve quando já tem HTML na memória, e golang.org/x/net/html é o recurso de fallback de baixo nível.
  • Restringe sempre o teu seletor a um <tbody>. Um seletor simples table geralmente captura a marcação de layout; table#id > tbody é o padrão seguro.
  • Modele as linhas como uma estrutura tipada, não como um mapa. As tags de estrutura fornecem chaves JSON estáveis e permitem que o compilador detecte erros de digitação antes da produção.
  • Envie JSON e CSV em conjunto. Ambos os formatos custam cerca de dez linhas adicionais e desbloqueiam tanto os fluxos de trabalho da API como os dos analistas.
  • Planeie os bloqueios com antecedência. Alterne os agentes de utilizador, limite o tráfego, tente novamente em 5xx e 429 e recorra a proxies ou a uma API gerida assim que o destino recusar.

Perguntas frequentes

Preciso do Colly para extrair tabelas HTML em Go, ou posso usar o goquery ou o net/html em vez disso?

Não, o Colly não é necessário. Use o goquery quando já tiver o HTML e precisar apenas de seleção CSS no estilo jQuery em um *goquery.Document. Recorra ao golang.org/x/net/html quando precisar de controlo ao nível do token. Escolha o Colly quando o rastreamento, a limitação de pedidos, os cookies e os ganchos de proxy o obrigariam, de outra forma, a reinventá-los.

Como exporto linhas de tabelas extraídas para CSV em Go em vez de JSON?

Use o pacote encoding/csv . Abra um ficheiro com os.Create, envolva-o em csv.NewWriter, escreva um cabeçalho com w.Write([]string{...}), depois percorra as suas estruturas de linha e chame w.Write por linha. Sempre defer w.Flush() e defer f.Close() para que o ficheiro seja gravado no disco.

Como posso extrair uma tabela que se estende por várias páginas paginadas com o Colly?

Dois padrões cobrem a maioria dos casos. Se a página exibir um link «Próximo», registe um OnHTML manipulador no seu seletor e chame e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href"))). Se as páginas seguirem um parâmetro de consulta numérico, construa a URL com fmt.Sprintf e faça um loop c.Visit. Combine qualquer um dos padrões com colly.LimitRule e RandomDelay para que as recuperações simultâneas sejam educadas.

Como posso extrair uma tabela HTML quando as linhas são renderizadas por JavaScript?

Renderize a página primeiro e, em seguida, analise-a. chromedp controla um Chrome headless real a partir do Go, permite-lhe WaitVisible no seletor de destino e retorna o DOM pós-JS que pode ser passado para o goquery. Se preferir ignorar as operações do navegador, envie a URL para uma API de renderização sem interface gráfica e analise o HTML devolvido com o Colly como se fosse qualquer página estática.

Como evito ser bloqueado ao extrair muitas páginas de dados tabulares em Go?

Crie camadas de defesa. Randomize os user agents com extensions.RandomUserAgent, limite o tráfego através de colly.LimitRule com RandomDelay, tente novamente respostas transitórias 5xx e 429 dentro de OnErrore alterne proxies residenciais através de proxy.RoundRobinProxySwitcher. Armazene respostas em cache durante o desenvolvimento para não ter de testar novamente contra a origem ativa. Se os CAPTCHAs se tornarem rotineiros, transfira a camada de pedidos para um ponto final de scraping gerido.

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.