Volver al blog
Guías
Andrei OgiolanLast updated on May 7, 202612 min read

Cómo raspar tablas HTML en Golang con Colly: Guía de principio a fin

Cómo raspar tablas HTML en Golang con Colly: Guía de principio a fin
En resumen: Esta guía explica cómo extraer datos de tablas HTML en Golang de principio a fin: elige entre Colly, goquery y golang.org/x/net/html, selecciona las <tbody>, modelar las filas como una estructura tipada y exportar JSON y CSV limpios. También obtendrás patrones de paginación, anti-bloqueo y tablas renderizadas con JavaScript.

Si alguna vez has intentado alimentar un HTML <table> en un almacén de Postgres o un CSV para analistas, los datos están ahí mismo en el DOM, pero extraerlos de forma fiable es un pequeño proyecto en sí mismo. Esta guía explica cómo extraer datos de tablas HTML en Golang de una manera que funcione en páginas reales, no solo en tutoriales limpios.

Una tabla HTML es una cuadrícula estructurada de filas (<tr>) y celdas (<td> o <th>). Extraerla significa analizar el marcado, recorrer esos elementos y convertir cada fila en un registro tipado que tu código pueda utilizar más adelante. En Go tienes tres opciones serias: Colly, goquery y el golang.org/x/net/html. Veremos cuándo es adecuado cada uno y luego crearemos un scraper funcional basado en Colly v2.

Aprenderás a inspeccionar una página en DevTools, escribir un selector CSS preciso, modelar filas como una estructura, exportar tanto a JSON como a CSV, y gestionar la paginación, la renderización de JavaScript y los bloqueos antibots. Al final, tendrás un patrón listo para copiar y pegar sobre cómo extraer datos de tablas HTML en Golang.

Por qué merece la pena aprender a extraer datos de tablas HTML en Golang

Los datos tabulares aparecen por todas partes: páginas de precios, estadísticas deportivas, informes financieros, conjuntos de datos públicos que nunca tuvieron una API real. Si tu proceso comienza con <table> el marcado y termina en un almacén o un cuaderno, necesitas una forma fiable de extraer esos datos. Go se compila en un único binario, gestiona bien la concurrencia y ofrece un rendimiento predecible a gran escala. Saber cómo extraer datos de tablas HTML en Golang significa implementar ese proceso como un servicio autónomo, sin necesidad de un entorno de ejecución de Python.

Cuándo usar Colly, goquery o net/html

Si eliges la biblioteca equivocada, pasarás más tiempo luchando con la API que analizando filas. Aquí tienes una matriz de decisión rápida.

Biblioteca

Ideal para

Evítala cuando

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

Rastrear muchas páginas con callbacks de ciclo de vida (OnRequest, OnHTML, OnError), cookies, limitación de velocidad, ganchos de proxy

Ya tienes una cadena HTML en memoria y no necesitas conexión a la red

goquery (github.com/PuerkitoBio/goquery)

Selección CSS al estilo jQuery en un *goquery.Document ya has recuperado

También necesitas rastreo, limitación de velocidad y configuración de proxy

golang.org/x/net/html

Recorrido de tokens y nodos de bajo nivel cuando el CSS no es suficiente

Puedes expresar lo que quieres en CSS; goquery requiere tres veces menos código

El hilo de Stack Overflow, de larga duración, sobre el análisis de tablas HTML en Go sigue apareciendo en los resultados de esta búsqueda, y sus respuestas principales apuntan a goquery y x/net/html. Ambos son sólidos. Colly los combina con la ergonomía de rastreo que querrás tener en cuanto tengas más de una página que visitar.

Configura tu proyecto Go e instala Colly

Crea un módulo y descarga 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

Fíjate en el /v2 . La importación original github.com/gocolly/colly importación es la línea v1, y la mayoría de los tutoriales antiguos aún la mencionan. Los proyectos nuevos deben usar la v2 para las correcciones de errores actuales y la compatibilidad con los módulos de Go.

Añade una comprobación de integridad main.go:

package main

import "fmt"

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

Ejecuta go run main.go. Si ves scraper booted, la cadena de herramientas está configurada y Colly está en go.sum. A partir de aquí, cada fragmento de código sustituye el cuerpo de main o añade un tipo a nivel de paquete.

Examina la tabla de destino antes de escribir código

Antes de escribir en Go, abre la página de destino en tu navegador y traza la tabla que desees. Usaremos la demostración de DataTables en https://datatables.net/examples/styling/display.html como ejemplo práctico. Haz clic con el botón derecho en la tabla, selecciona «Inspeccionar» y confirma tres cosas:

  1. El selector. Busca un id (la demostración utiliza #example) o única. Evita table solo, ya que las páginas suelen envolver el diseño en elementos de tabla anidados.
  2. Estructura del encabezado. Confirma <thead> y <tbody> están separados. Si no es así, te saltarás la primera fila en el código.
  3. Estático frente a dinámico. Desactiva JavaScript y vuelve a cargar la página. Si las filas desaparecen, la tabla se renderiza en el lado del cliente. Trataremos esa rama más adelante.

Cinco minutos en DevTools valen más que una hora depurando un fragmento vacío. Nuestra hoja de referencia de selectores CSS recoge los patrones que más utilizan los rastreadores de tablas.

Configurar el Collector y las callbacks de Colly

Colly's Collector es el objeto central: emite solicitudes y envía callbacks del ciclo de vida. Considera los cuatro callbacks siguientes como código repetitivo que puedes copiar en todos los proyectos.

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 se activa antes de cada llamada de red, OnResponse cuando el servidor responde, y OnError detecta respuestas que no sean 2xx y errores de transporte, que es donde la mayoría de los rastreadores de producción fallan silenciosamente. A continuación, añadiremos OnHTML a continuación la llamada de retorno donde se lleva a cabo el análisis de la tabla.

Selecciona la tabla con un selector CSS preciso

En la demostración de DataTables, al ejecutar document.querySelectorAll('table') en la consola del navegador devuelve más de una coincidencia porque el marcado de diseño en otras partes también utiliza elementos de tabla. Seleccionar table por sí solo extraería las filas equivocadas, así que comprueba siempre los selectores en la consola antes de escribir el código Go.

El selector fiable aquí es table#example > tbody. Se limita a una sola tabla mediante id y omite el <thead> , por lo que no es necesario eliminar manualmente la fila de encabezado. El widget DataTables también inserta filas de encabezado y pie de página duplicadas; restringir a > tbody las mantiene fuera de tu conjunto de datos.

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

OnHTML coincide con elementos mediante un selector CSS y llama al controlador para cada coincidencia. Cambia #example por lo que te muestre DevTools. Si estás sopesando CSS frente a XPath, nuestra comparación de selectores XPath vs CSS cubre las ventajas y desventajas.

Recorre las filas y extrae cada celda

Dentro del OnHTML controlador, llama a h.ForEach("tr", ...) y extrae cada celda con 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)
    })
})

Las celdas de las tablas HTML casi nunca contienen class o id , por lo que nth-child(n) es la forma más limpia de abordar las columnas. Si la página reorganiza las columnas, solo tienes que cambiar un número por campo en lugar de reescribir tu analizador.

Un patrón más resistente consiste en leer <thead> primero, crear un map[string]int índice de nombres de columna y buscar las celdas por la etiqueta del encabezado. Vale la pena el código adicional si la fuente reorganiza las columnas. Envuelve siempre el texto en strings.TrimSpace y analiza las columnas de moneda o fecha con strconv y time.Parse antes de la serialización, para que los consumidores no obtengan cadenas como "$320,800" cuando esperaban números.

Modela la fila con una estructura y un segmento de Go

Define el tipo de fila a nivel de paquete para que las etiquetas JSON se mantengan:

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

¿Por qué una estructura tipada en lugar de map[string]string? Tres razones:

  1. Claves JSON estables. Las etiquetas de la estructura controlan los nombres de los campos y las mayúsculas y minúsculas en la salida, en lugar de heredar lo que hayas escrito durante el análisis.
  2. Seguridad en tiempo de compilación. Los errores tipográficos impiden la compilación, en lugar de generar silenciosamente valores vacíos que te causan problemas en el entorno de staging.
  3. Refactorizaciones sencillas. Al analizar números y fechas, cambia Age por int o StartDate por time.Time y el compilador te guía a través de cada corrección.

Añade cada row dentro employeeData dentro del bucle de filas. La sección está lista para ser marshalizada una vez que c.Visit devuelva un resultado.

Exporta los resultados a JSON (y a CSV como extra)

JSON es el formato predeterminado adecuado para las API y los servicios posteriores; CSV es lo que buscan las herramientas de BI y los analistas. Generar ambos archivos requiere unas diez líneas adicionales.

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 archivos terminan en tu directorio de trabajo. Mantener ambos formatos disponibles para los procesos posteriores es uno de los hábitos más útiles a la hora de aprender a extraer datos de tablas HTML en Golang.

Gestionar la paginación y las páginas múltiples

La mayoría de las páginas con tablas no caben en una sola pantalla. Hay dos patrones que cubren la mayoría de los casos.

Patrón A: Seguir el siguiente enlace.

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

Patrón B: Recorrer una plantilla de URL con número de página.

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

Combina cualquiera de los patrones con colly.LimitRule para limitar las solicitudes y evitar saturar el servidor de origen:

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

Esto mantiene el tráfico dentro de unos límites razonables y reduce la probabilidad de un error 429 en la página siete.

Evita que te bloqueen: proxies, encabezados y reintentos

Una vez que superas unos cientos de solicitudes, se activan las defensas básicas contra los bots. Una lista de verificación independiente del proveedor sobre cómo extraer tablas HTML en Golang a gran escala:

  1. Rota los agentes de usuario. extensions.RandomUserAgent(c) Inserta un agente de usuario nuevo en cada solicitud.
  2. Limita el ancho de banda. colly.LimitRule con RandomDelay hace que el tráfico parezca menos robótico.
  3. Reintentar en caso de errores transitorios. Dentro de OnError, comprueba el código de estado y llama a r.Request.Retry() para respuestas 5xx y 429.
  4. Rota los proxies. Pasa una lista a proxy.RoundRobinProxySwitcher y adjúntala mediante c.SetProxyFunc(...). Los grupos de IP residenciales se camuflan mejor que los rangos de centros de datos.
  5. Ajuste el transporte. Una configuración personalizada http.Transport con un DialContext y ajustado MaxIdleConns , reduce la rotación de conexiones en objetivos inestables.
  6. Externaliza cuando deje de ser divertido. Una API de scraping gestionada supera las horas de ingeniería una vez que los CAPTCHAs y el fingerprinting se convierten en el proyecto. Nuestra guía de consejos para evitar ser bloqueado al hacer web scraping profundiza en esto desde un punto de vista independiente del lenguaje.

¿Y si la tabla se renderiza con JavaScript?

Abre la página con JavaScript desactivado. Si <tbody> está vacío en la respuesta HTML sin procesar, las filas son inyectadas por JS del lado del cliente y Colly por sí solo no las verá. Dos opciones:

  1. Navegador sin interfaz gráfica en proceso. chromedp controla una instancia real de Chrome desde Go, espera a que se renderice la tabla y te entrega el DOM renderizado.
  2. API de renderizado sin interfaz gráfica. Descarga el navegador a un punto final gestionado que devuelva HTML post-JS y, a continuación, introduce ese HTML en Colly o goquery como de costumbre.

Poniendo todo junto: un scraper totalmente funcional

La versión mínima ejecutable, lista para un nuevo 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")
}

Probado en Go 1.22 con Colly v2 en el momento de escribir este artículo. Incorpora el límite de velocidad, el cambiador de proxy y la extensión de agente de usuario una vez que hayas pasado de la URL de demostración. Nuestra guía más amplia sobre el scraping web con Go cubre la cadena de herramientas.

Conclusión y próximos pasos

Ahora tienes el patrón completo para extraer tablas HTML en Golang: elige la biblioteca adecuada, define un selector preciso, modela las filas como una estructura, exporta a JSON y CSV, y recurre a chromedp o a la rotación de proxies solo cuando la página lo requiera.

El siguiente paso lógico es la concurrencia. Cambia tu recopilador al modo asíncrono con c.Async = true, genera Parallelism en tu colly.LimitRule, y llama a c.Wait() después del último c.Visit() para distribuirlo entre muchas páginas.

Cuando el objetivo se vuelve agresivo a la hora de bloquear y prefieres enviar el pipeline en lugar de mantener la infraestructura de proxy, nuestra API de scraper en WebScrapingAPI devuelve el HTML renderizado detrás de un único punto final, por lo que el código de análisis de Colly que has escrito hoy sigue funcionando.

Conclusiones clave

  • Adapta la herramienta al trabajo. Colly v2 es la mejor opción para el rastreo y las devoluciones de llamada, goquery es la opción más ligera cuando ya tienes HTML en memoria, y golang.org/x/net/html es la alternativa de bajo nivel.
  • Limita siempre tu selector a un <tbody>. Un selector table selector suele capturar el marcado de diseño; table#id > tbody es el valor predeterminado seguro.
  • Modela las filas como una estructura tipada, no como un mapa. Las etiquetas de estructura te proporcionan claves JSON estables y permiten que el compilador detecte errores tipográficos antes de la producción.
  • Envía JSON y CSV juntos. Ambos formatos suponen unas diez líneas adicionales y habilitan tanto los flujos de trabajo de la API como los de los analistas.
  • Planifica los bloqueos con antelación. Alterna los agentes de usuario, limita el tráfico, reintenta en caso de errores 5xx y 429, y recurre a proxies o a una API gestionada si el destino devuelve un error.

Preguntas frecuentes

¿Necesito Colly para extraer tablas HTML en Go, o puedo usar goquery o net/html en su lugar?

No, Colly no es necesario. Usa goquery cuando ya tengas el HTML y solo necesites selección CSS al estilo jQuery en un *goquery.Document. Recurre a golang.org/x/net/html cuando necesites control a nivel de token. Elige Colly cuando el rastreo, la limitación de tráfico, las cookies y los hooks de proxy te obligarían a reinventarlos de otro modo.

¿Cómo exporto las filas de la tabla extraída a CSV en Go en lugar de a JSON?

Utiliza el paquete encoding/csv . Abre un archivo con os.Create, envuélvelo en csv.NewWriter, escribe un encabezado con w.Write([]string{...}), luego recorre tus estructuras de filas y llama a w.Write por fila. Siempre defer w.Flush() y defer f.Close() para que el archivo se guarde en el disco.

¿Cómo puedo extraer una tabla que abarca varias páginas paginadas con Colly?

Hay dos patrones que cubren la mayoría de los casos. Si la página muestra un enlace «Siguiente», registra un OnHTML controlador en su selector y llama a e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href"))). Si las páginas siguen un parámetro de consulta numérico, construye la URL con fmt.Sprintf y recorre c.Visit. Combina cualquiera de estos patrones con colly.LimitRule y RandomDelay para que las recuperaciones simultáneas se mantengan correctas.

¿Cómo puedo extraer una tabla HTML cuando las filas se generan mediante JavaScript?

Primero renderiza la página y luego analízala. chromedp ejecuta un Chrome sin interfaz gráfica real desde Go, te permite WaitVisible en el selector de destino y devuelve el DOM post-JS que puedes introducir en goquery. Si prefieres saltarte las operaciones del navegador, envía la URL a una API de renderizado sin interfaz gráfica y analiza el HTML devuelto con Colly como si fuera cualquier página estática.

¿Cómo evito que me bloqueen al extraer muchas páginas de datos tabulares en Go?

Aplica varias capas de defensa. Aleatoriza los agentes de usuario con extensions.RandomUserAgent, limita el ancho de banda mediante colly.LimitRule con RandomDelay, reintenta las respuestas transitorias 5xx y 429 dentro de OnError, y alterna proxies residenciales mediante proxy.RoundRobinProxySwitcher. Almacena en caché las respuestas durante el desarrollo para no volver a realizar pruebas contra el origen en producción. Si los CAPTCHAs se convierten en algo habitual, descarga la capa de solicitudes a un punto final de scraping gestionado.

Acerca del autor
Andrei Ogiolan, Desarrollador Full Stack @ WebScrapingAPI
Andrei OgiolanDesarrollador Full Stack

Andrei Ogiolan es desarrollador full stack en WebScrapingAPI, donde colabora en todas las áreas del producto y ayuda a crear herramientas y funciones fiables para la plataforma.

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.