用Golang写了个360厄瓜多尔VS韩国视频直播爬虫?这事儿我真干过
- 技巧
- 2026-08-20 15:42:56
- 44
为什么是Golang?
你可能会奇怪,看个足球直播,跟Golang有什么关系?
上周四晚上,我窝在沙发里,手机架在茶几上,正准备看360体育的“厄瓜多尔VS韩国”视频直播,结果你猜怎么着?页面卡成PPT,弹幕刷屏刷到飞起,画质糊得我差点以为自己在看马赛克艺术展。
当时我脑子一热:要不,自己写个爬虫,把直播流地址扒下来,用VLC看高清?
然后我就真干了,用Golang,花了大概两小时,写出了个能用的东西,今天就把这事儿拆开聊聊,顺便安利一下Go语言在多线程抓取上的爽感。
为什么选Golang而不是Python?
我知道,说到爬虫,大家第一反应是Python,但这次我偏要用Go,理由有三:
- 并发是刻在骨子里的 —— 直播流解析往往要同时请求多个CDN节点,Go的goroutine写起来比Python的threading舒服太多了。
- 编译成单个二进制 —— 丢到服务器上就能跑,不需要装依赖,不像Python还得配venv。
- 性能确实顶 —— 解析M3U8切片的时候,Go的吞吐量比Python高一个量级,尤其在弱网环境下重试策略更稳。
我当时的需求很简单:从360体育的网页里,挖出隐藏的.m3u8直播流地址,然后丢给ffmpeg转码播放。
第一步:从网页里抠出视频流地址
打开360体育的直播页面,按F12,切到Network面板,刷新一下,你会发现一个典型的master.m3u8请求,这就是直播流的“总纲”。
但问题来了:这个URL是动态生成的,而且带着加密参数,直接复制粘贴能用,但要写成代码,就得逆向它的JS逻辑。
我当时偷了个懒,用了一个取巧的办法:在Go里调用Chrome DevTools Protocol,直接让无头浏览器帮我拿到渲染后的DOM,再从中提取iframe的src,代码大概长这样:
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var html string
err := chromedp.Run(ctx,
chromedp.Navigate("https://live.360.com/ecuador-vs-korea"),
chromedp.WaitVisible(`#player`, chromedp.ByID),
chromedp.OuterHTML(`#player`, &html, chromedp.ByID),
)
if err != nil {
log.Fatal(err)
}
// 从HTML里正则提取m3u8链接
// 这段代码略,因为360的地址格式天天变
fmt.Println(strings.Contains(html, "m3u8"))
}
说实话,当时跑通这一步的时候,我激动得差点把茶几上的可乐打翻。 因为这意味着,我可以用代码替代肉眼去“看”直播地址了。
第二步:处理防盗链和Referer校验
拿到.m3u8只是开始,360体育可不是吃素的,它的CDN节点会校验Referer和User-Agent,如果你直接用curl去拉master.m3u8,大概率返回403。
这时候,Golang的net/http就派上用场了,我写了个自定义Transport,带上浏览器一模一样的Headers:
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
}
req, _ := http.NewRequest("GET", m3u8URL, nil)
req.Header.Set("Referer", "https://live.360.com/")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
resp, err := client.Do(req)
这里有个小坑:有些CDN节点还会校验Origin头,而且这个值会随着你从哪个页面跳转过来而变化,我当时排查了半小时,最后发现是少了Origin头,加上就通了。
第三步:用goroutine池子拉切片
视频直播流在传输时,会切成一个个小的.ts文件,通常每2-6秒一个,为了流畅播放,你需要预缓存后面几个切片。
这里Go的goroutine简直不要太爽,我写了个简易的切片下载器:
func downloadSegment(url string, wg *sync.WaitGroup, sem chan struct{}) {
defer wg.Done()
sem <- struct{}{} // 限制并发数
defer func() { <-sem }()
// 实际下载逻辑,本地缓存到/tmp
}
控制并发数用带缓冲的channel,防止把家门口的路由器搞崩,当时我设置了8个并发,拉720P的切片,缓存时间大概能提前15秒左右,配合上ffplay播放,体验比网页端好太多。
第四步:从直播流到本地播放
拿到所有.ts片段后,最简单的办法是写一个临时的playlist.m3u8,把本地文件路径按顺序填进去,然后用VLC或者ffplay播放。
但更优雅的方式是:在Go里直接调用ffmpeg子进程,把流拼接并推给本地回环地址:
cmd := exec.Command("ffmpeg",
"-i", "pipe:0",
"-c", "copy",
"-f", "mpegts", "udp://127.0.0.1:1234",
)
stdin, _ := cmd.StdinPipe()
cmd.Start()
// 循环把下载的片段写入stdin
这时候我猛然发现,自己其实不是在写爬虫,而是在写一个迷你直播调度系统。 有点上头,又接着加了断线重连和自动切换清晰度的逻辑。
一些踩坑记录
- 360的签名算法变了好几次。 第一天能用,第三天就失效了,后来我改成模拟浏览器执行JS的方式,才稳定下来。
- 直播延迟问题。 网页端延迟大概在20秒左右,我抓下来的流延迟更高,接近35秒,因为要预缓存切片,这是没办法的事。
- 内存泄漏。 跑久了内存涨得厉害,后来发现是
http.Transport没复用连接,改用了sync.Pool才解决。
看球体验升级总结
那天晚上,我用自己写的Golang爬虫,成功把360厄瓜多尔VS韩国视频直播的画面,从模糊的浏览器窗口“救”到了本地播放器里,画质清晰不少,也不卡顿了。
虽然韩国队最后输了,但我一点也不亏——毕竟我学会了几件新东西:
- Golang的并发模型用在IO密集型任务上有多顺手。
- 反爬对抗的本质是模拟人的行为,而不是破解加密。
- 看球需要仪式感,而这种仪式感,有时候靠代码来实现更有意思。
这个爬虫现在估计已经失效了,360体育每次大版本更新都会改接口,但这不重要,重要的是,用代码解决生活小麻烦的那种痛快感,是真的会上瘾。
下次如果再有想看的比赛,我可能会再打开编辑器,重新写一遍那几百行Go代码,至于用什么语言?还用问吗,Golang,YYDS。
