说实话,我一开始也没想到自己会跟CBA赛程较上劲,平时也就周末打开网易体育瞅两眼,看看今晚有没有广东队或者辽宁队的比赛,直到有一天,我发现手动翻赛程表实在太反人类了——明明是个程序员,为什么还要像翻日历一样一页页点? 我决定用Golang写个小工具,把网易体育的CBA赛程扒下来。
你可能觉得,“扒赛程”这事儿听起来挺“黑科技”的,但其实吧,真没那么玄乎,用Golang写,甚至比用Python还顺手,因为它的并发模型写爬虫简直就是天生一对,今天我就把这套思路拆开揉碎了跟你聊聊,顺便把完整的实现过程写出来——你放心,没有那种“我一行代码就搞定”的装逼感,全是实打实的踩坑记录。

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参数翻页,所以我们要做的就三件事:
- 用Golang的
net/http发送请求 - 用
encoding/json解析返回的数据 - 把所有页的数据合并起来,写到本地文件里
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-Agent和Referer——别小看这两行,很多接口就是靠这个做初级校验的。
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
评论列表(4条)
我是678体育 - 全网热门体育赛事高清直播平台的签约作者“kyadmin”!
希望本篇文章《用Golang扒拉网易体育CBA赛程,这事儿真没那么玄乎》能对你有所帮助!
本站[678体育 - 全网热门体育赛事高清直播平台]内容主要涵盖:678体育,678体育官网,678赛事直播
本文概览:说实话,我一开始也没想到自己会跟CBA赛程较上劲,平时也就周末打开网易体育瞅两眼,看看今晚有没有广东队或者辽宁队的比赛,直到有一天,我发...