利用协程爬取m3u8视频
在进行了爬虫的相关学习之后,自己尝试爬取了一些视频,但动辄ts文件就几百个,单线程伤不起那时间,一个一个等实在太慢了,想利用多线程,查看相关资料,又说python是假的多线程,而且爬取视频这操作也属于IO阻塞操作多的那种,感觉时间提升也不大,多线程和多进程还有协程,协程挺适合这种的,就毅然决然的使用协程了。
一.查看网站并分析
1.找到各集数对应网站
首先理清爬取思路,对我这种刚入门的菜鸡来说,爬视频就是看它是不是mp4,或者是不是m3u8格式的,但如标题所言,我今天找的这个是m3u8格式的。直接查看网站源码,如图所示,我们想爬取所有集数首先需要找到这些集数的href
可以看到,我们如果想进这些集数的网站里面得到m3u8文件还是需要进行拼接操作的,这样才能得到进一步m3u8文件。
我们首先进入第一集里面看一看,瞅一瞅,熟悉的打开网站,熟悉的搜索video,打开一看,哇,
是不是很简单
我的上帝啊,瞧瞧那document上面一排是什么,哇,是iframe标签,看来还需要打开一个网站,这标签代表着视频是在内嵌的框架播放的,因为网页中嵌入的<Iframe></Iframe>
所包含的内容与整个页面是一个整体(上面这句专业的话那肯定不是我说的),大概就是相当于另一个网站吧。直接requests肯定得不到m3u8文件了。接下来找到那个内嵌网站
2.找到内嵌网站
接下里我们就直接进入网站源码里面进行查找吧,腚眼一看
这不就找到了内嵌网站了吗,只是斜杠在处理的时候需要换一下,这里注意,处理的时候需要转义一下,单斜杠替换的时候不会处理掉的。
3.进入内嵌网站拿到m3u8
进入视频网站,可以知道,整个屏幕都是视频,我们怎么查看源码呢,同样是按F12,在sources里面打开网址路径对应的文件夹下打开,可以找到m3u8文件
什么,你说你看到上面也有一个m3u8文件
我才不会跟你说我写这一篇博客的时候才看到,没事,大不了多一个步骤。刷新页面,可以看到这个m3u8的预览
很明显,我们需要通过这个m3u8文件拿到完整的m3u8文件,
当拿到完整的m3u8文件之后,我们也可以通过这个网站先看一下完整的m3u8文件预览
对味了,啊,那居然还有加密,可以看到是AES-128加密,没事,我们到时候把每一集的m3u8文件这个对应URI拿到并且得到key就行,从uri里面得到密码直接解密,AES的原理详情我也不是很清楚,但好像解密步骤就那回事,需要看看是AES的什么类型。这些可以拿到每一集的m3u8文件了,并且边解密边写入文件
4.将ts文件写入对应的文件夹
这里就利用到我们需要的协程了,直接贴完整代码吧,首先是总体思路
总体思路就是这样,其他的都是一些细枝末节了,接下来也就是看协程怎么运用了。至于ts的合并,就放在下边,也不多赘述了,主要就是利用顺序来合并,自动合并的话顺序有些问题。
import os
import glob
for t in range(0,14):
if t < 10:
x = glob.glob0(f"./第0{t}集/*.ts")
print(len(x))
with open(f"第0{t}集总合并.ts", "ab") as g:
for i in range(0, len(x) - 1):
with open(f"./第0{t}集/{i}.ts", 'rb') as xx:
g.write(xx.read())
else:
x = glob.glob(f"./第{t}集/*.ts")
print(len(x))
with open(f"第{t}集总合并.ts", "ab") as g:
for i in range(0, len(x) - 1):
with open(f"./第{t}集/{i}.ts", 'rb') as xx:
g.write(xx.read())
二.完整代码
import os
import random
import aiofiles
from lxml import html
import requests
import asyncio
import aiohttp
import re
import datetime
from Crypto.Cipher import AES
import time
etree = html.etree
episode_list = []
html_list = []
headers = {
'User-Agent': 'Mozilla / 5.0(WindowsNT10.0;Win64;x64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / '
'91.0.4472.124Safari / 537.36 '}
def get_key(name):
with open(f"{name}的m3u8.txt", "r") as f:
for i in f:
# 使用正则来得到密码连接
x = re.compile(r'URI="(.*?)"')
t = x.findall(i)
if len(t):
# print(t[0])
# 直接把密码原文得到,byte类型
resp = requests.get(t[0], headers=headers).content
# print(resp)
return resp
# 正式写入文件夹里面的ts文件
# 必须得用n来排序,要不然顺序乱了
async def download_ts_descrpt(name, line, n, key,sem):
#这里就是调用同时并发协程的数量
async with sem:
# aiohttp.ClientSession() as session:可以放在上一个函数打开文件上面,因为我的操作相当于每一次运用协程
# 都创建一个连接池,不建议我这样的做法
async with aiohttp.ClientSession() as session:
async with session.get(url=line, headers=headers) as f:
#需要利用aiofiles来打开文件,也是异步操作
async with aiofiles.open(f"{name}/{n}.ts", 'wb') as x:
t = await f.content.read() #使用协会时候需要使用read()
# 利用密钥进行解密操作,CBC为猜的,偏移量数量设置跟解出来的密码个数相同
aes = AES.new(key=key, IV=b'0000000000000000', mode=AES.MODE_CBC)
await x.write(aes.decrypt(t))
# 创建保存ts的文件夹,从m真实的m3u8文件里面读取ts
async def create_ts_encrpt(name, key):
count = 0
#使用count来计数,这样不会使协程写入的时候,ts文件顺序变乱
tasks = []
#判断是否有文件夹 ,如果有就跳过,没有就创建
if not os.path.exists(f'{name}'):
os.makedirs(f"{name}")
#这里注意,协程里面再次创建协程,这就是进行异步操作,把每一步ts文件添加到协程任务列表
#sem = asyncio.Semaphore(5)代表允许的并发协程数量为5,这里不建议设多,也不建议删掉,太多
# 的话容易信号灯超时,也就是aiohttp.ClientSession创建的连接池里面请求,网站会响应不过来
sem = asyncio.Semaphore(5)
async with aiofiles.open(f"./{name}的m3u8.txt", mode='r') as f:
async for line1 in f:
if line1.startswith("#"):
continue
else:
line1 = line1.strip()
# print(line1)
# 添加另外一个协程事件进行操作,这就是写入加密得ts
tasks.append(download_ts_descrpt(name, line1, count, key,sem))
count += 1
await asyncio.wait(tasks)
# 把第二层m3u8文件写下来
async def download_m3u82(url, name):
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as f:
f1 = await f.content.read()
# 因为只能写入content所以不需要decode
with open(f"{name}的m3u8.txt", 'wb') as f2:
f2.write(f1)
# 这里得到每一个密钥然后传入下一个协程函数。这里调用了get_key函数得到每一个密钥
key_encrpt = get_key(name)
await create_ts_encrpt(name, key_encrpt)
# 把第一层m3u8文件写下来,并把第二层m3u8的文件读出来
async def download_m3u8(url, name):
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as f:
f1 = await f.content.read()
# 因为只能写入content所以不需要decode
with open(f"{name}.txt", 'wb') as f2:
f2.write(f1)
#这里就是读出来
with open(f"{name}.txt", 'r', encoding='utf-8') as y:
for line in y:
if line.startswith("#"):
continue
else:
# 去掉空白和换行符
line = line.strip()
line = "https://vod2.buycar5.cn" + line
# print(line)
await download_m3u82(line, name) #又传入下一个函数
# 在视频里面把每一个m3u8文件拿出来
async def get_html_m3u8(url, name):
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as f:
html2 = await f.content.read()
html2 = html2.decode('utf-8')
# print(html2)
html2_m3u8 = re.findall(r'var main = "(.*?)";', html2)[0]
html2_m3u8 = 'https://vod2.buycar5.cn' + html2_m3u8
await download_m3u8(html2_m3u8, name)
# 从每一集的链接里面把视频的链接提取出来
async def get_html_m3u8_pre(url, name):
async with aiohttp.ClientSession() as session: #实例化session通过session来get
async with session.get(url, headers=headers) as f:
html1 = await f.content.read()
html1 = html1.decode('utf-8')
# print(html1),第一集吗,格式跟其他集数有所不同可以理解
if name == "第01集":
html1_m3u8 = re.findall(r'"link_pre":"","url":"(.*?)","url_next"', html1)[0].replace("\\", "") #这里就是双斜杠,文中提到的需要转义的地方
else:
html1_m3u8 = re.findall(r'.html","url":"(.*?)","url_next"', html1)[0].replace("\\", "")
# print(html1_m3u8)
await get_html_m3u8(html1_m3u8, name) #使用await调用协程函数
#得到每一集的初始链接
async def get_html(url):
tasks = [] #因为有12集,从主页面的li下面的得到的每一集的链接需要进行拼接,并且将12集用协程来完成,创建协程列表
resp = requests.get(url, headers=headers).content.decode('utf-8')
resp = etree.HTML(resp)
resp_list = resp.xpath('//ul[@class="stui-content__playlist clearfix"]')
for res in resp_list:
episode = res.xpath('./li/a/@href') #得到名字和网址,此时返回的都是列表,注意
episode_name = res.xpath('./li/a/text()')
# print(episode_name)
for i in range(0, len(episode)):
episode[i] = "https://www.autonicdq.com" + episode[i]
tasks.append(get_html_m3u8_pre(episode[i], episode_name[i])) #逐项添加协程任务,此时协程任务的函数写在这里面
await asyncio.wait(tasks) #执行协程函数
# 得到主页面的的每一集的链接
if __name__ == '__main__':
t1 = datetime.datetime.now() #记录开始时间
url_main = "https://www.autonicdq.com/voddetail/4002.html"
loop = asyncio.get_event_loop() #协程的开始实例化一个loop对象
loop.run_until_complete(get_html(url_main)) #在loop对象添加要完成的任务
t2 = datetime.datetime.now() #记录结束时间的
print(t2 - t1)
速度比正常快了很多,虽然把协程并发数量进行了限制,但不限制,网站遭不住