当前位置:首页 > 新闻 > 正文

Go语言开发者如何用爬虫优雅地获取CBA篮球比分网实时数据

  • 新闻
  • 2026-09-02 18:15:59
  • 27
摘要: 说实话,我写这篇文章的起因挺偶然的,前阵子跟朋友聊起CBA联赛,他说每次想查比分都得打开好几个网页,烦得很,我当时正好在研究Go...

说实话,我写这篇文章的起因挺偶然的,前阵子跟朋友聊起CBA联赛,他说每次想查比分都得打开好几个网页,烦得很,我当时正好在研究Go语言的网络编程,就琢磨着——能不能用Go写个小工具,直接从cba篮球比分网抓数据?这一试,还真捣鼓出点门道来。

为什么偏偏选Go语言干这活?

你可能要问,Python写爬虫不香吗?确实,Python生态成熟,但Go有几个天生的优势特别对我的胃口。

  • 部署简单:编译出来就是一个二进制文件,扔到服务器上就能跑,不用配一堆依赖
  • 并发能力强:goroutine轻量得很,同时抓取多个页面的比分数据毫无压力
  • 标准库够用:net/http、encoding/json这些都是现成的,少装很多第三方包

我当时写了个小demo,从cba篮球比分网拉数据,整个代码也就七八十行,第一次跑起来的时候,看着终端里刷刷刷蹦出来的比分数字,那种成就感,嘿,真挺带劲的。

先摸清楚目标网站的结构

在动手写代码之前,有一件事特别重要——你得先搞清楚cba篮球比分网的数据是怎么组织的,我一开始太心急,直接上手就写正则表达式去匹配,结果网页改版后全废了。

后来学聪明了,先用Go写了个简单的HTTP客户端,把网页源码拉下来瞅了瞅,发现这网站的数据藏在特定的class里,

resp, err := http.Get("https://www.cba-basketball-scores.com/live")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body)[:5000]) // 先打印前5000字符看看结构

这里有个很容易踩的坑——headers必须要伪装,我一开始没设置User-Agent,直接被403了,加上这个就好使了:

req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")

正则还是CSS选择器?这是个问题

说到解析HTML,很多从Python转过来的朋友习惯用BeautifulSoup,Go这边呢,我试过几个库,比如goquery(和jQuery语法几乎一样)和colly(一个挺全面的爬虫框架)。

不过我之前写过一个更“笨”的方法——纯正则匹配,虽然不够优雅,但对于结构固定的页面反而意外地稳定,比如匹配比分数据:

re := regexp.MustCompile(`<span class="score">(\d+)\s*:\s*(\d+)</span>`)
matches := re.FindAllStringSubmatch(string(body), -1)
for _, m := range matches {
    fmt.Printf("比分: %s - %s\n", m[1], m[2])
}

对,我知道用正则解析HTML会被很多人吐槽,但怎么说呢,在快糙猛面前,谁用谁知道,不过要是你的需求比较复杂,那还是老老实实上goquery吧。

数据清洗,比想象中更磨人

你可能觉得,抓下来数据不就完事了?天真了,我拿到的第一份原始数据简直没法看——乱码、多余的空格、换行符混在一起。

这里有几个实操技巧:

  1. 统一编码:有的页面是UTF-8,有的搞GBK,你最好在读取body的时候顺手转一下
  2. 字符串清理:用strings.ReplaceAll干掉多余的空格和换行
  3. 时间格式化:CBA比赛时间格式挺乱,“19:35”和“2024-01-15 19:35”都有

我当时写了个小函数专门处理这个:

func cleanData(raw string) string {
    // 去掉多余的空白
    space := regexp.MustCompile(`\s+`)
    cleaned := space.ReplaceAllString(raw, " ")
    // 去掉首尾空格
    return strings.TrimSpace(cleaned)
}

动态加载的数据?那就得换个思路

我原以为cba篮球比分网是纯静态网页,直接拉HTML就完事了,结果发现了一个坑——部分比分数据是通过AJAX动态加载的

这个时候单纯用http.Get就不灵了,页面源码里根本就没有那些数字,那怎么办?三个办法试下来,我觉得各有优劣:

方法 优点 缺点 适用场景
分析XHR接口 最轻量、速度最快 需要抓包找接口规律 网站有开API或JSON
用chromedp模拟浏览器 啥都能抓 吃内存,慢 页面逻辑复杂
找隐藏接口拼参数 不碰前端 参数加密困难 签名规则简单

我最喜欢的是第一种——直接找数据接口,只需要用浏览器的开发者工具,在Network栏里看看比分数据是从哪个URL返回的,多半是个JSON文件,那Go解析起来不要太爽:

type TeamScore struct {
    TeamName string `json:"team_name"`
    Score    int    `json:"score"`
}
type MatchData struct {
    GameID  string      `json:"game_id"`
    Home    TeamScore   `json:"home"`
    Away    TeamScore   `json:"away"`
    Status  string      `json:"status"`
}

再配合上encoding/json,一手的结构化数据,哪像HTML里那么难抠。

别忽略了并发控制的细节

Go的goroutine真的是把双刃剑,你要是无脑开几百个goroutine去扒网站,很容易把对方服务器搞崩,自己的IP也可能被封。

我当时写了个简单的工作池模式:

var wg sync.WaitGroup
sema := make(chan struct{}, 5) // 最多同时5个请求
for _, matchURL := range matchURLs {
    wg.Add(1)
    go func(url string) {
        defer wg.Done()
        sema <- struct{}{} // 占用一个槽位
        fetchAndParse(url)
        <-sema // 释放
    }(matchURL)
}
wg.Wait()

这样既保证了速度,又不会太暴力,就像喝茶一样,一小口一小口地品,别一口闷。

存放数据,文件的尽头是数据库

刚开始我图省事,把比分数据直接写进了一个JSON文件,但后来发现不对劲——我要的是历史记录,每天抓一次,一个月下来文件就一万多行了,手动找数据得眼瞎。

后来换了思路,嵌入SQLite,Go这里有个go-sqlite3的包,虽然基于CGO有点烦人,但胜在稳定,你要是嫌CGO麻烦,就用modernc.org/sqlite,这是纯Go实现,不用装gcc。

存表结构大概这样:

CREATE TABLE IF NOT EXISTS cba_scores (
    game_id INTEGER PRIMARY KEY,
    match_date TEXT NOT NULL,
    home_team TEXT NOT NULL,
    away_team TEXT NOT NULL,
    home_score INTEGER,
    away_score INTEGER,
    status TEXT,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

最后说说我踩过的一些坑

我这人写代码挺毛躁的,一轮调试下来,确实遇到了不少糟心事,挑几个说说,你以后可能用得上:

  • 超时处理别忘了http.Client要设置Timeout,不然那边没响应你就永久卡住了
  • 重定向问题:有些页面会302跳转,Go的http.Client默认跟着跳,但有时候会丢失header
  • 随机User-Agent池:一个固定的ua用久了会被识别,准备个数组轮换着来
  • 程序中断怎么恢复:做个断点续传的思路,记录下已经抓过哪些比赛

这个项目我断断续续搞了一周多,如今跑在服务器上,每天自动抓取,中午和晚上推送比分到我手机,别人问我CBA打得咋样了,我瞟一眼手机就能聊两句,挺有意思的,代码这东西本来就是为了解决实际问题嘛,顺手拿来主义一把,倒也让看球这件事变得更舒坦了,你要是也爱打球又爱敲代码,不妨自己动手试试。

Go语言开发者如何用爬虫优雅地获取CBA篮球比分网实时数据