晚上HLS什么意思

生活百科 | 发布时间:2024-03-09 03:29:02 | 小编:找百科 - www.80007.net
找百科:专业的百科知识平台 QQ:7384656

最近在看极客的视频课,想把视频离线到本地看,发现下载下来的ts流打开均无法播放,后面又下载了极客APP可以离线下载视频,但是离线的视频只能在极客APP里播放,这哪天不用APP了,视频也看不了了,所以视频一定是被加密过的,需要进行解密。在网上找了很多方法均没有找到一个完美的解密方法,看了好几天的博客,现在终于有眉目了,好了不说那么多了,下面我们直接进入正题。

一、准备工作

本文只针对极客加密视频课程的分析处理,专栏的略过。

需一定的Chrome开发者工具调试能力和代码能力,需要准备的工具:极客时间里购买的视频课程、ChromeF12、Pycharm、Python,我使用的是Python,当然也可以用Java等语言。

阿里云视频加密方案包含两部分:加密转码+解密播放,我们主要分析解密播放部分。

二、获取视频课程列表

首先登录极客时间首页,按F12打开调试工具-Network,选择Fetch/XHR,点击个人头像-我的课程,可以看到product接口,此接口返回了所有的课程信息,需要我们过滤出视频课程类型(type=c3)

晚上HLS什么意思

F12调试

示例代码如下:

def_product(self,_type='c3'):\n"""商品列表(就是课程)的接口)方法"""\nlog.info("请求获取课程列表接口:")\nurl="https://time.geekbang.org/serv/v3/learn/product"\nmethod="POST"\nheaders=deepcopy(self.common_headers)\nheaders["Host"]="time.geekbang.org"\nheaders["Origin"]="https://time.geekbang.org"\nheaders["Cookie"]=self.cookie.cookie_string\nparams={\n"desc":'true',\n"expire":1,\n"last_learn":0,\n"learn_status":0,\n"prev":0,\n"size":20,\n"sort":1,\n"type":"",\n"with_learn_count":1\n}\n\nlog.info(f"接口请求参数:{params}")\nres=requests.request(method,url,headers=headers,json=params)\n\nifres.status_code!=200:\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"课程列表接口请求出错,返回内容为:{res.content.decode()}")\nraiseRequestError(f"课程列表接口请求出错,返回内容为:{res.content.decode()}")\ndata=res.json().get('data',{})\nself.cookie.load_set_cookie(res.headers['Set-Cookie'])\n\nifdata:\nself.products+=self._parser_products(data,_type)\nelse:\n_save_finish_article_id_to_file()\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"课程列表接口没有获取到内容,请检查请求。返回结果为:{res.content.decode()}")\nraiseNotValueError(f"课程列表接口没有获取到内容,请检查请求。返回结果为:{res.content.decode()}")\nlog.info('-'*40)\n\ndef_parser_products(self,data,_type='c3'):\n"""\n解析课程列表内容的方法(从中提取部分数据)\nArgs:\ndata:课程相关信息,一般为接口返回的数据\n_type:课程类型,c1代表专栏,c3代表视频课,all代表全部,默认只获取c3的内容\nReturns:\n解析后的结果,以列表形式\n"""\nresult=[]\nkeys=['title','type','id']#定义要拿取的字段\nproducts=data.get('products',[])\nlists=data.get('list',[])\nforproductinproducts:\n#如果课程标题在需要排除的列表中,则跳过该课程\nifproduct.get('title','')inself.exclude:\ncontinue\n\nnew_product={key:valueforkey,valueinproduct.items()ifkeyinkeys}\nnew_product['articles']=[]#定义章节列表(用来存储文章信息)\nnew_product['article_ids']=[]#定义章节ID列表(用来存储文章ID信息))\nforproinlists:\nifnew_product['id']==pro['pid']:\nnew_product['aid']=pro['aid']\nif_type.lower()=='all'ornew_product['type']==_type:\nresult.append(new_product)\nreturnresult

三、遍历获取视频课程章节信息

点击视频课程,此时调用了articles接口,此为获取该视频课章节列表接口,cid为对应课程id

def_articles(self,cid,pro):\n"""获取视频课程章节列表接口方法"""\nglobalALL_ARTICLES\nlog.info("请求获取视频课程章节列表接口:")\nurl="https://time.geekbang.org/serv/v1/column/articles"\nmethod="POST"\nheaders=deepcopy(self.common_headers)\nheaders["Host"]="time.geekbang.org"\nheaders["Origin"]="https://time.geekbang.org"\nheaders["Cookie"]=self.cookie.cookie_string\nparams={\n"cid":cid,\n"size":500,\n"prev":0,\n"order":"earliest",\n"sample":"false"\n}\n\nlog.info(f"接口请求参数:{params}")\nres=requests.request(method,url,headers=headers,json=params)\n\nifres.status_code!=200:\n_save_finish_article_id_to_file()\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"获取章节列表接口请求出错,返回内容为:{res.json()}")\nraiseRequestError(f"获取章节列表接口请求出错,返回内容为:{res.json()}")\ndata=res.json().get('data',{})\nself.cookie.load_set_cookie(res.headers['Set-Cookie'])\n\nifdata:\nids=[]\narticle_list=data.get('list',[])\nforarticleinarticle_list:\nids.append(article['id'])\nALL_ARTICLES+=ids\npro['article_ids']+=ids\nelse:\n_save_finish_article_id_to_file()\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"获取章节列表接口没有获取到内容,请检查请求。返回结果为:{res.json()}")\nraiseNotValueError(f"获取章节列表接口没有获取到内容,请检查请求。返回结果为:{res.json()}")\nlog.info('-'*40)

点击任意章节进行播放时,我们发现调用了article接口,此为章节信息接口,aid是课程章节id。在此接口中,我们需要拿到m3u8下载地址,章节title及章节id。然后开始处理m3u8文件。

def_article(self,aid,pro,file_type=None,get_comments=False):\n"""通过课程ID获取视频课程章节信息接口方法"""\nglobalFINISH_ARTICLES\nlog.info("请求获取视频课程章节信息接口:")\nurl="https://time.geekbang.org/serv/v1/article"\nmethod="POST"\nheaders=deepcopy(self.common_headers)\nheaders["Host"]="time.geekbang.org"\nheaders["Origin"]="https://time.geekbang.org"\nheaders["Cookie"]=self.cookie.cookie_string\nparams={\n"id":aid,\n"include_neighbors":"true",\n"is_freelyread":"true"\n}\n\nlog.info(f"接口请求参数:{params}")\nres=requests.request(method,url,headers=headers,json=params)\n\nifres.status_code!=200:\n_save_finish_article_id_to_file()\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"获取章节信息接口请求出错,返回内容为:{res.content.decode()}")\nraiseRequestError(f"获取章节信息接口请求出错,返回内容为:{res.content.decode()}")\ndata=res.json().get('data',{})\nself.cookie.load_set_cookie(res.headers['Set-Cookie'])\n\nifdata:\n#comments=self._comments(aid)ifget_commentselseNone\nkeys=['hls_videos','article_title','id']#定义要拿取的字段\narticle={key:valueforkey,valueindata.items()ifkeyinkeys}\nm3u8_url=article['hls_videos']['ld']['url']\n\nlog.info("【开始下载】课程章节:%s"%article['article_title'])\nparse_m3u8_url(m3u8_url,article['article_title'])\nlog.info("【下载完成】课程章节:%s"%article['article_title'])\n\nFINISH_ARTICLES.append(article['id'])#将该章节ID加入到遍历完成的列表中\npro['cid']=data['cid']\nelse:\n_save_finish_article_id_to_file()\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"获取章节信息接口没有获取到内容,请检查请求。返回结果为:{res.content.decode()}")\nraiseNotValueError(f"获取j信息接口没有获取到内容,请检查请求。返回结果为:{res.content.decode()}")\nlog.info('-'*40)

首先需要把m3u8文件内容提取出来,可以看到m3u8内容中,视频流是有加密的,ts文件没有解密是无法播放的

晚上HLS什么意思

m3u8文件

四、遍历视频课程章节m3u8文件

我们发现这个m3u8里只有一个解密地址,应该是获取解密key的,即所有ts文件都用同一个key进行解密,但是没有iv,后面发现iv就是16位16进制的0即可。

注意:

每个m3u8里的解密地址是不一样的,所以每遍历一个新的章节时,需要重新获取key进行解密ts后面分析发现这个解密地址只能访问一次,再次访问就会失效,代码中需要控制下次数

提取出解密地址以及所有ts流地址:

defparse_m3u8(self,m3u8_content,url_path):\nself.m3u8=m3u8_content\nself.ts_url_list=['{}/{}'.format(url_path,ts_name)forts_nameinre.findall(r'.*\\.ts',self.m3u8)]\nkey_url_list=re.findall(r'EXT-X-KEY:METHOD=AES-128,URI="http.*"',self.m3u8)#提取ts解密地址\niv_list=re.findall(r'IV=0x.{32}',self.m3u8)\n\nself.key_url_dealt=[]\nforkeyinkey_url_list:\nkey=key[30:-1]\nself.key_url_dealt.append(key)\n\nself.iv_dealt=[]\nforiviniv_list:\niv=iv[5:]\nself.iv_dealt.append(iv)

五、获取课程章节m3u8对应解密key

获取解密key方法,返回bytes类型:

defparse_key(self,key_url):\ntry:\nreq=requests.get(key_url,self.headers)\nreq.raise_for_status()\nreq.encoding=req.apparent_encoding\nkey=req.content\nreturnkey\nexcept:\ntraceback.print_exc()

六、下载ts流解密并保存至本地

遍历ts地址列表并解密ts流,保存到章节临时列表:

defdecoding(self):\nkey=""\nforiinrange(0,len(self.ts_url_list)):\nts_url=self.ts_url_list[i]\nprint("No",i,"file\\t",ts_url)\nts=self.save_ts_url(ts_url)\n\nkey_name=ts_url.split("/")[-1].split(".ts")[0]+".key"\niv_name=ts_url.split("/")[-1].split(".ts")[0]+".iv"\nts_name=ts_url.split("/")[-1].split(".ts")[0]+"_convert.ts"\n\n#只可获取一次key,第二次及之后无效\nifi<=0:\n#key=self.get(self.key_url_dealt[0])\nkey=self.parse_key(self.key_url_dealt[0])\n\niv=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'\nprint("key_url:",self.key_url_dealt[0])\nprint("key:",key)\nprint("iv:",iv)\n\nself.save_content(key_name,key,self.path)\nself.save_content(iv_name,iv,self.path)\n\npc=PrpCrypt(key,iv)\nresult=pc.decrypt(ts)\nwithopen(self.ts_path+"\\\\"+ts_name,'wb')asf:\nf.write(result)\nself.ts_list.append(result)#章节解密的ts流列表

七、合并课程章节已解密的ts流

最后把解密的ts流列表写入到本地文件,即可正常播放了:

defmerge_ts(self,title):\nprint("【开始合并】课程章节==>{}.ts".format(title))\nout_file=open(self.result_path+os.path.sep+self.check_filename("{}.ts".format(title)),"wb")\n\nforiinrange(0,len(self.ts_list)):\nin_file=self.ts_list[i]\nout_file.write(in_file)\nout_file.close()\nprint("【合并完成】课程章节==>{}.ts".format(title))\n\ndefcheck_filename(self,file_name):\n"""\n校验文件名称的方法,在windows中文件名不能包含('\\','/','*','?','<','>','|')字符\nArgs:\nfile_name:文件名称\nReturns:\n修复后的文件名称\n"""\nreturnfile_name.replace('\\\\','')\\\n.replace('/','')\\\n.replace('*','x')\\\n.replace('?','')\\\n.replace('<','《')\\\n.replace('>','》')\\\n.replace('|','_')\\\n.replace('\\n','')\\\n.replace('\\b','')\\\n.replace('\\f','')\\\n.replace('\\t','')\\\n.replace('\\r','')

总体思路,先遍历过滤出的视频课程列表,再遍历课程章节信息,再遍历m3u8解密保存:

forproingeek.products:\ngeek._articles(pro['id'],pro)#获取章节列表\n\narticle_ids=pro['article_ids']\nforaidinarticle_ids:\nifset(ALL_ARTICLES)==set(FINISH_ARTICLES):\nimportsys\nlog.info("正常抓取完成啦,不用再继续跑脚本了。")\nsys.exit(1)\n\nifstr(aid)inFINISH_ARTICLES:\ncontinue\ngeek._article(aid,pro,file_type=file_type,get_comments=get_comments)#获取单个章节的信息

以上整个过程中,涉及到的几个接口都是验证登录的,所以需要写个登录接口,事先获取cookie使用。

登录接口不可频繁调用,否则会验证码拦截,严重也可能封IP:

def_login(self):\n"""登录接口方法"""\nlog.info("请求登录接口:")\nurl="https://account.geekbang.org/account/ticket/login"\nmethod="POST"\nheaders=deepcopy(self.common_headers)\nheaders["Host"]="account.geekbang.org"\nheaders["Origin"]="https://account.geekbang.org"\nheaders["Cookie"]=self.cookie.cookie_string\nparams={\n"country":86,\n"cellphone":self.cellphone,\n"password":self.password,\n"captcha":"",\n"remember":1,\n"platform":3,\n"appid":1,\n"source":""\n}\n\nlog.info(f"接口请求参数:{params}")\nres=requests.request(method,url,headers=headers,json=params)\n\nif(res.status_code!=200)or(str(res.json().get('code',''))=='-1'):\n_save_finish_article_id_to_file()\nlog.info(f"此时products的数据为:{self.products}")\nlog.error(f"登录接口请求出错,返回内容为:{res.content.decode()}")\nraiseRequestError(f"登录接口请求出错,返回内容为:{res.content.decode()}")\nself.cookie.load_set_cookie(res.headers['Set-Cookie'])\nlog.info('-'*40)

忙了一晚上,终于大功告成,成果图:

晚上HLS什么意思

爬取日志

晚上HLS什么意思

解密ts文件

晚上HLS什么意思

ts播放

注意:

此文仅为学习交流分享,请勿用于非法及商业用途,否则后果自负,与本人无关。对Python或爬虫有兴趣的同学,可分享转发交流。
找百科:专业的百科知识平台 QQ:7384656
版权声明

本文仅代表作者观点,不代表找百科立场。
本文系作者授权找百科发表,未经许可,不得转载。

小编推荐