本次爬虫目标:爬取《某某少女某某篇》这部电影,并选择ukyun源进行,最后保存起来
本次爬虫使用工具/方法:request、正则、协程、ffmpeg视频合并、AES解密
声明:本文仅用于学习
直接f12分析,发现他的资源是在iframe的,并且我们要爬取的是一个m3u8的文件
并且复制他的iframe标签,在Ctrl+u中搜索,也是没有的,这是个js加载的,一般获取这种动态加载的可以使用Selenium打开浏览器等待页面加载完成后,在进入iframe获取src标签的网址内容。src网址直接打开是这样的,一个全屏的视频播放。这里我就不搞那么麻烦了,直接复制然后发送请求使用
下面就是iframe中scr中的链接全屏播放样子
对这个该页进行发送请求返回结果:
在返回的结果中,这个url对应的就是相应的m3u8文件,我们在浏览器播放视频时抓包也能看到,这个地址是包含相应的电影资源的
我们使用正则把该网址提取出来,并且保存一下
得到以下结果:
这也对应着网页监控中的相应内容
这些ts文件就是这部要爬取的dianying切割的一小部分。在m3u8文件中我们可以知道,每个ts文件视频长度最多不超过6s,而且这些ts是经过AES对称加密的,通过观察到的METHOD=AES-128,确认16字节,以及浏览器看到的秘钥网址中的hls字样,能得出该加密模式为CBD模式,别去key的地址也在m3u8文件里
在此之前,我们对获取的m3u8的文件中已经得到的ts文件地址进行访问,把这些文件给下载下来。一部电影少说也有上千的ts文件组成,所有我们使用异步对这些ts文件爬取。
semaphore = asyncio.Semaphore(50)
async def download_one(session,url_ts): # 使用异步方法去下载,毕竟量太大了
for i in range(4): # 重试4次
try:
file_name = url_ts.split('/')[-1]
async with semaphore: #
async with session.get(url_ts,timeout=30) as resp: # 30s都没有就算了
content = await resp.content.read()
async with aiofiles.open(f"./电影_源_加密/{file_name}",mode="wb") as f:
await f.write(content)
print(url_ts,'下载成功')
break
except:
print('下载失败',url_ts)
await asyncio.sleep((i+1)*2) # 失败了就休息一下再爬
async def download_ts_all(session):
tasks = []
with open('./m3u8需解密.txt','r',encoding='utf-8') as f:
for line in f:
if line.startswith('#'):
continue
line = parse.urljoin(url,line)
task = asyncio.create_task(download_one(session,line))
tasks.append(task)
await asyncio.wait(tasks) # 把任务挂起,去等待执行
结果如下:
这些爬取完成的ts文件,因为AES加密,所有并不能查看这些视频。
对m3u8文件中,key的地址进行访问,并转成字节
def get_key(): # 获取key
obj = re.compile(r'URI="(.*?)"',re.S)
with open('m3u8需解密.txt','r',encoding='utf-8') as f:
result = obj.findall(f.read())[0]
f.close()
url_key=parse.urljoin(url,result)
print(url_key)
key_str = get_html(url_key)
print(key_str.encode('utf-8'))
输出:
b'e3dec185bc8cbceb'
这时候,我们就拿着这个key,去打开那些ts文件,进行解密后再保存起来,因为数量很多,依旧使用异步,如下:
async def dec_one(filename,key): # 解密
print(f"{filename}:开始解密")
# 加密解密对象开始创建,偏移量0,模式MODE_CBC
aes = AES.new(key=key,IV=b'0000000000000000',mode=AES.MODE_CBC)
async with aiofiles.open(f'./电影_源_加密/{filename}',mode='rb') as f1, \
aiofiles.open(f'./电影_源_解密后/{filename}', mode='wb') as f2:
# 解密后直接保存到另一个文件夹
content = await f1.read()
bc = aes.decrypt(content) # 解密
await f2.write(bc)
print(f"{filename}:解密完成")
现在我们在去看看这些ts文件,可以发现,这是时长只有几秒的ts文件是能够播放的,解密完成,随便点一个,也是能播放的
然后就是需要把这些视频合并在一起。因为是使用异步爬取的视频,所有顺序也是乱糟糟的,我们依旧读取m3u8文件,根据文件里的ts顺序来整理一下,写到一个txt文件里,然后再用ffmpeg根据txt文件里的顺序进行一次性合并
最后也是成功的将这上千个ts文件合并成一个mp4文件了
完整的代码如下:
import subprocess
import requests
import re
from urllib import parse
from fake_useragent import UserAgent
import asyncio
import aiohttp
import aiofiles
from Crypto.Cipher import AES
ua = UserAgent()
user_agent = ua.edge
url = 'https://ukzy.ukubf3.com/share/lLvozncp364PSH7M' # 视频页链接
headers = {
'user-agent': user_agent
}
def get_html(url):
print('获取页面')
resp = requests.get(url=url,headers=headers)
# print(resp.text)
return resp.text
def get_m3u8_url():
# 获取视频页的m3u8 链接
print('获取视频页中的m3u8网址')
page_source = get_html(url)
ojb = re.compile(r'"url":"(?P<m3u8>.*?)"}',re.S)
result = ojb.search(page_source)
m3u8_url = result.group('m3u8')
m3u8_url = parse.urljoin(url,m3u8_url)
print(m3u8_url)
return m3u8_url
def down_m3u8_url(m3u8_url):
print('获取m3u8文件')
# 获取m3u8文件
page_source = get_html(m3u8_url)
print(page_source)
with open('./m3u8需解密.txt','w',encoding='utf-8') as f:
f.write(page_source)
semaphore = asyncio.Semaphore(50)
async def download_one(session,url_ts): # 使用异步方法去下载,毕竟量太大了
for i in range(4): # 重试4次
try:
file_name = url_ts.split('/')[-1]
async with semaphore: #
async with session.get(url_ts,timeout=30) as resp: # 30s都没有就算了
content = await resp.content.read()
async with aiofiles.open(f"./电影_源_加密/{file_name}",mode="wb") as f:
await f.write(content)
print(url_ts,'下载成功')
break
except:
print('下载失败',url_ts)
await asyncio.sleep((i+1)*2) # 失败了就休息一下再爬
async def download_ts_all(session):
tasks = []
with open('./m3u8需解密.txt','r',encoding='utf-8') as f:
for line in f:
if line.startswith('#'):
continue
line = parse.urljoin(url,line)
task = asyncio.create_task(download_one(session,line))
tasks.append(task)
await asyncio.wait(tasks) # 把任务挂起,去等待执行
def get_key(): # 获取key
obj = re.compile(r'URI="(.*?)"',re.S)
with open('m3u8需解密.txt','r',encoding='utf-8') as f:
result = obj.findall(f.read())[0]
f.close()
url_key=parse.urljoin(url,result)
print(url_key)
key_str = get_html(url_key)
print(key_str.encode('utf-8'))
return key_str.encode('utf-8')
async def des_all_ts_file(key): # 获取文件名
tasks = []
with open('m3u8需解密.txt','r',encoding='utf-8') as f:
for line in f:
if line.startswith('#'):
continue
line = line.strip()
file_name = line.split('/')[-1]
# 依旧异步操作
task = asyncio.create_task(dec_one(file_name,key))
tasks.append(task)
await asyncio.wait(tasks)
# 解密 对称加密
async def dec_one(filename,key): # 解密
print(f"{filename}:开始解密")
# 加密解密对象开始创建,偏移量没有,模式MODE_CBC 这些信息几乎都在m3u8里边了
aes = AES.new(key=key,IV=b'0000000000000000',mode=AES.MODE_CBC)
async with aiofiles.open(f'./电影_源_加密/{filename}',mode='rb') as f1, \
aiofiles.open(f'./电影_源_解密后/{filename}', mode='wb') as f2:
# 解密后直接保存到另一个文件夹
content = await f1.read()
bc = aes.decrypt(content) # 解密
await f2.write(bc)
print(f"{filename}:解密完成")
def merge_ts(output_file): # 合并所有的ts文件,但是需要安装顺序去合并
with open('./m3u8需解密.txt', 'r', encoding='utf-8') as f1,\
open('./ts文件按顺序排列.txt','w',encoding='utf-8') as f2:
for line in f1:
if line.startswith('#'):
continue
line = parse.urljoin(url, line)
file_name = line.split('/')[-1]
f2.write(f"file './电影_源_解密后/{file_name}'\n") # 按顺序把这些名字个给存起来
# 执行 FFmpeg 合并命令
cmd = [
'ffmpeg',
'-f', 'concat',
'-safe', '0',
'-i', './ts文件按顺序排列.txt',
'-c', 'copy',
output_file
]
try:
subprocess.run(cmd, check=True)
print(f"合并完成: {output_file}")
except subprocess.CalledProcessError as e:
print(f"合并失败: {e}")
async def main():
# m3u8_txt = get_html(url)
m3u8_url = get_m3u8_url()
down_m3u8_url(m3u8_url)
async with aiohttp.ClientSession() as session:
await download_ts_all(session)
# 进行解密
# key = get_key()
# await(des_all_ts_file(key))
if __name__ == '__main__':
# asyncio.run(main())
# key = get_key()
# asyncio.run(des_all_ts_file(key))
# 合并视频输出
merge_ts('奇迹少女伦敦篇.mp4')