用Golang写一个12nba总决赛数据爬虫,我踩过的那些坑

说实话,我一开始也是冲着“12nba总决赛”这几个字去的,作为一个老球迷,从2012年热火对雷霆那轮系列赛看到现在,每年总决赛我都想自己...

说实话,我一开始也是冲着“12nba总决赛”这几个字去的,作为一个老球迷,从2012年热火对雷霆那轮系列赛看到现在,每年总决赛我都想自己动手拉点数据玩玩,但以前做这事儿要么靠Excel手动粘贴,要么找现成的网站凑合看看,总觉得缺点什么,直到我开始用Golang写了个小工具,事情才变得有意思起来。

为什么选Golang干这活?

你可能觉得,爬个比赛数据而已,Python不香吗?确实香,但Golang有几个让我这个“懒人”没法拒绝的理由:

  • 编译成单个可执行文件:写好之后扔服务器上就能跑,不用装Python环境、不用管依赖冲突,我上次在树莓派上跑,直接scp过去就完事儿。
  • 并发是原生技能:我要爬12nba总决赛的历年数据,每场比赛的详细统计、球员数据、甚至每节比分,用goroutine一把梭,速度不是快一点点。
  • 标准库够用net/httpencoding/jsonregexp,基本不用装第三方包就能搭起来,哪怕你要解析HTML,golang.org/x/net/html也够稳。

但话说回来,实际动手的时候,还是被现实狠狠教育了几回。

第一步:目标锁定——12nba总决赛到底指什么?

如果你搜“12nba总决赛”,大多数情况指的是2012年NBA总决赛——迈阿密热火对阵俄克拉荷马城雷霆,那一年勒布朗·詹姆斯拿到了生涯第一个总冠军,杜兰特还在雷霆,威少还是那个愣头青。

但“12”也可能被理解为12月份的比赛,或者某个网站的编号,我决定稳妥起见,我自己写的小工具需要支持模糊匹配,把用户输入的“12nba总决赛”拆解成“2012”“NBA总决赛”“NBA Finals 2012”等关键词,然后去多个数据源拉取。

核心代码长什么样?

我直接给你看一个最简版本的核心结构,这玩意儿不是我一次性写成的,是改了四五遍之后才觉得“嗯,能用了”。

用Golang写一个12nba总决赛数据爬虫,我踩过的那些坑

package main
import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)
// 定义比赛数据结构
type Game struct {
    Date        string `json:"date"`
    HomeTeam    string `json:"home_team"`
    AwayTeam    string `json:"away_team"`
    HomeScore   int    `json:"home_score"`
    AwayScore   int    `json:"away_score"`
    Series      string `json:"series"`
}
// 这是我最开始写的版本,后来发现返回数据格式不统一,又加了一层
type FinalsData struct {
    Year   int     `json:"year"`
    Games  []Game  `json:"games"`
    Champion string `json:"champion"`
}

说实话,一开始我没用json标签,结果爬回来的数据字段名全是首字母大写,跟网站给的不匹配,后来老老实实加上标签,数据才正常解析,这种小问题我起码折腾了半小时。

接着是核心的爬取函数

func fetchFinalsData(year int) (FinalsData, error) {
    url := fmt.Sprintf("https://api.example.com/nba/finals/%d", year)
    resp, err := http.Get(url)
    if err != nil {
        return FinalsData{}, fmt.Errorf("请求失败: %v", err)
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return FinalsData{}, fmt.Errorf("读取响应失败: %v", err)
    }
    var data FinalsData
    if err := json.Unmarshal(body, &data); err != nil {
        return FinalsData{}, fmt.Errorf("解析JSON失败: %v", err)
    }
    return data, nil
}

这个函数看起来简单,但实际用的时候,你会发现有些网站返回的数据格式跟文档不一样,比如有的把champion写成champ,有的年份字段叫season而不是year,这时候就要根据实际情况做字段映射,我写了个小工具表来处理:

来源网站 年份字段名 冠军字段名 比赛列表字段名
Site A year winner games
Site B season champ matches
Site C y c g

后来我干脆写了个通用的适配器模式,每个数据源实现一个接口,这样就灵活多了。

并发爬取:看起来很美,但别踩坑

因为我要爬的是12nba总决赛,除了2012年,我还想顺便拉2011、2013的数据做个对比(反正代码写都写了),这时候并发就派上用场了:

func CrawlMultipleYears(years []int) []FinalsData {
    ch := make(chan FinalsData, len(years))
    for _, year := range years {
        go func(y int) {
            data, err := fetchFinalsData(y)
            if err != nil {
                fmt.Printf("获取%d年数据失败: %v\n", y, err)
                return
            }
            ch <- data
        }(year)
    }
    var results []FinalsData
    for i := 0; i < len(years); i++ {
        results = append(results, <-ch)
    }
    return results
}

这里有个超级容易踩的坑:goroutine中的循环变量捕获问题,如果你写成go func() { ... year }()而不是go func(y int) { ... y }(),你爬回来的数据会全是一样的,别问我怎么知道的,我debug了一整个下午才发现问题。

并发虽好,但别太贪心,我一开始直接开了30个goroutine去爬历年总决赛,结果被目标服务器封了IP,后来加了限流器,用time.Sleep或者rate.Limiter控制请求频率,才稳下来。

import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(10, 1) // 每秒最多10个请求
func fetchWithRateLimit(url string) (*http.Response, error) {
    err := limiter.Wait(context.Background())
    if err != nil {
        return nil, err
    }
    return http.Get(url)
}

数据解析:是不是每场比赛都要?

爬回来的JSON里,12nba总决赛的数据大概长这样:

[
  {
    "game_id": 1,
    "date": "2012-06-12",
    "home": "OKC",
    "away": "MIA",
    "score": "105-94"
  },
  ...
]

但如果你想看每节比分球员技术统计犯规次数,那就得深入到单场比赛的详情页去抓了,我当时纠结了一下:要不要爬?

最后我决定:只爬用户最关心的核心数据,对于12年总决赛,大多数人想知道的无非是:

  • 每场比赛的比分
  • 谁是最后冠军
  • 场均得分最高的球员
  • 系列赛MVP是谁

这些数据一般在外层列表就有,没必要再深入,除非用户明确要求“我要看詹姆斯那轮系列赛的场均助攻数”,那再单独开一个goroutine去查。

输出:数据到手了,然后呢?

数据爬下来之后,我没急着存数据库,而是先打印出来看了看:

func PrintFinalsSummary(data FinalsData) {
    fmt.Printf("=== %d年NBA总决赛 ===\n", data.Year)
    fmt.Printf("冠军: %s\n", data.Champion)
    fmt.Println("比赛详情:")
    for i, game := range data.Games {
        fmt.Printf("第%d场: %s %d - %d %s (日期: %s)\n", 
            i+1, game.AwayTeam, game.AwayScore, game.HomeScore, game.HomeTeam, game.Date)
    }
}

输出结果长这样:

=== 2012年NBA总决赛 ===
冠军: Miami Heat
比赛详情:
第1场: MIA 94 - 105 OKC (日期: 2012-06-12)
第2场: MIA 100 - 96 OKC (日期: 2012-06-14)
第3场: OKC 85 - 91 MIA (日期: 2012-06-17)
第4场: OKC 98 - 104 MIA (日期: 2012-06-19)
第5场: MIA 121 - 106 OKC (日期: 2012-06-21)

看到这个输出的时候,我是真的挺激动的,虽然这些数据网上随便查都有,但自己动手爬下来的感觉完全不一样,我能看到原始JSON里字段是怎么命名的,能看到响应时间,能感受到每个接口返回数据的细微差别。

写都写了,顺便搞个HTML报告

既然Golang的模板引擎那么好用,我干脆把结果渲染成一个简单的HTML页面,这样看起来更直观:

import "html/template"
const tmpl = `
<h1>{{.Year}}年NBA总决赛</h1>
<p>冠军: <strong>{{.Champion}}</strong></p>
<table>
    <tr>
        <th>场次</th>
        <th>客队</th>
        <th>比分</th>
        <th>主队</th>
        <th>日期</th>
    </tr>
    {{range $i, $game := .Games}}
    <tr>
        <td>{{add $i 1}}</td>
        <td>{{$game.AwayTeam}}</td>
        <td><strong>{{$game.AwayScore}} - {{$game.HomeScore}}</strong></td>
        <td>{{$game.HomeTeam}}</td>
        <td>{{$game.Date}}</td>
    </tr>
    {{end}}
</table>
`
func RenderHTML(data FinalsData) string {
    t, _ := template.New("report").Funcs(template.FuncMap{
        "add": func(a, b int) int { return a + b },
    }).Parse(tmpl)
    var buf bytes.Buffer
    t.Execute(&buf, data)
    return buf.String()
}

这个模板我第一次写的时候忘了加FuncMap,结果add函数不生效,场次全显示0,后来加上了,页面正常了。

生成的HTML直接保存成文件,用浏览器打开就能看,如果你不想手动打开,也可以用exec.Command("open", "report.html")自动弹出(macOS上管用)。

一点小感想

写这个12nba总决赛爬虫的过程中,我最大的感受是:写代码和看球赛其实挺像的,看着数据从接口里一点点流出来,就像看比赛一点点推进,你会遇到突发状况(有些网站用反爬机制,有些字段文档不全),但回头解决掉之后,那种成就感跟看到喜欢的球队夺冠差不多。

我的这个小工具里还留着不少“不完美”的地方:比如错误处理不够精细,有些年份数据会少一场(因为被拒绝访问了),日志输出也比较随意,但说实话,能用就行,哪天想优化了,再改呗。

如果你也想写一个类似的工具,建议从最简单的开始——就爬一个年份的数据,打印出来看看,然后慢慢加上并发、输出格式、容错处理,别一上来就想搞个全自动全历史的爬虫,那太容易把人劝退了。

好了,我代码还跑着,先去喝口水。

本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.yikaoba.cn/nba/584.html

(2)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-07-06

    我是be365的签约作者“kyadmin”!

  • kyadmin
    kyadmin 2026-07-06

    希望本篇文章《用Golang写一个12nba总决赛数据爬虫,我踩过的那些坑》能对你有所帮助!

  • kyadmin
    kyadmin 2026-07-06

    本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名

  • kyadmin
    kyadmin 2026-07-06

    本文概览:说实话,我一开始也是冲着“12nba总决赛”这几个字去的,作为一个老球迷,从2012年热火对雷霆那轮系列赛看到现在,每年总决赛我都想自己...

    联系我们

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

    关注我们