北京vs鹏程视频直播回放,用Golang写个爬虫,把失去的精彩瞬间抓回来
- 技术
- 2026-08-27 17:44:08
- 73
为啥Golang特别适合搞视频回放抓取?
前两天朋友问我,说错过了北京队和鹏程队那场直播,到处找回放都找不到完整的,我笑了笑,跟他说:“这事儿啊,用咱Go语言写个爬虫,半小时搞定。”他一脸懵,但我是认真的——Golang的并发模型和高性能HTTP客户端,天生就是干这个的料。
你可能会问,为啥不用Python?Python当然也行,但Go有它独有的优势:
- 编译成单一可执行文件,丢到服务器上就能跑,不用装一堆依赖
- goroutine轻量级并发,同时抓几十个视频分片毫无压力
- 标准库net/http就够用,不用引入一堆第三方库
- 内存占用小,跑在2G内存的云主机上都绰绰有余
第一步:搞清楚视频回放从哪来
北京vs鹏程这场比赛的回放,常见来源有这么几个:
| 来源 | 特点 | 抓取难度 |
|---|---|---|
| 官方直播平台 | 画质好,但可能有防盗链 | |
| 第三方聚合站 | 来源杂,画质参差不齐 | |
| 论坛/网盘分享 | 可能不完整,但容易抓 |
大多数情况下,直播平台会把视频切成TS分片文件(MPEG-TS格式),通过m3u8索引文件来按顺序播放。这个m3u8文件就是我们爬虫的钥匙。
第二步:写个最基本的m3u8下载器
咱们别整那些虚的,直接上代码,下面这个例子假设你已经从浏览器开发者工具里找到了m3u8的地址(Networking面板,筛选media类型):
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
)
func fetchM3U8(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var segments []string
for _, line := range strings.Split(string(body), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "http") {
segments = append(segments, line)
}
}
return segments, nil
}
func downloadSegment(url string, wg *sync.WaitGroup, index int) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("第%d个分片下载失败: %v\n", index, err)
return
}
defer resp.Body.Close()
out, err := os.Create(fmt.Sprintf("seg_%04d.ts", index))
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
fmt.Printf("第%d个分片写入失败: %v\n", index, err)
}
}
func main() {
m3u8URL := "替换成实际的m3u8地址"
segments, err := fetchM3U8(m3u8URL)
if err != nil {
fmt.Println("获取m3u8失败:", err)
return
}
fmt.Printf("共发现%d个分片\n", len(segments))
var wg sync.WaitGroup
for i, seg := range segments {
wg.Add(1)
go downloadSegment(seg, &wg, i)
}
wg.Wait()
fmt.Println("所有分片下载完成")
}
这段代码已经能并发下载所有TS分片了。注意并发数——别一口气开500个goroutine,你的路由器会抗议的,建议用个带缓冲的channel,限制同时下载数量在10个左右。
第三步:把分片合成一个完整的视频文件
分片下载完了,还得合并,这事儿在Linux上直接:
cat seg_*.ts > beijing_vs_pengcheng_full.ts
但在Windows上,命令行就不那么爽了,咱用Go合并:
func mergeSegments(pattern string, output string) error {
out, err := os.Create(output)
if err != nil {
return err
}
defer out.Close()
for i := 0; ; i++ {
fname := fmt.Sprintf(pattern, i)
if _, err := os.Stat(fname); os.IsNotExist(err) {
break
}
data, err := os.ReadFile(fname)
if err != nil {
return err
}
if _, err := out.Write(data); err != nil {
return err
}
}
return nil
}
这样匹配seg_0000.ts, seg_0001.ts...这样的文件顺序合并。合并时注意顺序,有的分片命名不是从0开始,或者有跳号的情况,最好先把文件名解析出来排个序。
第四步:处理防盗链和加密问题
这边要泼点冷水:很多直播平台的m3u8不是直接能访问的,可能会遇到这些情况:
- Referer检查——需要带上来源页面地址
- User-Agent伪装——别用Go的默认UA(一眼假)
- AES-128加密——m3u8里会有
#EXT-X-KEY:METHOD=AES-128,URI="key.key"这样的标记
对于加密的分片,你得先下载key文件,再用crypto/aes包解密每个分片:
func decryptSegment(data []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(data) % aes.BlockSize != 0 {
return nil, fmt.Errorf("数据长度不是块大小的整数倍")
}
iv := make([]byte, aes.BlockSize) // 根据实际情况,IV可能在m3u8里给出
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(data))
mode.CryptBlocks(decrypted, data)
return decrypted, nil
}
第五步:加个断点续传,免得中途挂了从头来
看比赛回放经常是十几分钟到几个小时,视频文件大,分片多,网络不稳定时,断点续传是刚需,咱可以在下载前先检查本地是否已有同名文件:
- 文件存在且大小不为0,跳过(或者校验哈希)
- 文件不完整(比如下载了一半),重新下载
- 记录已下载的分片索引到
progress.json,下次启动时读取
func loadProgress() map[string]bool {
data, err := os.ReadFile("progress.json")
if err != nil {
return make(map[string]bool)
}
var progress map[string]bool
json.Unmarshal(data, &progress)
return progress
}
func saveProgress(progress map[string]bool) {
data, _ := json.Marshal(progress)
os.WriteFile("progress.json", data, 0644)
}
那场北京vs鹏程,我最后看到了吗?
回到开头那个朋友的问题——我用这套Golang爬虫,花了个把小时(大部分时间在等下载),真的抓到了完整的回放,那个晚上,我泡了杯茶,把错过的第四节和加时赛,从头看到尾。鹏程队那记压哨三分,我还反复放了好几遍。
技术这东西,本质上不就是为了让人能更方便地看到自己想看的内容吗?从m3u8解析到TS合并,从防盗链到解密,每一步都碰了点壁,但都过来了,你要是也遇到过这种想看回放却找不到资源的时刻,不妨搭个Go环境,照着上面这段逻辑,自己动手写一个,反正代码又不复杂,跑起来稳得很——就像那场比赛的最后两分钟,心跳加速但终究没崩。
