返回博客
指南
Andrei OgiolanLast updated on May 7, 20263 min read

如何使用 Colly 在 Golang 中抓取 HTML 表格:端到端指南

如何使用 Colly 在 Golang 中抓取 HTML 表格:端到端指南
简而言之:本指南详细介绍了如何在 Go 语言中从头到尾抓取 HTML 表格:在 Colly、goquery 和 golang.org/x/net/html,定位正确的 <tbody>,将行数据建模为类型化结构体,并导出干净的 JSON 和 CSV 文件。此外,本指南还涵盖分页、防阻塞以及 JavaScript 渲染的表格处理模式。

如果你曾尝试将 HTML <table> 导入 Postgres 数据仓库或生成供分析师使用的 CSV 文件,你会发现数据明明就在 DOM 中,但要可靠地提取出来却是一项独立的小工程。本指南将详细讲解如何使用 Go 语言抓取 HTML 表格,确保方案不仅适用于干净的教程页面,更能经受住真实网页的考验。

HTML 表格是由行(<tr>)和单元格(<td><th>)组成的结构化网格。抓取它意味着解析标记、遍历这些元素,并将每一行转换为代码后续可用的类型化记录。在 Go 中,你有三种主要选项:Colly、goquery 以及更底层的 golang.org/x/net/html。我们将探讨每种方案的适用场景,并围绕 Colly v2 构建一个可运行的爬取器。

你将学会如何在开发者工具中检查页面、编写精确的 CSS 选择器、将行建模为结构体、导出 JSON 和 CSV 格式,以及处理分页、JavaScript 渲染和防机器人封锁。课程结束时,你将掌握一套可直接复制粘贴的 Go 语言 HTML 表格抓取方案。

为何值得花时间学习用 Go 语言抓取 HTML 表格

表格数据无处不在:定价页面、体育数据、财务报表,以及那些从未真正提供API的公共数据集。如果你的数据处理流程始于 <table> 标记语言开始,最终存储于数据仓库或笔记本中,您就需要一种可靠的方法来提取这些数据。Go 语言编译后生成单一二进制文件,能很好地处理并发,并在大规模应用时提供可预测的性能。掌握如何用 Go 语言抓取 HTML 表格,意味着可以将该数据管道作为独立服务部署,无需依赖 Python 运行时环境。

何时使用 Colly、goquery 还是 net/html

选错库,你将耗费更多时间与 API 搏斗,而非解析数据行。以下是一个快速决策矩阵。

最适合

何时应跳过

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

需要通过生命周期回调爬取大量页面(OnRequest, OnHTML, OnError)、Cookie、速率限制、代理钩子

内存中已有 HTML 字符串且无需网络通信

goquery (github.com/PuerkitoBio/goquery)

*goquery.Document 你已经获取了

你还需要爬取、限流和代理配置

golang.org/x/net/html

当 CSS 不够用时,需要低级别的标记和节点遍历

你可以用 CSS 表达需求;goquery 的代码量仅需三分之一

关于在 Go 中解析 HTML 表格的那个长期活跃的 Stack Overflow 讨论帖至今仍能搜索到,其热门回答指向 goquery 和 x/net/html。两者都很可靠。Colly 将它们与爬虫的易用性结合在一起,一旦你需要访问多个页面,这些功能就会派上用场。

配置 Go 项目并安装 Colly

创建模块并拉取 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

请注意 /v2 后缀。原始的 github.com/gocolly/colly 导入语句是 v1 版本,大多数旧教程仍引用该版本。新项目应使用 v2 以获取最新的 bug 修复和 Go 模块支持。

添加一个基本检查 main.go:

package main

import "fmt"

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

运行 go run main.go。若看到 scraper booted,说明工具链已正确配置,且 Colly 位于 go.sum。从这里开始,每个代码片段都会替换 main 的正文,或添加一个包级类型。

编写代码前请检查目标表格

在编写 Go 代码之前,请在浏览器中打开目标页面并定位所需的表格。我们将以 https://datatables.net/examples/styling/display.html 上的 DataTables 演示为例进行说明。右键点击表格,选择“检查”,并确认以下三点:

  1. 选择器。查找一个稳定的 id (该演示使用 #example) 或唯一的类。避免 table 单独使用,因为页面通常会将布局包裹在嵌套的表格元素中。
  2. 表头结构。确认 <thead><tbody> 是否分离。若未分离,代码中将跳过第一行。
  3. 静态与动态。禁用 JavaScript 并刷新页面。如果行消失,则该表格由客户端渲染。我们稍后会处理这一分支。

在开发者工具中花五分钟,胜过花一小时调试一个空切片。我们的 CSS 选择器速查表列出了表格抓取工具最常用的模式。

连接 Colly 的收集器和回调

Colly的 Collector 是核心对象:它负责发起请求并分发生命周期回调。请将下方的四个回调视为模板代码,可复制到每个项目中。

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 在每次网络请求前触发, OnResponse 服务器响应时,以及 OnError 捕获非 2xx 响应和传输错误——这正是大多数生产环境爬虫会无声失败的地方。接下来我们将添加 OnHTML 接下来,即实际进行表格解析的回调。

使用精确的 CSS 选择器定位表格

在 DataTables 演示页面上,运行 document.querySelectorAll('table') 在浏览器控制台中会返回多个匹配结果,因为其他地方的布局标记也使用了 table 元素。仅选择 table 仅此一项会导致抓取错误的行,因此在编写 Go 代码前,请务必先在控制台中验证选择器。

此处的可靠选择器是 table#example > tbody。通过 id ,并跳过 <thead> ,因此无需手动剔除表头行。DataTables 组件还会插入镜像的表头和表尾行;通过限定为 > tbody 可将其排除在数据集之外。

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

OnHTML 通过 CSS 选择器匹配元素,并对每个匹配项调用处理程序。将 #example 为开发工具中显示的任意内容。若您正在权衡 CSS 与 XPath,我们的《XPath 与 CSS 选择器对比》文章详细阐述了二者之间的取舍。

遍历行并提取每个单元格

OnHTML 处理程序中,调用 h.ForEach("tr", ...) 并使用 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)
    })
})

HTML 表格单元格几乎从不包含稳定的 classid 属性,因此 nth-child(n) 是处理列的最简洁方式。如果页面重新排列了列,你只需为每个字段更改一个数字,而无需重写解析器。

一种更具弹性的模式是读取 <thead> ,构建一个 map[string]int 列名到索引表,并通过表头标签查找单元格。如果数据源重新排列了列,这额外的代码是值得的。始终将文本包裹在 strings.TrimSpace ,并使用 strconvtime.Parse ,这样消费者就不会收到像 "$320,800"

使用 Go 结构体和切片建模行

在包级别定义行类型,以便 JSON 标签随其一同传递:

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

为何选择带类型的结构体而非 map[string]string?有三个原因:

  1. 稳定的 JSON 键。结构体标签控制输出中的字段名称和大小写,而不是在解析时继承你输入的内容。
  2. 编译时安全性。拼写错误会导致编译失败,而非默默生成空值,从而在预发布环境中给你带来麻烦。
  3. 轻松重构。当您解析数字和日期时,只需将 AgeintStartDatetime.Time ,编译器会引导你完成每个修复步骤。

将每个解析后的 rowemployeeData 行循环内部。一旦 c.Visit 返回后,该切片即可进行序列化。

将结果导出为 JSON(并附赠 CSV 格式)

JSON是API和下游服务的理想默认格式;CSV则是BI工具和分析师所需要的。同时输出这两种格式只需额外十行代码。

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})
}

这两个文件最终都会保存在您的工作目录中。在学习如何用 Go 语言抓取 HTML 表格时,为下游管道保留这两种格式是极有用的习惯之一。

处理分页与多页

大多数包含表格的页面无法在一屏内显示。两种模式可覆盖绝大多数情况。

模式 A:点击下一页链接。

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

模式 B:循环遍历页码 URL 模板。

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

将任一模式与 colly.LimitRule 以限制请求频率,避免对源服务器造成过载:

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

这能确保流量行为得体,并降低在第七页收到 429 状态码的概率。

避免被封锁:代理、请求头与重试

一旦请求量超过几百次,基本的反机器人防御机制就会启动。以下是一份供应商中立的检查清单,用于在 Go 语言中批量抓取 HTML 表格:

  1. 轮换用户代理。 extensions.RandomUserAgent(c) 在每次请求中使用新的用户代理。
  2. 限流。 colly.LimitRule 配合 RandomDelay 使流量看起来不那么像机器人。
  3. 对临时错误进行重试。OnError内部,检查状态码并调用 r.Request.Retry() 处理 5xx 和 429 响应。
  4. 轮换代理。将列表传递给 proxy.RoundRobinProxySwitcher ,并通过 c.SetProxyFunc(...)。住宅IP池比数据中心IP范围更易混入网络环境。
  5. 调整传输协议。自定义 http.Transport ,并设置 60-90 秒的 DialContext 超时,并配合经过优化的 MaxIdleConns 可减少对不稳定目标的连接波动。
  6. 当工作不再有趣时,不妨外包。一旦项目主要涉及验证码和指纹识别,使用托管式爬取API比投入工程开发时间更划算。我们关于“如何避免在网页爬取中被封禁”的指南,从语言无关的角度对此进行了更深入的探讨。

如果表格由 JavaScript 渲染怎么办?

请在禁用 JavaScript 的情况下打开页面。如果 <tbody> 原始 HTML 响应中为空,则表示行数据由客户端 JS 注入,仅靠 Colly 无法识别。有两种方案:

  1. 进程内无头浏览器。chromedp 通过 Go 语言驱动真实的 Chrome 实例,等待表格渲染完成,并向您提供渲染后的 DOM。
  2. 无头渲染 API。将浏览器操作转移至一个受控的端点,该端点返回经过 JavaScript 处理后的 HTML,随后像往常一样将该 HTML 输入到 Colly 或 goquery 中。

整合所有内容:完整的可运行抓取器

可运行的最小版本,已准备好用于新模块:

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")
}

本文撰写时已在 Go 1.22 搭配 Colly v2 环境下测试通过。当您不再仅限于演示 URL 时,请逐步集成速率限制、代理切换器及用户代理扩展功能。我们关于使用 Go 进行网页抓取的全面指南涵盖了相关工具链。

结论与后续步骤

现在您已掌握在 Golang 中抓取 HTML 表格的完整流程:选择合适的库,锁定精确的筛选器,将行建模为结构体,导出为 JSON 和 CSV,仅在页面要求时才使用 chromedp 或代理轮换。

接下来的自然步骤是并发处理。使用 c.Async = true,并抛出 Parallelism ,并在 colly.LimitRule中抛出,并在 c.Wait() 在最后一个 c.Visit() 调用,以在多个页面上进行扇出。

当目标网站开始积极进行封锁,而你更倾向于直接输出数据流而非维护代理基础设施时,WebScrapingAPI 上的 Scraper API 会通过单一接口返回渲染后的 HTML,这样你今天编写的 Colly 解析代码就能继续正常运行。

关键要点

  • 因地制宜选择工具。Colly v2 在爬取和回调方面表现优异,goquery 最适合已将 HTML 加载到内存中的场景,而 golang.org/x/net/html 则是低级别的备选方案。
  • 始终将选择器限定为 <tbody>一个简单的 table 选择器通常会捕获布局标记; table#id > tbody 是安全的默认选择。
  • 将行数据建模为带类型的结构体,而非哈希表。结构体标签能提供稳定的 JSON 键,并让编译器在投入生产前捕获拼写错误。
  • 同时提供 JSON 和 CSV 格式。这两种格式仅需额外十行代码,却能同时支持 API 和分析师的工作流程。
  • 尽早规划阻塞处理。轮询用户代理、实施速率限制、对 5xx 和 429 状态码进行重试,一旦目标端出现阻塞,应立即使用代理或托管 API。

常见问题

在 Go 中抓取 HTML 表格是否必须使用 Colly,还是可以使用 goquery 或 net/html 替代?

不需要,Colly并非必需。当您已拥有HTML内容,且仅需对 *goquery.Document。当您需要令牌级别的控制时,请使用 golang.org/x/net/html 。若需对标记进行精细控制,请选用 goquery。当爬取、限流、Cookie 及代理钩子等功能会迫使您重新实现时,请选择 Colly。

如何在 Go 中将抓取的表格行导出为 CSV 而不是 JSON?

使用标准库中的 encoding/csv 包。使用 os.Create,将其封装在 csv.NewWriter,使用 w.Write([]string{...}),然后遍历行结构体并调用 w.Write 。务必 defer w.Flush()defer f.Close() ,这样文件就会保存到磁盘上。

如何使用 Colly 抓取跨越多个分页的表格?

两种模式可覆盖大多数情况。如果页面提供了“下一页”链接,请在其选择器上注册一个 OnHTML 处理程序,并调用 e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href")))。如果页面采用数字查询参数的形式,则使用 fmt.Sprintf 并循环 c.Visit。将任一模式与 colly.LimitRuleRandomDelay ,以确保并发请求保持礼貌。

当行由 JavaScript 渲染时,如何抓取 HTML 表格?

先渲染页面,然后进行解析。 chromedp 通过 Go 驱动真正的无头 Chrome,允许你 WaitVisible 目标选择器,并返回经过 JavaScript 处理后的 DOM,你可以将其作为参数传入 goquery。如果你更希望跳过浏览器操作,可以将 URL 发送给无头渲染 API,并使用 Colly 解析返回的 HTML,就像处理任何静态页面一样。

在 Go 中抓取多页表格数据时,如何避免被封禁?

分层部署防御措施。使用 extensions.RandomUserAgent,并通过 colly.LimitRule ,并使用 RandomDelay,在 OnError,并通过 proxy.RoundRobinProxySwitcher轮换住宅代理。开发期间缓存响应,避免对生产源服务器重复测试。若验证码(CAPTCHA)已成为常态,请将请求层卸载至托管抓取端点。

关于作者
Andrei Ogiolan, 全栈开发工程师 @ WebScrapingAPI
Andrei Ogiolan全栈开发工程师

安德烈·奥吉奥兰(Andrei Ogiolan)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。