用Go语言写个爬虫,把365大佬第2季视频给扒下来(附完整代码)
- 体育
- 2026-08-04 04:58:43
- 77
说实话,我一开始听到“365大佬第2季视频”这词儿,第一反应是——这又是个啥付费课程?结果一查,好家伙,是个讲365天不间断输出内容的实战教程,但问题是,视频不给你下载链接,你只能在线看,这就很烦了,对吧?尤其是我这种地铁上没信号还想缓存下来看的,所以我琢磨着,用Go写个下载器,把这活儿干利索了。
先别急着写代码,搞清楚视频藏在哪儿
你得知道,这种平台(比如某课、某站)的视频资源,通常不是直接一个.mp4链接扔给你的,它往往是先给你一个.m3u8文件,里面写着一堆.ts分片文件的地址,你真正的任务,是把这堆.ts碎片下下来,再拼成一个完整的视频。
这儿有个关键点:你得拿到那个.m3u8的URL,怎么拿?两个办法:
- 浏览器F12抓包:打开开发者工具,切到Network(网络)标签,过滤
m3u8,然后播放视频,就能看到一个.m3u8的请求,右键复制它的地址。 - 用Go直接模拟请求:如果你知道视频页面的URL,可以分析它的网页源码,看有没有内嵌的JSON配置里面藏着视频地址,这一招需要点运气,因为很多平台现在都改成动态加载了,直接去源码里扒往往扑空。
我这儿先假设你已经通过抓包拿到了那个https://xxx.xxx.com/playlist.m3u8,下面咱们直接用Go来处理。
第一步:用Go拉取m3u8文件
先别管分片,我们先把.m3u8拿到手,这里有个小坑:很多服务器的.m3u8文件是带Key或者Token校验的,所以你请求的时候,得把浏览器里的User-Agent和Referer带上,不然大概率给你返回403。
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func fetchM3U8(url string) ([]byte, error) {
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// 伪装成浏览器,不然有些服务器会拒绝
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Referer", "https://qian.lu/") // 换成你实际的来源页面
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP错误: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func main() {
m3u8URL := "你的m3u8地址" // 抓包拿到的链接
content, err := fetchM3U8(m3u8URL)
if err != nil {
fmt.Printf("拉取失败: %v\n", err)
return
}
fmt.Printf("拿到了m3u8,字节数:%d\n", len(content))
// 先打印出来看看结构
fmt.Println(string(content[:500])) // 只打印前面部分
}
跑一下这段,你会看到类似这样的输出:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
segment0.ts
#EXTINF:10.0,
segment1.ts
...
好,看到这些segment0.ts就对了,这些就是视频的分片,但注意,这些segment0.ts是相对路径,你得拼上你.m3u8文件所在的目录才能完整请求到,你的.m3u8链接是https://abc.com/video/playlist.m3u8,那分片地址就是https://abc.com/video/segment0.ts。
第二步:解析m3u8,提取分片列表
别用正则硬抠,太累,咱们就按行读,遇到以#EXTINF开头的行,它的下一行就是分片文件名,这里有个细节,有些m3u8是加密的(带#EXT-X-KEY),但咱们先处理不加密的,加密的后面再提一嘴。
import (
"bufio"
"strings"
)
func parseM3U8(content []byte, baseURL string) ([]string, error) {
var segments []string
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// 这一行就是分片路径
if strings.HasPrefix(line, "http") {
segments = append(segments, line)
} else {
// 相对路径,拼接baseURL
segments = append(segments, baseURL+"/"+line)
}
}
return segments, scanner.Err()
}
注意,baseURL你得自己算,可以用net/url包解析出你.m3u8链接的目录部分:
import "net/url" u, _ := url.Parse(m3u8URL) baseURL := u.Scheme + "://" + u.Host + u.Path[0:strings.LastIndex(u.Path, "/")]
这样拿到完整的分片地址列表,不出意外的话,会有几百个.ts文件,这就是视频的本体。
第三步:并发下载所有分片(别嫌多,Go就是干这个的)
分片数多,但每个分片不大(几MB到几十MB),咱们直接并发拉,这里要小心——别把服务器冲垮,也别被平台封IP,设置个并发上限,比如5个协程同时干活。
import (
"sync"
"os"
"path/filepath"
)
func downloadSegment(url string, dir string, wg *sync.WaitGroup) {
defer wg.Done()
// 生成文件名,用URL里的最后一段
filename := filepath.Base(url)
filepath := filepath.Join(dir, filename)
// 下之前先判一下存在不存在,省得重复下载(断点续传的思想)
if _, err := os.Stat(filepath); err == nil {
fmt.Printf("已存在,跳过: %s\n", filename)
return
}
resp, err := http.Get(url)
if err != nil {
fmt.Printf("下载失败 %s: %v\n", url, err)
return
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取失败 %s: %v\n", url, err)
return
}
err = os.WriteFile(filepath, data, 0644)
if err != nil {
fmt.Printf("写入失败 %s: %v\n", filename, err)
return
}
fmt.Printf("完成: %s (%d KB)\n", filename, len(data)/1024)
}
然后主函数里:
func main() {
// ... 前面拿到 segments ...
os.MkdirAll("video_cache", 0755) // 建个缓存目录
var wg sync.WaitGroup
sem := make(chan struct{}, 5) // 并发控制,最多5个同时下
for _, segURL := range segments {
wg.Add(1)
sem <- struct{}{} // 占位
go func(u string) {
defer func() { <-sem }() // 释放
defer wg.Done()
downloadSegment(u, "video_cache", &wg)
}(segURL)
}
wg.Wait()
fmt.Println("所有分片下载完毕")
}
跑完后,你的video_cache目录里应该躺着一堆segment0.ts、segment1.ts…… 这时候,别急着合并,先检查一下有没有下载失败的文件(字节数为0或者只有几十字节的),我这儿写了个简易检查:
// 检查所有文件大小,小于1K的删掉重下
filepath.Walk("video_cache", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && info.Size() < 1024 {
fmt.Printf("删除疑似失败文件: %s (%d bytes)\n", path, info.Size())
os.Remove(path)
}
return nil
})
第四步:合并所有.ts为完整视频
这一步用的是ffmpeg,虽然咱们是Go程序,但调用外部工具不算作弊,实际上你可以用Go写个二进制合并,但ffmpeg更省事,它还会重新编码一下,保证播放流畅。
import "os/exec"
func mergeSegments(output string) error {
// 先生成一个文件列表,ffmpeg需要这个
listFile, err := os.Create("filelist.txt")
if err != nil {
return err
}
defer os.Remove("filelist.txt") // 用完删除
// 按文件名排序,确保顺序对
files, _ := filepath.Glob("video_cache/segment*.ts")
// 这里需要数字排序,所以用sort.Slice
sort.Slice(files, func(i, j int) bool {
numI := extractNumber(files[i])
numJ := extractNumber(files[j])
return numI < numJ
})
for _, f := range files {
fmt.Fprintf(listFile, "file '%s'\n", strings.ReplaceAll(f, "'", `'\''`))
}
listFile.Close()
cmd := exec.Command("ffmpeg", "-f", "concat", "-safe", "0", "-i", "filelist.txt", "-c", "copy", output)
outputBytes, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("合并失败: %v\n%s", err, outputBytes)
}
return nil
}
func extractNumber(filename string) int {
// 提取"segment"后面的数字
base := filepath.Base(filename)
base = strings.TrimPrefix(base, "segment")
base = strings.TrimSuffix(base, ".ts")
num, _ := strconv.Atoi(base)
return num
}
写好了,跑一下试试,如果运气好,一两分钟就合并出一个几百MB的MP4。
关于365大佬第2季视频的坑,我必须说两句
你可能发现,上面代码跑通了,但视频可能不是1080P,为啥?因为很多平台的分片是分清晰度的,.m3u8文件里会嵌套引用子.m3u8(比如playlist_1080.m3u8),你得先去抓最高清晰度的那个.m3u8,而不是主入口的。
具体怎么看呢?打印一下那个content,如果里面出现的不是segment0.ts,而是../1080p/segment0.ts,那说明你抓的是个主目录文件,还得再往下挖一层,这就得另写个递归解析了,但我建议你先手动用浏览器打开那个.m3u8链接,看它返回的东西长啥样,再决定怎么改代码。
如果.m3u8里有#EXT-X-KEY:METHOD=AES-128,URI="key"这种行,那就说明分片是加密的,你需要额外下载那个key文件,然后解密每个分片,解密逻辑也不复杂——需要用OpenSSL的AES-128-CBC,IV默认是分片序号,但365大佬的视频一般不至于加密,真遇到加密了,你还是用现成的工具吧,比如N_m3u8DL-RE,别自己硬肝。
顺便提一嘴:这种下载行为的边界
写这玩意儿主要是图个方便,自己交了钱买了课,离线看看无可厚非,但是别拿去传播,别弄到网盘上卖,我见过太多人因为这事儿被请去喝茶的事儿了——365大佬那个圈子里的诉讼风险不是说着玩的,你私下用用,装个ffmpeg,在本地合并完自己看,问题不大。
最后给你个省事儿的建议
如果你不想折腾代码,想直接下载的话,可以用上面提到的N_m3u8DL-RE,它有命令行版,支持多线程、AES解密、合并,一条命令搞定:
N_m3u8DL-RE "你的m3u8地址" --header "User-Agent: Mozilla/5.0" --header "Referer: https://qian.lu"
它会自动下载所有分片并合并成MP4,比我自己写的那个精简版靠谱多了,但我为什么还写这么一大段Go代码?因为学Go嘛,就是图个折腾的过程,你把这个跑通了,以后遇到任何.m3u8视频,都有底气说自己能搞定,不虚。
不说了,我那个segment1024.ts好像又下载失败了,我得去瞅瞅是不是IP被临时限速了,你要是也遇到这种,歇个几分钟再重跑一遍那个检查代码,效果立竿见影。
