爬虫案例之视频爬取与合成
案例网址:https://v6.dious.cc
下载视频的网址:https://www.99meijutt.com/play/97071-0-0.html
- 用到的知识点
- asynic,协程异步操作。
- ffmpeg,合成视频指令
- aiohttp,在协程里面发送异步请求
【一】分析
(1)分析网站
- 打开网站首页
-
打开开发者模式进行抓包
- 思路:我们已经知道每个视频都是由一个个ts文件片段组成
- 而这些ts片段的地址就存在m3u8文件中
- 所以我们现在的主要目标放在m3u8文件中
- 思路:我们已经知道每个视频都是由一个个ts文件片段组成
-
这里我们找到了第一个m3u8文件,查看其响应内容
- 我们可以发现,其响应内容中存在类似链接的地址
- 我们继续往下看到第二个m3u8文件,打开,查看其响应内容
- 我们可以清楚的看到,其响应内容中存在的就是我们一个个ts文件的链接
(2)准备爬取工作
- 本次案例使用的是asynic模块
- 不为别的,就是单纯的下载批量文件图一块!嘎嘎快!
【二】分析完就动手!
(1)导入模块
# 请求头中的headers中的User-Agent模拟
from fake_useragent import UserAgent
# os 系统模块
import os
# 正则表达式模块
import re
# 涉及到加密动作,需要解密模块(本次涉及到的是AES加密)
from Crypto.Cipher import AES
# asynic 异步协程模块
import asyncio
# 协程中的请求模块
import aiohttp
(2)初识化需要用到的全局变量
# 初始化模块def __init__(self):# 初始化请求头 headersself.headers = {'User-Agent': UserAgent().random}# 初始化文件夹名字(存储ts文件)self.file_name = 'video'# 判断文件夹是否存在(做判断)if not os.path.exists(self.file_name):os.mkdir(self.file_name)
(3)获取到所有的ts文件的链接
# 这里是请求第一个m3u8文件操作async def get_page_url(self):# 这是找到的第一个m3u8的网址index1_url = 'https://v6.dious.cc/20220428/X2mBsQ9X/index.m3u8'# 对上述链接发起请求async with aiohttp.ClientSession() as session:async with session.get(url=index1_url, headers=self.headers) as response:# 拿到第一个m3u8文件里的内容,将下一个m3u8文件地址解析page_text = await response.text() # 这里是文本文件,所以用response.text()# 从获取到的文件中解析获得第二个m3u8地址index2_url = 'https://v6.dious.cc' + page_text.split('\n')[-2] # 做字符串拼接成完整的下一个链接# 对上述链接发起请求async with aiohttp.ClientSession() as session:async with session.get(url=index2_url, headers=self.headers) as response:# 拿到第二个m3u8文件,这里面存的就是所有ts文件的链接地址page_text = await response.text() # 这里是文本文件,所以用response.text()# 将带有ts地址的m3u8文件写入到本地 - 为下一步的合并做准备# 定义文件路径file_path = f'{self.file_name}' + '\\' + 'index.m3u8'# 打开文件夹with open(file_path, 'w') as f:# 循环获取取到的第二个m3u8文件中的每一个ts文件for line in page_text.split('\n'):# 如果文件中不存在以 .ts 文件为结尾的文件if not line.endswith('.ts'):# 则直接写入# 目的是为了合并做准备,ffmpeg合并文件,其中m3u8文件必须带头部,否则会报错f.write(line + '\n')# 如果文件中存在 .key 结尾的文件,将其头部补充完整elif line.endswith('.key'):# 拼接成完整的urlf.write('http://v6.dious.cc' + line + '\n')# 如果存在 .ts 为结尾的文件else:# 将字符串进行分隔,取最后一部分,因为文件名不能存在 / \ 等特殊字符line = line.split('/')[-1]f.write(line + '\n')# 将所有的ts文件链接提取出来tss_urls = re.findall(r'(.*.ts)', page_text)# 将所有ts文件链接提取出来返回,留待下一部分使用return tss_urls
(4)下载所有ts文件
# 这个部分是下载文件的部分async def download_file(self, i, sem):''':param i: 每一个ts链接:param sem: 信号量:return: 打印每一个文件的下载情况'''# 设置并发量 --- 防止异步协程太快,造成timeoutasync with sem:# 将每一个ts链接拼接完整ts_url = 'https://v6.dious.cc' + i# 对每一个ts链接发起请求async with aiohttp.ClientSession() as session:async with session.get(url=ts_url, headers=self.headers) as response:# 这里请求到的数据是被加密的ts文件数据(视频文件)response_data = await response.read() # 这里返回的是二进制文件,所以用response.read()# 这里是将每个ts文件的后缀名字取出来,当做每一个文件的名字ts_name = os.path.basename(ts_url)# 通过观察m3u8文件,发现其存在AES加密,这是cbc加密模式所需要的key的链接(在m3u8文件的上方,可以看到有一个key.key的文件,其中存的就是key)key_url = 'https://v6.dious.cc/20220428/X2mBsQ9X/1500kb/hls/key.key'# 对链接发起请求async with aiohttp.ClientSession() as session:async with session.get(url=key_url, headers=self.headers) as response:# 拿到key并存储为二进制数据key = await response.read() # 这里返回的是二进制文件,所以用response.read()# 这是cbc模式所需要的iv偏移量(文件中没有声明,默认设置为16位的 0 )iv = b'0000000000000000' # 需要将格式转为二进制模式,才能传到aes参数里面# 创建aes对象aes = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)# 声明文件路径file_path = f'{self.file_name}' + '\\' f'{ts_name}'# 打开文件with open(file_path, 'wb') as f:# 如果需要解密ts视频文件# data = aes.decrypt(response_data)# f.write(data)# 这里采取不解密ts文件f.write(response_data)print(f'{ts_name}已经下载完成')
(5)ffmpeg视频合并命令
# 这里的部分是用来合并ts片段async def merge_video(self, filename='video'):''':param filename: 合成文件的文件名:return: '''# ffmpeg -i ts文件夹下的m3u8格式文件 -c copy 合成后的视频名字# ffmpeg -i index.m3u8 -c copy apple.mp4# 切换到ts文件列表os.chdir(f'{self.file_name}')# 合成文件os.system(f'ffmpeg -protocol_whitelist "file,http,crypto,tcp" -i index.m3u8 -c copy {filename}.mp4')
(6)设置协程的主程序入口
async def main(self):# 声明任务列表global tasks# 创建任务列表tasks = []# 信号量:控制协程下载的并发量sem = asyncio.Semaphore(50)# 获取到所有ts链接tss_urls = await self.get_page_url()# 循环获取到每一个ts链接for i in tss_urls:# 创建任务,并传入参数,传给下一个函数调用下载task = asyncio.create_task(self.download_file(i, sem))# 将每一个任务添加到所有的任务中tasks.append(task)# 收集任务await asyncio.wait(tasks)# 待所有文件下载完成后进行ts文件合并await self.merge_video()
(7)设置文件的主程序入口
if __name__ == '__main__':# 实例化类对象s = Spider()# 调用类对象中的main方法# 执行协程启动run函数asyncio.run(s.main())