俗话说,兴趣所在,方能大展拳脚。so结合兴趣的学习才能事半功倍,更加努力专心,apparently本次任务是在视频网站爬取一些好看的小电影,地址不放(狗头保命)只记录过程。
实现功能:
从网站上爬取采用m3u8分段方式的视频文件,对加密的 "ts"文件解密,实现两种方式合并"ts"文件,为防止IP被封,使用代理,最后删除临时文件。
环境 &依赖
- Win10 64bit
- IDE:Pycharm
- Python 3.8
- Python-site-package:requests + BeautifulSoup + lxml + m3u8 + AES
在PyCharm中创建一个项目会创建一个临时目录存放环境和所需要的package包,所以要在PyCharm 中项目解释器(Project Interpreter)中添加所有需要的包,这张截图是本项目的包列表,红框中是所必须的包,其他有的包我也不知道做什么用的。
下面开始我们的正餐,爬取数据第一步我们需要解析目标网站,找到我们需要爬取视频的地址,F12打开开发者工具
很不幸,这个网站视频是经过包装采用m3u8视频分段方式加载
科普一下:m3u8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。但无论是哪种播放列表,其内部文字使用的都是 utf-8 编码。
当 m3u8 文件作为媒体播放列表(Meida Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。
OK,本着“没有解决不了的困难“的原则我们继续,依旧在开发者模式,从Elements模式切换到NetWork模式,去掉不需要的数据,我们发现了两个m3u8文件一个key文件和一个ts文件
分别点击之后我们可以 看到对应的地址
OK,现在地址已经拿到了,我们可以开始我们的数据下载之路了。
首先进行初始化,包括路径设置,请求头的伪装等,之后我们通过循环去下载所有ts文件,至于如何定义循环的次数我们可以通过将m3u8文件下载之后解析文件得到所有ts的列表,之后拼接地址然后循环就可以得到所有ts文件了。
第一层
-
#EXTM3U
-
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=500000,RESOLUTION=720x406
-
500kb/hls/index.m
3u
8
观察数据,不是真正路径,第二层路径在第三行可以看到,结合我们对网站源码分析再次拼接字符串请求:
第二层
-
#
EXT-X-VERSION:3
-
#
EXT-X-TARGETDURATION:2
-
#
EXT-X-MEDIA-SEQUENCE:0
-
#
EXT-X-KEY:METHOD=AES-128,URI="key.key"
-
#
EXTINF:2.000000,
-
IsZhMS5924000.ts
-
#
EXTINF:2.000000,
-
IsZhMS5924001.ts
-
-
#
EXT-X-ENDLIST
之后我们循环得到的TS列表,通过拼接地址下载视频片段。但是问题远远没有这么简单,我们下载的ts文件居然无法播放,通过对第二层下载得到的m3u8文件进行分析我们可以发现这一行代码:
#EXT-X-KEY:METHOD=AES-128,URI="key.key"
此网站采用AES方法对所有ts文件进行了加密,其中
METHOD=ASE-128 :说明此视频采用ASE-128方式进行加密,
URI=“key.key”:代表key的地址
综上所诉,感觉好难啊,好绕了,都拿到了视频还看不了,但是我们要坚持我们的初心不能放弃。Fortunately,我们应该庆幸Python强大的模块功能,这个问题我们可以通过下载AES模块解决。
完成之后我们需要将所有ts合并为一个MP4文件,最简单的在CMD命令下我们进入到视频所在路径然后执行:
copy /b *.ts fileName.mp4
需要注意所有TS文件需要按顺序排好。在本项目中我们使用os模块直接进行合并和删除临时ts文件操作。
完整代码:
方法一:
-
import re
-
import requests
-
import m3u8
-
import time
-
import os
-
from bs4
import BeautifulSoup
-
import json
-
from Crypto.Cipher
import AES
-
-
class VideoCrawler():
-
def __init__(self,url):
-
super(VideoCrawler, self).__init__()
-
self.url=url
-
self.down_path=
r"F:\Media\Film\Temp"
-
self.final_path=
r"F:\Media\Film\Final"
-
self.headers={
-
'Connection':
'Keep-Alive',
-
'Accept':
'text/html,application/xhtml+xml,*/*',
-
'User-Agent':
'Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; MZ-m2 note Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 MZBrowser/6.5.506 UWS/2.10.1.22 Mobile Safari/537.36'
-
}
-
def get_url_from_m3u8(self,readAdr):
-
print(
"正在解析真实下载地址...")
-
with open(
'temp.m3u8',
'wb')
as file:
-
file.write(requests.get(readAdr).content)
-
m3u8Obj=m3u8.load(
'temp.m3u8')
-
print(
"解析完成")
-
return m3u8Obj.segments
-
-
def run(self):
-
print(
"Start!")
-
start_time=time.time()
-
os.chdir(self.down_path)
-
html=requests.get(self.url).text
-
bsObj=BeautifulSoup(html,
'lxml')
-
tempStr = bsObj.find(class_=
"iplays").contents[
3].string
#通过class查找存放m3u8地址的组件
-
firstM3u8Adr=json.loads(tempStr.strip(
'var player_data='))[
"url"]
#得到第一层m3u8地址
-
tempArr=firstM3u8Adr.rpartition(
'/')
-
realAdr=
"%s/500kb/hls/%s"%(tempArr[
0],tempArr[
2])
#一定规律下对字符串拼接得到第二层地址, 得到真实m3u8下载地址,
-
key_url=
"%s/500kb/hls/key.key"%tempArr[
0]
#分析规律对字符串拼接得到key的地址
-
key=requests.get(key_url).content
-
fileName=bsObj.find(class_=
"video-title w100").contents[
0].contents[
0]
#从源码中找到视频名称的规律
-
fileName=re.sub(
r'[\s,!]',
'',fileName)
#通过正则表达式去掉中文名称中的感叹号逗号和空格等特殊字符串
-
cryptor=AES.new(key,AES.MODE_CBC,key)
#通过AES对ts进行解密
-
urlList=self.get_url_from_m3u8(realAdr)
-
urlRoot=tempArr[
0]
-
i=
1
-
for url
in urlList:
-
resp=requests.get(
"%s/500kb/hls/%s"%(urlRoot,url.uri),headers=crawler.headers)
-
if len(key):
-
with open(
'clip%s.ts' % i,
'wb')
as f:
-
f.write(cryptor.decrypt(resp.content))
-
print(
"正在下载clip%d" % i)
-
else:
-
with open(
'clip%s.ts'%i,
'wb')
as f:
-
f.write(resp.content)
-
print(
"正在下载clip%d"%i)
-
i+=
1
-
print(
"下载完成!总共耗时%d s"%(time.time()-start_time))
-
print(
"接下来进行合并......")
-
os.system(
'copy/b %s\\*.ts %s\\%s.ts'%(self.down_path,self.final_path,fileName))
-
print(
"删除碎片源文件......")
-
files=os.listdir(self.down_path)
-
for filena
in files:
-
del_file=self.down_path+
'\\'+filena
-
os.remove(del_file)
-
print(
"碎片文件删除完成")
-
if __name__==
'__main__':
-
crawler=VideoCrawler(
"地址大家自己找哦")
-
crawler.start()
-
crawler2=VideoCrawler(
"地址大家自己找哦")
-
crawler2.start()
方法二
在方法一中我们是下载所有ts片段到本地之后在进行合并,其中有可能顺序会乱,有时候解密的视频还是无法播放合并之后会导致整个视频时间轴不正确而且视频根本不能完整播放,在经过各种努力,多方查资料之后有的问题还是得不到完美解决,最后突发奇想,有了一个新的想法,我们不必把所有ts片段都下载到本地之后进行合并,而是采用另一种思维模式,一开始我们只创建一个ts文件,然后每次循环的时候不是去下载ts文件而是将通过地址得到的视频片段文件流直接添加到我们一开始创建的ts文件中,如果出现错误跳出当前循环并继续下次操作,最后我们直接得到的就是一个完整的ts文件,还不需要去合并所有片段。具体看代码如何实现。
本代码好多地方和上面都一样,我们只需要领悟其中的原理和方法就OK了
-
import re
-
import requests
-
import m3u8
-
import time
-
import os
-
from bs4
import BeautifulSoup
-
import json
-
from Crypto.Cipher
import AES
-
import sys
-
import random
-
-
class VideoCrawler():
-
def __init__(self,url):
-
super(VideoCrawler, self).__init__()
-
self.url=url
-
self.down_path=
r"F:\Media\Film\Temp"
-
self.agency_url=
'https://www.kuaidaili.com/free/'
#获取免费代理的网站,如果网站过期或者失效,自己找代理网站替换
-
self.final_path=
r"F:\Media\Film\Final"
-
self.headers={
-
'Connection':
'Keep-Alive',
-
'Accept':
'text/html,application/xhtml+xml,*/*',
-
'User-Agent':
'Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; MZ-m2 note Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 MZBrowser/6.5.506 UWS/2.10.1.22 Mobile Safari/537.36'
-
}
-
def get_url_from_m3u8(self,readAdr):
-
print(
"正在解析真实下载地址...")
-
with open(
'temp.m3u8',
'wb')
as file:
-
file.write(requests.get(readAdr).content)
-
m3u8Obj=m3u8.load(
'temp.m3u8')
-
print(
"解析完成")
-
return m3u8Obj.segments
-
def get_ip_list(self,url, headers):
-
web_data = requests.get(url, headers=headers).text
-
soup = BeautifulSoup(web_data,
'lxml')
-
ips = soup.find_all(
'tr')
-
ip_list = []
-
for i
in range(
1, len(ips)):
-
ip_info = ips[i]
-
tds = ip_info.find_all(
'td')
-
ip_list.append(tds[
0].text +
':' + tds[
1].text)
-
return ip_list
-
def get_random_ip(self,ip_list):
-
proxy_list = []
-
for ip
in ip_list:
-
proxy_list.append(
'http://' + ip)
-
proxy_ip = random.choice(proxy_list)
-
proxies = {
'http': proxy_ip}
-
return proxies
-
-
def run(self):
-
print(
"Start!")
-
start_time=time.time()
-
self.down_path =
r"%s\%s" % (self.down_path, uuid.uuid1())
#拼接新的下载地址
-
if
not os.path.exists(self.down_path):
#判断文件是否存在,不存在则创建
-
os.mkdir(self.down_path)
-
html=requests.get(self.url).text
-
bsObj=BeautifulSoup(html,
'lxml')
-
tempStr = bsObj.find(class_=
"iplays").contents[
3].string
#通过class查找存放m3u8地址的组件
-
firstM3u8Adr=json.loads(tempStr.strip(
'var player_data='))[
"url"]
#得到第一层m3u8地址
-
tempArr=firstM3u8Adr.rpartition(
'/')
-
all_content = (requests.get(firstM3u8Adr).text).split(
'\n')[
2]
#从第一层m3u8文件中中找出第二层文件的的地址
-
midStr = all_content.split(
'/')[
0]
#得到其中有用的字符串,这个针对不同的网站采用不同的方法自己寻找其中的规律
-
realAdr =
"%s/%s" % (tempArr[
0], all_content)
#一定规律下对字符串拼接得到第二层地址, 得到真实m3u8下载地址,
-
key_url =
"%s/%s/hls/key.key" % (tempArr[
0], midStr)
#分析规律对字符串拼接得到key的地址
-
key_html = requests.head(key_url)
#访问key的地址得到的文本
-
status = key_html.status_code
#是否成功访问到key的地址
-
key =
""
-
if status ==
200:
-
all_content=requests.get(realAdr).text
#请求第二层m3u8文件地址得到内容
-
if
"#EXT-X-KEY"
in all_content:
-
key = requests.get(key_url).content
#如果其中有"#EXT-X-KEY"这个字段说明视频被加密
-
self.fileName = bsObj.find(class_=
"video-title w100").contents[
0].contents[
0]
#分析网页得到视频的名称
-
self.fileName=re.sub(
r'[\s,!]',
'',self.fileName)
#因为如果文件名中有逗号感叹号或者空格会导致合并时出现命令不正确错误,所以通过正则表达式直接去掉名称中这些字符
-
iv =
b'abcdabcdabcdabcd'
#AES解密时候凑位数的iv
-
if len(key):
#如果key有值说明被加密
-
cryptor = AES.new(key, AES.MODE_CBC, iv)
#通过AES对ts进行解密
-
urlList=self.get_url_from_m3u8(realAdr)
-
urlRoot=tempArr[
0]
-
i=
1
-
outputfile=open(os.path.join(self.final_path,
'%s.ts'%self.fileName),
'wb')
#初始创建一个ts文件,之后每次循环将ts片段的文件流写入此文件中从而不需要在去合并ts文件
-
ip_list=self.get_ip_list(self.agency_url,self.headers)
#通过网站爬取到免费的代理ip集合
-
for url
in urlList:
-
try:
-
proxies=self.get_random_ip(ip_list)
#从ip集合中随机拿到一个作为此次访问的代理
-
resp = requests.get(
"%s/%s/hls/%s" % (urlRoot, midStr, url.uri), headers=crawler.headers,proxies=proxies)
#拼接地址去爬取数据,通过模拟header和使用代理解决封IP
-
if len(key):
-
tempText=cryptor.decrypt(resp.content)
#解密爬取到的内容
-
progess=i/len(urlList)
#记录当前的爬取进度
-
outputfile.write(tempText)
#将爬取到ts片段的文件流写入刚开始创建的ts文件中
-
sys.stdout.write(
'\r正在下载:%s,进度:%s %%'%(self.fileName,progess))
#通过百分比显示下载进度
-
sys.stdout.flush()
#通过此方法将上一行代码刷新,控制台只保留一行
-
else:
-
outputfile.write(resp.content)
-
except Exception
as e:
-
print(
"\n出现错误:%s",e.args)
-
continue
#出现错误跳出当前循环,继续下次循环
-
i+=
1
-
outputfile.close()
-
print(
"下载完成!总共耗时%d s"%(time.time()-start_time))
-
self.del_tempfile()
#删除临时文件
-
def del_tempfile(self):
-
file_list=os.listdir(self.down_path)
-
for i
in file_list:
-
tempPath=os.path.join(self.down_path,i)
-
os.remove(tempPath)
-
os.rmdir(self.down_path)
-
print(
'临时文件删除完成')
-
if __name__==
'__main__':
-
url=input(
"输入地址:\n")
-
crawler=VideoCrawler(url)
-
crawler.run()
-
quitClick=input(
"请按Enter键确认退出!")
碰到的问题:
一、一开始以为电脑中Python环境中有模块就OK了,最后发现在Pycharm中自己虚拟的环境中还需要添加对应模块,
二、No module named Crypto.Cipher ,网上看了很多最后通过添加pycryptodome模块解决,电脑环境Win10
三、文件名不能有感叹号,逗号或者空格等这些特殊字符,不然执行合并命令的时候会提示命令不正确
四、在下载中将ts文件流写入文件时会出现这种错误('Data must be padded to 16 byte boundary in CBC mode',) Data must be padded,我们直接continue跳出当前循环继续下次下载。
五、有时出现 “Protocol Error, Connection abort, os.error”,应该是爬取操作太频繁ip被封,针对此问题我们使用免费代理。
ok 我们可以生成exe去开心的下载自己想要的视频了,happy!!!
参考链接:
转载:https://blog.csdn.net/a1191835397/article/details/107147105