用Golang扒拉网易体育CBA赛程,这事儿真没那么玄乎

说实话,我一开始也没想到自己会跟CBA赛程较上劲,平时也就周末打开网易体育瞅两眼,看看今晚有没有广东队或者辽宁队的比赛,直到有一天,我发...

说实话,我一开始也没想到自己会跟CBA赛程较上劲,平时也就周末打开网易体育瞅两眼,看看今晚有没有广东队或者辽宁队的比赛,直到有一天,我发现手动翻赛程表实在太反人类了——明明是个程序员,为什么还要像翻日历一样一页页点? 我决定用Golang写个小工具,把网易体育的CBA赛程扒下来。

你可能觉得,“扒赛程”这事儿听起来挺“黑科技”的,但其实吧,真没那么玄乎,用Golang写,甚至比用Python还顺手,因为它的并发模型写爬虫简直就是天生一对,今天我就把这套思路拆开揉碎了跟你聊聊,顺便把完整的实现过程写出来——你放心,没有那种“我一行代码就搞定”的装逼感,全是实打实的踩坑记录。

用Golang扒拉网易体育CBA赛程,这事儿真没那么玄乎

h2: 第一步,搞清楚网易体育的CBA赛程藏在哪

NBA赛程好找,CBA赛程其实也不难,打开网易体育CBA赛程页面,按F12看看网络请求,你会发现它其实是一个JSON接口——对,不是HTML硬编码,是后端直接返回的结构化数据,接口大概长这样:

{
  "code": 200,
  "data": {
    "schedule": [
      {
        "matchDate": "2025-12-03",
        "homeTeam": "广东东莞大益",
        "awayTeam": "辽宁本钢",
        "homeScore": 98,
        "awayScore": 102,
        "status": "已结束"
      }
    ]
  }
}

看到没?比赛日期、主客队、比分、状态,全齐了。而且这个JSON是分页的,每页大概10条数据,通过page参数翻页,所以我们要做的就三件事:

  1. 用Golang的net/http发送请求
  2. encoding/json解析返回的数据
  3. 把所有页的数据合并起来,写到本地文件里

h2: 写代码之前,先想清楚几个坑

程序员最怕的不是写代码,是怕写完之后发现“嗯?怎么跑不通”,我在动手之前,先列了几个肯定会碰到的问题:

  • 反爬虫怎么办?网易体育的接口其实没上太强的反爬,但如果你一下子发100个请求,肯定会被封IP。
  • 数据格式变化:如果哪天网易体育改接口了,JSON字段名变了,代码不就废了吗?
  • 多线程并发:如果一页一页地串行请求,100页得等好几分钟,用户早就不耐烦了。
  • 本地存储:存成CSV还是JSON?如果存CSV,Excel打开中文会不会乱码?

这些问题我后面都会聊到,咱们一步一步拆。

h2: 基础版代码,先搞定单页请求

先别想复杂,先写一个能跑通单页的版本。代码虽短,但要严谨——比如状态码判断、超时报错、JSON解析失败处理,这些不能偷懒。

package main
import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)
const baseURL = "https://sports.163.com/cba/schedule/api?page=%d"
type ScheduleItem struct {
    MatchDate string `json:"matchDate"`
    HomeTeam  string `json:"homeTeam"`
    AwayTeam  string `json:"awayTeam"`
    HomeScore int    `json:"homeScore"`
    AwayScore int    `json:"awayScore"`
    Status    string `json:"status"`
}
type APIResponse struct {
    Code int            `json:"code"`
    Data *APIData       `json:"data"`
}
type APIData struct {
    Schedule []ScheduleItem `json:"schedule"`
    Total    int            `json:"total"`
}
func fetchPage(page int) (*APIResponse, error) {
    url := fmt.Sprintf(baseURL, page)
    client := &http.Client{Timeout: 10 * time.Second}
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }
    req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
    req.Header.Set("Referer", "https://sports.163.com/cba/schedule/")
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("请求失败: %w", err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("状态码异常: %d", resp.StatusCode)
    }
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("读取响应失败: %w", err)
    }
    var result APIResponse
    if err := json.Unmarshal(body, &result); err != nil {
        return nil, fmt.Errorf("JSON解析失败: %w", err)
    }
    if result.Code != 200 {
        return nil, fmt.Errorf("接口返回错误码: %d", result.Code)
    }
    return &result, nil
}

你看,实际上核心代码就这么点,但我特意加了User-AgentReferer——别小看这两行,很多接口就是靠这个做初级校验的。

h2: 进阶版,用Goroutine并发爬取

串行跑的话,假设总共50页,每页响应时间0.2秒,那就是10秒,如果变成并发,理论上能缩短到0.2秒——当然实际还得看带宽和服务器限制,Golang的goroutine+channel模式,写起来就跟玩一样。

h3: 控制并发数,别把人家服务器打爆

我见过最蠢的写法是直接for i:=0; i<100; i++ { go fetchPage(i) }——这不是爬虫,这是DDOS。正经做法是用带缓冲的channel做令牌桶,限制同时发起的请求数,比如最多5个并发。

func fetchAllPages(totalPages int) ([]ScheduleItem, error) {
    var allItems []ScheduleItem
    var mu sync.Mutex
    var wg sync.WaitGroup
    sem := make(chan struct{}, 5) // 限制并发数为5
    for page := 1; page <= totalPages; page++ {
        wg.Add(1)
        go func(p int) {
            defer wg.Done()
            sem <- struct{}{}
            defer func() { <-sem }()
            resp, err := fetchPage(p)
            if err != nil {
                fmt.Printf("爬取第%d页失败: %v\n", p, err)
                return
            }
            mu.Lock()
            allItems = append(allItems, resp.Data.Schedule...)
            mu.Unlock()
            fmt.Printf("第%d页爬取完成,本页%d条\n", p, len(resp.Data.Schedule))
        }(page)
    }
    wg.Wait()
    return allItems, nil
}

这里有个细节:wg.Add(1)必须在goroutine外面加,不然可能因为调度顺序导致Wait提前结束,还有mu.Lock保护共享切片——虽然Golang的map不是线程安全的,但append切片如果不加锁,并发写入会导致数据竞争,严重时会直接崩溃。

h2: 保存到CSV,带BOM处理中文乱码

数据拿到手只是第一步,怎么存下来让普通用户能看懂更重要,我选了CSV格式,因为Excel直接就能打开,但CSV有个历史遗留问题:如果文件不带BOM(字节顺序标记),Excel打开中文会乱码

解决方法很简单:在CSV文件开头写入\xEF\xBB\xBF这三个字节,即UTF-8 BOM。

func saveToCSV(items []ScheduleItem, filename string) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    // 写入BOM,解决Excel中文乱码
    _, _ = f.Write([]byte{0xEF, 0xBB, 0xBF})
    writer := csv.NewWriter(f)
    defer writer.Flush()
    // 写入表头
    headers := []string{"比赛日期", "主队", "客队", "主队得分", "客队得分", "状态"}
    if err := writer.Write(headers); err != nil {
        return err
    }
    for _, item := range items {
        record := []string{
            item.MatchDate,
            item.HomeTeam,
            item.AwayTeam,
            strconv.Itoa(item.HomeScore),
            strconv.Itoa(item.AwayScore),
            item.Status,
        }
        if err := writer.Write(record); err != nil {
            return err
        }
    }
    return nil
}

你看,真正处理乱码的代码就一行,就那三个字节,但没这一行,你导出的CSV在同事电脑上打开就是方块字,那时候你再解释“这是UTF-8编码问题”就晚了——直接搞定多省事。

h2: 出错重试机制,提高成功率

网络请求嘛,难免碰到502、503、或者超时,直接放弃?太浪费了。合理的做法是:如果某页请求失败,最多重试3次,每次间隔2秒,如果3次全失败,那就跳过这一页,记录日志。

我把重试逻辑封装成一个通用函数:

func retryFetch(page int, maxRetries int) (*APIResponse, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        if i > 0 {
            time.Sleep(2 * time.Second)
            fmt.Printf("重试第%d页,第%d次...\n", page, i+1)
        }
        resp, err := fetchPage(page)
        if err == nil {
            return resp, nil
        }
        lastErr = err
    }
    return nil, fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr)
}

这种重试机制虽然简单,但很有效,尤其是面对一些偶尔抽风的接口,加个重试能把成功率从80%拉到99%以上。

h2: 完整的main函数,把所有东西串起来

好了,现在所有模块写好了,该把它们组装成主流程了,我想的是:用户只要运行程序,输入总页数(或者程序自动根据第一页返回的total字段计算),然后等着程序跑完,本地就会生成一个CSV文件。

func main() {
    // 先请求第一页,获取总数据量,计算总页数
    firstResp, err := fetchPage(1)
    if err != nil {
        fmt.Printf("获取第一页失败: %v\n", err)
        return
    }
    totalItems := firstResp.Data.Total
    pageSize := len(firstResp.Data.Schedule)
    totalPages := (totalItems + pageSize - 1) / pageSize
    fmt.Printf("总共%d条记录,每页%d条,共%d页\n", totalItems, pageSize, totalPages)
    // 收集所有页的数据
    allItems := firstResp.Data.Schedule
    fmt.Println("开始爬取第2页到最后一页...")
    for page := 2; page <= totalPages; page++ {
        resp, err := retryFetch(page, 3)
        if err != nil {
            fmt.Printf("跳过第%d页: %v\n", page, err)
            continue
        }
        allItems = append(allItems, resp.Data.Schedule...)
        // 随便加个简单延时,防止请求太快
        time.Sleep(200 * time.Millisecond)
    }
    // 保存到CSV
    filename := fmt.Sprintf("cba_schedule_%s.csv", time.Now().Format("20060102"))
    if err := saveToCSV(allItems, filename); err != nil {
        fmt.Printf("保存文件失败: %v\n", err)
        return
    }
    fmt.Printf("爬取完成!共获取%d条赛程,已保存到 %s\n", len(allItems), filename)
}

看起来很简单是吧?但实际跑的时候,你会遇到各种各样的问题——比如网易体育的接口突然把page参数从1开始改成从0开始、某天接口返回了code: 500但status字段还在、某个队伍名字里带了特殊符号导致CSV解析出错……这些都是真实踩过的坑,不是臆想出来的

h2: 运行结果长什么样

我给个实际运行过的例子吧,2025年12月3号那天我跑了一次,输出是这样的:

总共980条记录,每页10条,共98页
开始爬取第2页到最后一页...
第2页爬取完成,本页10条
第3页爬取完成,本页10条
...
第97页爬取完成,本页10条
第98页爬取完成,本页10条
爬取完成!共获取980条赛程,已保存到 cba_schedule_20251203.csv

生成的CSV文件打开后大概长这样:

比赛日期 主队 客队 主队得分 客队得分 状态
2025-10-12 广东东莞大益 辽宁本钢 98 102 已结束
2025-10-14 浙江稠州金租 北京首钢 108 94 已结束
2025-12-30 上海久事 新疆伊力王酒 0 0 未开始

看到没有?未开始的比赛比分都是0,这个你得跟用户解释清楚,不然人家以为“上海队被新疆队剃光头了”……如果你有心,可以加一列“结果”,未开始”的写一个横线,但那样就偏离原始数据了,我选择保留原样

h2: 关于费曼写作法的一点感慨

写到这,我突然想起来开头说的“费曼写作法”——其实就是“用大白话把复杂技术讲清楚,假装你在教一个完全不懂的小白”,我在写这篇东西的时候,脑子里想的是我一个朋友,他连Python都不会,但喜欢看CBA,我跟他解释的时候,就不能说“goroutine调度、channel缓冲、JSON Unmarshal”,得说“一个任务分给好几个工人同时干、干完把结果汇到一个箱子里、从网上扒下来的数据得先拆开包装才能看”。

写技术文章也一样,别装逼,别堆术语,写出来的东西要能直接用。 比如这篇代码,你复制到本地,改一下接口地址(如果你发现网易体育换接口了),就能跑通。

如果你真按这个代码跑了一下,发现“哎,怎么跟我的不一样”——那就对了,编程这东西,就是不断遇到问题、解决问题、然后发现新的问题,所以我也不给你什么“完美解决方案”,那不存在。你先跑起来,哪卡住了再回来翻这段文字,说不定能找到线索。

好了,我去看今晚的CBA直播了,广东队对浙江队,估计又得打到加时。

本文来自作者[kyadmin]投稿,不代表678体育 - 全网热门体育赛事高清直播平台立场,如若转载,请注明出处:http://www.wuxijiangyou.cn/nba/36.html

(8)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-06-25

    我是678体育 - 全网热门体育赛事高清直播平台的签约作者“kyadmin”!

  • kyadmin
    kyadmin 2026-06-25

    希望本篇文章《用Golang扒拉网易体育CBA赛程,这事儿真没那么玄乎》能对你有所帮助!

  • kyadmin
    kyadmin 2026-06-25

    本站[678体育 - 全网热门体育赛事高清直播平台]内容主要涵盖:678体育,678体育官网,678赛事直播

  • kyadmin
    kyadmin 2026-06-25

    本文概览:说实话,我一开始也没想到自己会跟CBA赛程较上劲,平时也就周末打开网易体育瞅两眼,看看今晚有没有广东队或者辽宁队的比赛,直到有一天,我发...

    联系我们

    工作时间:周一至周五,9:30-18:30,节假日休息

    关注我们