云上虎视频下载分析

2023-03-08
2分钟阅读时长

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【刘兵兵】联系作者立即删除!

逆向分析

首先打开网课网站并登录账号,进入视频播放页面,然后打开开发者者工具看下Fetch/XHR类型网络请求。如下图所示:

image-20230308092446865

从上图Fetch/XHR类型网络请求中可以看到有请求阿里云视频播放信息接口的请求,可以展开对应请求看具体的请求信息,如下图所示:

image-20230308092727632

image-20230308092812706

观察网络请求,并没有发现有请求获取阿里云播放请求的PlayAuth的获取接口,从而可以推测出PlayAuth信息不是通过单独网站业务接口获取,可能是通过内嵌在网页的html中,下面通过右键点击网页选择显示网页源代码,如下图所示进行操作:

image-20230308093236863

打开网页源代码之后,搜索PlayAuth关键字进行定位关键信息,看是否能找到我们想要的的东西,如下图所示果然从网页源代码中找到了PlayAuth,找到PlayAuth之后响应下载对应视频就很简单了,只用使用我的 https://github.com/lbbniu/aliyun-m3u8-downloader 项目就可以轻松搞定了。从源代码中我们还可以看到有初始化Aliplayer的相关代码。

image-20230308093939314

下面演示下获取到PlayAuth之后如何使用 https://github.com/lbbniu/aliyun-m3u8-downloader 工具信息视频下载,如下图演示所示可以成功进行m3u8视频下载

image-20230308094431553

作为低级程序员,通过手动复制PlayAuth进行一个一个视频下载还是太麻烦和耗费视频了,如果可以通过程序输入对应视频课程地址自动获取课程下所有课程链接和PlayAuth就可以解放双手了。下面分析如何通过程序完成自动化的视频下载单个课程下的所有视频。

通过网站请求分析可以知道,网站是通过cookie中token进行用户身份鉴定和鉴权的。如下图:

image-20230308095100149

接下来首先使用github.com/ddliu/go-httpclient构造请求网站的httpclient,如下代码所示 :

var huohujiaoyuClient *httpclient.HttpClient

huohujiaoyuClient = httpclient.NewHttpClient()
token := "<cookie 中 token>"
huohujiaoyuClient.Defaults(httpclient.Map{
    "Accept":                 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
    "Connection":             "keep-alive",
    "Cookie":                 fmt.Sprintf("token=%s", token),
    httpclient.OPT_USERAGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
})

对网站源码分析后,可以得知同html代码可以提取整个课程知识讲解案例教材的所有视频播放地址的链接地址。如下所示:

image-20230308095924507

提取html中视频播放链接地址我们可以使用 github.com/PuerkitoBio/goquery完成html数据提取,接下来我们使用httpclient 获取网页内容,并通过goquery 解析html分别获取知识讲解案例教材的对 div元素:

resp, err := huohujiaoyuClient.Get(url)
if err != nil {
    log.Fatalln(err)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
    log.Fatalln(err)
}
title := doc.Find(".span-title").Text()
basePath := title
cachePath := "cache"
doc.Find(".course_playlist .list .scroll").Each(func(idx int, scroll *goquery.Selection) {
    var typ = "知识讲解"
    if idx != 0 {
        typ = "案例教材"
    }
    // 这里进一步获取a链接地址
})

获取知识讲解案例教材的对应scroll解析器之后,我们就可以利用 scroll解析器进一步提取a链接地址:

lessonPath := path.Join(basePath, typ)
scroll.Find("a").Each(func(idx2 int, a *goquery.Selection) {
	// 解析a链接地址,进行下一步分析
})

获取a链接解析之后,我们通过a链接解析器提取链接地址href属性,提取a标签的内容作为视频文件名,然后再通过httpclient获取页面内容提取PlayAuth内容,首先是提取a标签相关内容和获取页面内容:

href, ok := a.Attr("href")
if !ok {
    return
}
href = fmt.Sprintf("https://www.huohujiaoyu.com%s", href)
filename := fmt.Sprintf("%s.mp4", strings.TrimSpace(a.Text()))
filePath, exitsPath := path.Join(lessonPath, filename), path.Join(cachePath, lessonPath, filename+".download")
// 判断视频是否已经下载,如果存在没有错误
if _, err := os.Stat(exitsPath); err == nil {
    return
}
log.Println(idx, typ, idx2, href, a.Text())
// 获取视频播放页面内容
resp, err = huohujiaoyuClient.Get(href)
if err != nil {
    log.Fatalln(err)
}
detail, err := resp.ToString()
if err != nil {
    log.Fatalln(err)
}

接下来,解析页面内容提取PlayAuth信息,为了方便解析PlayAuth内容我们这里定义了一个结构体,我们可以利用 https://mholt.github.io/json-to-go/ 快速通过json来生成go 结构体定义。

type PeriodDetail struct {
	ChannelID       interface{} `json:"channelId"`
	UserID          interface{} `json:"userId"`
	NickName        interface{} `json:"nickName"`
	Avatar          interface{} `json:"avatar"`
	ImToken         interface{} `json:"imToken"`
	MediaChannelKey interface{} `json:"mediaChannelKey"`
	VideoURL        string      `json:"videoUrl"`
	PlayAuth        string      `json:"playAuth"`
	CoverURL        string      `json:"coverUrl"`
	MaterialURL     string      `json:"materialUrl"`
	TaskURL         string      `json:"taskUrl"`
}

// 直接通过关键字进行提前包含 playAuth 的整个json字符串
index := strings.Index(detail, "var periodDetail = ")
if index > len(detail) {
    log.Fatalln("内容中确实playAuth数据")
}
detail = detail[index+len("var periodDetail = "):]
index = strings.Index(detail, "};")
if index > len(detail) {
    log.Fatalln("内容中确实playAuth数据")
}
detail = detail[:index+1]
var periodDetail PeriodDetail
if err = json.Unmarshal([]byte(detail), &periodDetail); err != nil {
    log.Fatalln(err)
}

提取到PlayAuth就可以直接通过我的github.com/lbbniu/aliyun-m3u8-downloader工具进行视频下载了:

log.Println("start download", filePath)
if err := download.Aliyun(lessonPath, filename, chanSize, periodDetail.VideoURL, periodDetail.PlayAuth); err != nil {
    log.Fatalln(err)
}
// 标识已经下载, 防止重复下载
if err := writeFile(exitsPath, []byte(filePath), fs.ModePerm); err != nil {
    log.Fatalln(err)
}
log.Println("end download:", filePath)

writeFile 函数实现:

func writeFile(filename string, data []byte, perm fs.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
		log.Fatalln(err)
	}
	return ioutil.WriteFile(filename, data, perm)
}

到此整个网课平台视频下载分析全部完成。