当前位置:首页 > 体育 > 正文

用Go语言写个爬虫,把365大佬第2季视频给扒下来(附完整代码)

  • 体育
  • 2026-08-04 04:58:43
  • 16
摘要: 说实话,我一开始听到“365大佬第2季视频”这词儿,第一反应是——这又是个啥付费课程?结果一查,好家伙,是个讲365天不间断输出...

说实话,我一开始听到“365大佬第2季视频”这词儿,第一反应是——这又是个啥付费课程?结果一查,好家伙,是个讲365天不间断输出内容的实战教程,但问题是,视频不给你下载链接,你只能在线看,这就很烦了,对吧?尤其是我这种地铁上没信号还想缓存下来看的,所以我琢磨着,用Go写个下载器,把这活儿干利索了。

先别急着写代码,搞清楚视频藏在哪儿

你得知道,这种平台(比如某课、某站)的视频资源,通常不是直接一个.mp4链接扔给你的,它往往是先给你一个.m3u8文件,里面写着一堆.ts分片文件的地址,你真正的任务,是把这堆.ts碎片下下来,再拼成一个完整的视频。

这儿有个关键点:你得拿到那个.m3u8的URL,怎么拿?两个办法:

  1. 浏览器F12抓包:打开开发者工具,切到Network(网络)标签,过滤m3u8,然后播放视频,就能看到一个.m3u8的请求,右键复制它的地址。
  2. 用Go直接模拟请求:如果你知道视频页面的URL,可以分析它的网页源码,看有没有内嵌的JSON配置里面藏着视频地址,这一招需要点运气,因为很多平台现在都改成动态加载了,直接去源码里扒往往扑空。

我这儿先假设你已经通过抓包拿到了那个https://xxx.xxx.com/playlist.m3u8,下面咱们直接用Go来处理。

第一步:用Go拉取m3u8文件

先别管分片,我们先把.m3u8拿到手,这里有个小坑:很多服务器的.m3u8文件是带Key或者Token校验的,所以你请求的时候,得把浏览器里的User-AgentReferer带上,不然大概率给你返回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.tssegment1.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被临时限速了,你要是也遇到这种,歇个几分钟再重跑一遍那个检查代码,效果立竿见影。

用Go语言写个爬虫,把365大佬第2季视频给扒下来(附完整代码)