云上虎视频下载分析
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【刘兵兵】联系作者立即删除!
逆向分析
首先打开网课网站并登录账号,进入视频播放页面,然后打开开发者者工具看下Fetch/XHR
类型网络请求。如下图所示:
从上图Fetch/XHR
类型网络请求中可以看到有请求阿里云视频播放信息接口
的请求,可以展开对应请求看具体的请求信息,如下图所示:
观察网络请求,并没有发现有请求获取阿里云播放请求的PlayAuth
的获取接口,从而可以推测出PlayAuth
信息不是通过单独网站业务接口获取,可能是通过内嵌在网页的html中,下面通过右键点击网页选择显示网页源代码,如下图所示进行操作:
打开网页源代码之后,搜索PlayAuth
关键字进行定位关键信息,看是否能找到我们想要的的东西,如下图所示果然从网页源代码中找到了PlayAuth
,找到PlayAuth
之后响应下载对应视频就很简单了,只用使用我的 https://github.com/lbbniu/aliyun-m3u8-downloader
项目就可以轻松搞定了。从源代码中我们还可以看到有初始化Aliplayer
的相关代码。
下面演示下获取到PlayAuth
之后如何使用 https://github.com/lbbniu/aliyun-m3u8-downloader
工具信息视频下载,如下图演示所示可以成功进行m3u8视频下载
作为低级程序员,通过手动复制PlayAuth
进行一个一个视频下载还是太麻烦和耗费视频了,如果可以通过程序输入对应视频课程地址自动获取课程下所有课程链接和PlayAuth
就可以解放双手了。下面分析如何通过程序完成自动化的视频下载单个课程下的所有视频。
通过网站请求分析可以知道,网站是通过cookie中token进行用户身份鉴定和鉴权的。如下图:
接下来首先使用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代码可以提取整个课程知识讲解
和案例教材
的所有视频播放地址的链接地址。如下所示:
提取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)
}
到此整个网课平台视频下载分析全部完成。