本文仅作为学习笔记参考使用:
哔哩哔哩视频抓取
b站视频的抓取还是比较困难的,相对于其他的网站的视频获取相对难一些,也是因为我的好奇不怕死心理,打算拿b站视频好好分析分析,具体抓取流程如下:
初始分析页面如下:
这个页面有点养眼,就从这个开始。本页面的请求以及响应信息较为容易抓取分析,主要就是获取分页标记以及视频跳转到指定播放界面的url地址(如下图所示)
我用的办法是把响应中的页面框架信息下载下来,然后与原始合并之后的Element页面对比,从而找出url,这一步不难找,就不多说了,直接上代码(en…大体代码就这样,我也懒得一点点分离了):
html_str01 = self.get_request(self.index_url, self.index_headers).content.decode("utf-8")
seconed_url_list = set(
re.findall(r'''href="//(www\.bilibili\.com/video/.*?\?from=search)"''', html_str01, re.S))
接下来,找到了初始url,我们就可以去分析原始视频抓取了:
下面就是来到这个页面,进行分析:
我们通过抓包,是获取不到完整的视频请求地址的,而是获取到一大批 .m4s格式文件的请求。初始猜测.m4s格式文件就是我们需要的视频文件,但是从数目上来看,是视频片段,可是,并不是所有的.m4s格式的请求都是一样的,大体分成(如下图所示的)这样几个请求,划掉的哪个肯定不是,因为响应为null,所以就剩下3类请求(30080 / 30216 / 30232)。
三种请求,那么哪一种是我需要的呢?继续分析,找了很多资料,得出结论,返回请求数据多的为视频格式,返回数据量少的是音频文件(如下图所示)
30080需要的请求的总数据大小
30216需要的请求的总数据大小
30232需要的请求的总数据大小
相比较而言,30080 > 30232 > 30216 的数据请求量
现在可以肯定的是 30080 这个数据请求的是视频文件,那么30232和30216这两个请求的文件,哪一个才是音频文件呢?
不慌,接着往下分析。
这里有个问题,30080文件有那么多个片段,我不能全部都下载下来然后再合成⑧,虽然这确实可行,但是比较复杂。还有更简单的方式获得整个视频文件,这个就需要在requests请求头里 把 Range(如下图所示) 这个参数改成 bytes 0-XXXXXXXX 这种形式,而这里的 XXXXXXXX 就是在上文中我们分析url类型的数据请求总数量,并且据我分析,这个值只能大于或者等于数据请求总量,而不能小于。所以这里有两种的方法获取完整的.m4s格式的方法,一种就是,把请求头的Range的值写的贼大,另一种就是,先请求 0-5 这样比较短的数据,进行返回头部试探,获取到响应头部以后再取到准确的数据量的值,然后再发送请求获取.m4s全部数据的请求,这样就获取到完整的.m4s格式的文件了
在这个分析的基础之上,对刚刚拿不准的 30232 和 30216格式的url请求进行对比,分别测试取得两种请求获取的数据,进行对比。如图所示:
我们下载下来两种请求到的数据,并全部保存为.mp3文件格式,通过本次测试得到,两个文件均为视频的音频文件,两者无差别。故此,我们请求稍微小一点的 30216。
这样,视频音频文件的获取就可以完成了,只不过,这里是分离开下载的,如果需要合成的话,这里说一下我尝试的两种方法:
【1】使用ffmpeg模块完成视音频合成
【2】使用 格式工厂
本程序中我没有使用ffmpeg来合成,原因由两个。一是因为实在是太慢了,并且转化过程中我笔记本cpu占用率升到了不可思议的 99%,二是因为格式工厂实在是太好用了,合成速度极快。故此,我选择了手动的格式工厂来合成视音频。
至此,整个分析流程结束,剩下的就是把整个程序写出来,这里就不说具体每个模块怎么写了,直接奉上代码截图及运行截图。
运行截图如下:
代码进行中部分保存结果展示:
视频播放展示:✔插一句,是高清,没错的
故此,本程序介绍结束:
部分码如下:
源码某部分
def run(self):
# print("第一次请求开始。。。。。")
html_str01 = self.get_request(self.index_url, self.index_headers).content.decode("utf-8")
# file_name = re.findall(r'''(.*?) ''', html_str01, re.S)[0] + ".mp4"
seconed_url_list = set(
re.findall(r'''href="//(www\.bilibili\.com/video/.*?\?from=search)"''', html_str01, re.S))
# print(seconed_url_list)
for seconed_url in seconed_url_list:
html_str02 = self.get_request("http://" + seconed_url, self.index_headers).content.decode("utf-8")
try:
m4s_30080 = re.findall(r'''"baseUrl":"(.*?)"''', html_str02, re.S)[0]
except Exception as e:
print(e)
continue
if self.audio_condition == 'Y':
mp3_30216 = re.findall(r'''"baseUrl":"(.*?)"''', html_str02, re.S)[-2]
# print(m4s_30080)
Referer_key = seconed_url
# 试探请求头大小
Range_key = 'bytes=0-5'
self.seconed_headers['Referer'] = 'https://' + Referer_key
self.seconed_headers['Range'] = Range_key
# 试探,取得total的值
html_bytes = self.get_request(m4s_30080, headers=self.seconed_headers).headers['Content-Range']
if self.audio_condition == 'Y':
audio_bytes = self.get_request(mp3_30216, headers=self.seconed_headers).headers['Content-Range']
# print(html_bytes)
total = re.findall(r"/(.*)", html_bytes, re.S)[0]
if self.audio_condition == 'Y':
audio_total = re.findall(r"/(.*)", audio_bytes, re.S)[0]
# print("total: " + str(total))
self.seconed_headers['Range'] = total
# print(total)
stream = True
chunk_size = 1024 # 每次块大小为1024
content_size = int(total)
if self.audio_condition == 'Y':
content_size_audio = int(audio_total)
print("文件大小:" + str(round(float((content_size + content_size_audio) / chunk_size / 1024), 4)) + "[MB]")
else:
print("文件大小:" + str(round(float(content_size / chunk_size / 1024), 4)) + "[MB]")
start = time.time()
m4s_bytes = self.get_request(m4s_30080, headers=self.seconed_headers, stream=stream)
self.write_data(str(self.num) + ".mp4", m4s_bytes, chunk_size, content_size)
if self.audio_condition == 'Y':
print("\n")
self.seconed_headers['Range'] = audio_total
mp3_bytes = self.get_request(mp3_30216, headers=self.seconed_headers, stream=stream)
self.write_data(str(self.num) + ".mp3", mp3_bytes, chunk_size, content_size_audio)
end = time.time()
print("总耗时:" + str(end - start) + "秒")
self.num = self.num + 1