本文使用 Zhihu On VSCode 创作并发布
环境
用到的 Python 库:
- Python 3.7
- requests
- moviepy
- json
- re
- os
浏览器:Firefox/ 83.0
访问测试
打开一个视频网址,如(https://www.bilibili.com/video/BV1E4411e7ir),然后直接打开发者工具,转到网络,选择XHR文件,再点击播放视频。可以看到很快就传输了很多文件。
![80d9231d66bf6f24f9b304150cc7fac0.png](https://i-blog.csdnimg.cn/blog_migrate/a7bc25a6bcf86e8ffe1564d969bc9f40.jpeg)
因为 B 站是把音频和视频分开传输的,所以很明显,一种是视频,另一种就是音频。按大小来分的话,30080 是视频,30280 是音频文件。
首先用试着获取其中的一个文件,来测试一下。 先把请求视频 url 复制下来,再把请求头弄下来,接着发送个请求。
import requests
headers = {
'Accept': '*/*',
'Accept-Language': 'zh,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br',
'TE': 'Trailers',
'Range': 'bytes=1431-391742',
'Origin': 'https://www.bilibili.com',
'Connection': 'keep-alive',
'DNT': '1',
'Referer': 'https://www.bilibili.com/video/BV1E4411e7ir',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
}
url = 'https://upos-sz-mirrorks3.bilivideo.com/upgcxcode/40/06/91280640/91280640-1-30280.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&uipk=5&nbs=1&deadline=1605719498&gen=playurl&os=ks3bv&oi=2028921166&trid=cfe12b1b9d1b4b58bf13bb1d08429d3au&platform=pc&upsig=00dbb5691fd73fdc2b8e3a834e50ad15&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,platform&mid=11418834&orderid=0,3&agrr=0&logo=80000000'
res = requests.get(url, headers=headers)
with open('test.flv', 'wb') as fp:
fp.write(res.content)
然而请求之后发现 flv 文件是空的,再看一下发回的请求文本,显示 403 禁止错误:
<html>
<head>
<title>403 Forbidden</title>
</head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr />
<center>QCMAS/V2</center>
</body>
</html>
再仔细观察一下抓包情况,在发送请 get 请求之前,浏览器会发送两个 options 请求,应该是请求许可的意思。分别是请求音频许可和请求视频许可,因为请求 url 与请求音视频的 url 相同。
![0fa9e05f9e91cc193432f8a74a7a7f1e.png](https://i-blog.csdnimg.cn/blog_migrate/b013dc52f0287ba460d0f02501c3b9e1.jpeg)
import requests
# 请求音视频的请求头
headers_1 = {
'Accept': '*/*',
'Accept-Language': 'zh,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br',
'TE': 'Trailers',
'Range': 'bytes=1431-391742',
'Origin': 'https://www.bilibili.com',
'Connection': 'keep-alive',
'DNT': '1',
'Referer': 'https://www.bilibili.com/video/BV1E4411e7ir',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
}
# 请求的OPTIONS的请求头
headers_2 = {
'Host': 'cn-gdgz4-cmcc-v-10.bilivideo.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
'Accept': '*/*',
'Accept-Language': 'zh,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'range',
'Referer': 'https://www.bilibili.com/video/BV1E4411e7ir',
'Origin': 'https://www.bilibili.com',
'DNT': '1',
'Connection': 'keep-alive',
'Cache-Control': 'max-age=0',
}
url = 'https://upos-sz-mirrorcos.bilivideo.com/upgcxcode/40/06/91280640/91280640-1-30080.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&uipk=5&nbs=1&deadline=1605752758&gen=playurl&os=cosbv&oi=2028921166&trid=18e55ae8ed4d41018ee9374a63501860u&platform=pc&upsig=346e8bea6d225d1ea592bb5a0c470c48&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,platform&mid=11418834&orderid=0,3&agrr=1&logo=80000000'
session = requests.session()
session.get(url, headers=headers_2)
res = session.get(url, headers=headers_1)
with open('test.flv', 'wb') as fp:
fp.write(res.content)
可以看到,test.flv 有文件大小了。
![0b36c05e4229c8f84b040a6398991dee.png](https://i-blog.csdnimg.cn/blog_migrate/dedebdbb39256f7fd6e9e91abda70468.png)
但打开时显示解析错误:
![e00a276c222eaf8c23e5c1adc579d36b.png](https://i-blog.csdnimg.cn/blog_migrate/a3a312eb1fd24e75a8fb158fe766947d.png)
再看一下多个不同请求的请求头,只有 range 发生明显的改变,而且 range 的值里的 bytes 参数,说明这很有可能是一个下载的文件大小范围。那么找到最后一个发送视频请求的的包,把最大值复制下来,然后再设置请求头里的 range 值为 0-最大值,即'Range':'bytes=0-29271958'
,然后再次运行 py 文件。很明显这次请求回来的文件比之前的大了许多,再点击播放,解析成功,有画面,但是没有声音。
![7a49ba40ffce18915441b42d7e9b4431.png](https://i-blog.csdnimg.cn/blog_migrate/149b155042ea5742723fef395a19854a.jpeg)
找到 url 地址
要找到请求 url 肯定不能在在抓包里找到,可以尝试看下网页的源代码。 复制一点 url 的信息,在网页中查找,果然找到了信息。
![82727bbcfc7909ef9025260a2ff02bf6.png](https://i-blog.csdnimg.cn/blog_migrate/d2a9826b2e9132c8278dbebeda24e459.png)
window.__playinfo__
里,然后把这个 json 提取出来,放到一个 json 文件里,再用 Firefox 打开。
![4f9bca30ade75e0c80b994a7c18f229e.png](https://i-blog.csdnimg.cn/blog_migrate/b107d70b30926325af981be18582e66a.png)
![9fab13dbdda37cf3cf118099bf239aac.png](https://i-blog.csdnimg.cn/blog_migrate/421476a4564d6916f1f9c56d9c0aafa3.png)
'id': 116
指就是高清 1080p60。 视频的在里面,那么音频的 url 也在
audio
这个键里面。
提取也很容易,先把 window._playinfo
用正则表达式获取到,再将其转为 python 的 json 对象,然后就可以取出来了。
请求的 range 参数怎么设置呢?可以把其删去,或者设为'range': 'bytes=0-'
,这样就会请求一个全文件了。
## 请求视频页面,注意此时的请求头不是同一个
res = session.get(url, headers=headers)
text = res.text
text = re.findall(r'<script>window.__playinfo__=(.*?)</script>', text)[0]
json_data = json.loads(text)
# 一般有多种格式可选,优先选1080p,没有就选720p。什么?没有720p!这样的视频还有下载的必要?
v_url = json_data['data']['dash']['video'][0]['baseUrl']
a_url = json_data['data']['dash']['audio'][0]['baseUrl']
# 获取准许
session.options(v_url, headers=headers_2)
session.options(a_url, headers=headers_2)
# 获取数据
video_content = session.get(v_url, headers=headers_1).content
audio_content = session.get(a_url, headers=headers_1).content
# 保存
with open('test.mp3', 'wb') as fp:
fp.write(audio_content)
with open('test.flv, 'wb') as fp:
fp.write(video_content)
弹幕
要获取 B 站的弹幕,首先得知道 B 站的弹幕文件是从哪加载的。B 站的弹幕文件放在http://comment.bilibili.com/{cid}.xml
,所以要获取弹幕,就要获取 B 站的视频的 cid。 视频的 cid 号在页面也可以找到,所以用正则表达式提取。
![54bc005bba83fa4f83f09345af7aced6.png](https://i-blog.csdnimg.cn/blog_migrate/e6677e6ad581ff6d458f6b6563a4d6a6.png)
text = res.text
# 获取cid
cid = re.findall(r'cid=([d]+)', text)[0]
# 获取弹幕文件
xml = 'http://comment.bilibili.com/{}.xml'.format(cid)
danmaku = session.get(xml, headers=headers)
# 不设置的话,提取会乱码
danmaku.encoding = 'utf-8'
# 保存
with open(cid+'.xml', 'w', encoding='utf-8') as fp:
fp.write(danmaku.text)
封面
封面也可以在页面找到。
text = res.text
cover_url = re.findall(
r'<meta data-vue-meta="true" itemprop="image" content="(.*?)">', text)[0]
合并音视频
这里使用 python 的 moviepy 库,安装命令pip install moviepy
。
导入 from moviepy.editor import *
,然后合并导出。
video = VideoFileClip(video_name)
audio = AudioFileClip(audio_name)
new_video = video.set_audio(audio)
new_video.to_videofile("test.mp4", fps=24, remove_temp=False)
os.remove(video_name)
os.remove(audio_name)
代码
完全代码查看我的 Github 地址