用 Python 处理 B 站下载视频
bilibili(哔哩哔哩,又称B站)是2009年6月推出的一个AGC相关的弹幕视频分享网站,是年轻人潮流文化的娱乐社区,可能对于听过但是不经常上b站的童鞋来说,对于b站最大的影响的就是二次元、动漫、弹幕等等。但是作为国内知名的弹幕视频网站,b站已经不仅仅局限于动漫,还有着丰富的学习资源。
作者本人常在b站搜索一些关于人工智能、机器学习类的视频资源,常常都是使用手机下载后离线观看,为了电脑观看方便也会使用“视频合并助手”一类的APP对视频进行转换处理后导入电脑观看,适逢春节假期再次下载视频想导入到电脑上观看,发现以前的视频转换APP已经失效,无法搜索到下载到手机里的b站视频资源,随后开始了下文描述的视频合成工作。
基本思路
目的:合成哔哩哔哩APP缓存到手机的文件,并转换为MP4格式
基本思路:
1.分析下载文件目录结构和缓存文件
2.使用库来合成文件开发环境:
1.手机:华为Mate20x EMUI10 哔哩哔哩APP 版本:5.53.1
2.开发环境:MacBook Pro 2015,PYTHON3.7.6 64-bit, Visual Studio Code 1.41.1
0x00 哔哩哔哩APP缓存文件目录结构及文件分析
打开手机文件管理器,找到Android/data/tv.danmaku.bili/down文件夹,结构如下图所示:
其中以8位数字命名的文件夹用于单个视频专辑的存储,其一级子目录是从数字1开始递增命名,每个目录内存储的是缓存的分节文件(可以理解为每一集)。其二级子目录均是以数字16命名,这是很规律的。
在每一个视频专辑下有以下几个文件:
1.在一级子目录下有danmaku.xml和entry.json,其中danmaku.xml为弹幕文件
<?xml version="1.0" encoding="UTF-8"?>
<i>
<chatserver>chat.bilibili.com</chatserver>
<chatid>132379211</chatid>
<mission>0</mission>
<maxlimit>3000</maxlimit>
<state>0</state>
<real_name>0</real_name>
<source>k-v</source>
<d p="22.23400,1,25,16777215,1575199941,0,aaaeeaeb,25196110486700034">地气儿</d>
<d p="1318.88600,1,25,16777215,1578391805,0,48b91c28,26869566679810052">指定了版本的那个装不上,只能装最新的</d>
<d p="582.62400,1,25,16777215,1578964914,0,15eedcf5,27170040630476802">nice</d>
<d p="26.29000,1,25,16777215,1579009720,0,c1d89d8e,27193531775320068">哈哈哈确实</d>
</i>
entry.json文件则是关于缓存视频的描述文件:
{
"media_type": 2,
"has_dash_audio": true,
"is_completed": true,
"total_bytes": 21176174,
"downloaded_bytes": 21176174,
"title": "(全)基于python的Opencv项目实战",
"type_tag": "16",
"cover": "http:\/\/i2.hdslb.com\/bfs\/archive\/afae181e4bb00d7ca2e97f192e6f11dc2c3d8142.jpg",
"prefered_video_quality": 16,
"guessed_total_bytes": 0,
"total_time_milli": 1152336,
"danmaku_count": 0,
"time_update_stamp": 1580398289030,
"time_create_stamp": 1580348758458,
"avid": 77390697,
"spid": 0,
"seasion_id": 0,
"bvid": "",
"page_data": {
"cid": 132379572,
"page": 6,
"from": "vupload",
"part": "06、边缘检测",
"link": "",
"rich_vid": "",
"vid": "",
"has_alias": false,
"weblink": "",
"offsite": "",
"tid": 39,
"width": 960,
"height": 540,
"rotate": 0,
"download_title": "视频已缓存完成",
"download_subtitle": "(全)基于python的Opencv项目实战 06、边缘检测"
}
}
我们需要从这个json中提取“download_subtitle”字段作为文件的命名。
2.在以“16”命名的二级子目录下有3个文件,从文件的名称可以判断audio.m4s和vedio.m4s两个文件应该是缓存的音频和视频文件,我们可以尝试使用播放器播放这两个文件,发现能够成功的播放但是视频中没有声音,可以断定B站将一个视频的音频和视频分开存储了。
{
"video": [{
"id": 16,
"base_url": "https:\/\/upos-sz-mirrorhw.bilivideo.com\/upgcxcode\/11\/92\/132379211\/132379211-1-30015.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&uipk=5&nbs=1&deadline=1580403879&gen=playurl&os=hwbv&oi=611071466&trid=69dea77b6c6a4049a6f88615e82ada05u&platform=android&upsig=1e3e3eedc71aba8b50ce51e67f3ca508&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,platform&mid=280178137",
"backup_url": ["https:\/\/upos-sz-mirrorks3.bilivideo.com\/upgcxcode\/11\/92\/132379211\/132379211-1-30015.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&uipk=5&nbs=1&deadline=1580403879&gen=playurl&os=ks3bv&oi=611071466&trid=69dea77b6c6a4049a6f88615e82ada05u&platform=android&upsig=6de0ffa1809d46318ca36387ca8d8634&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,platform&mid=280178137"],
"bandwidth": 104293,
"codecid": 7,
"size": 19069578,
"md5": "eab8c79d8ab56a973626a20e1dee6c25"
}],
"audio": [{
"id": 30216,
"base_url": "https:\/\/upos-sz-mirrorkodo.bilivideo.com\/upgcxcode\/11\/92\/132379211\/132379211-1-30216.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&uipk=5&nbs=1&deadline=1580403879&gen=playurl&os=kodobv&oi=611071466&trid=69dea77b6c6a4049a6f88615e82ada05u&platform=android&upsig=59e394e791a6ccb7c32b7d2eb1f0957d&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,platform&mid=280178137",
"backup_url": ["https:\/\/upos-sz-mirrorks3.bilivideo.com\/upgcxcode\/11\/92\/132379211\/132379211-1-30216.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&uipk=5&nbs=1&deadline=1580403879&gen=playurl&os=ks3bv&oi=611071466&trid=69dea77b6c6a4049a6f88615e82ada05u&platform=android&upsig=856c6fed7b7f7c1a4967a8e40cf8fc59&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,platform&mid=280178137"],
"bandwidth": 67113,
"codecid": 0,
"size": 12272062,
"md5": "5d7a6a8e6f4c2809ac61eeafa1d9eaae"
}]
}
这个json文件包含了音频文件和视频文件的相关信息。
0x01 合成音视频文件
通过上述分析,我们找到了单个专辑的缓存文件,下面需要做的就是将音轨合并到视频中去,为此我们需要使用Moviepy这个库。MoviePy是一个用于视频编辑的python模块,你可以用它实现一些基本的操作(比如视频剪辑,视频拼接,插入标题),还可以实现视频合成,还有视频处理,抑或用它加入一些自定义的高级的特效。此外,MoviePy可以读写绝大多数常见的视频格式,甚至包括GIF格式!详细的使用说明可以参考MoviePy - 中文文档和官方文档。
首先需要安装Moviepy库,使用pip直接安装即可,所需要的依赖库如numpy等会在安装时自动下载并配置:
pip install moviepy
安装完毕后即可使用,我们先用单个文件尝试一下,代码如下:
from moviepy.editor import VideoFileClip,AudioFileClip #从moviepy中导入editor包
audioFile = r"/Users/airwolf/Desktop/81427329/1/16/audio.m4s" #指定需要读取的音频文件
videoFile = r"/Users/airwolf/Desktop/81427329/1/16/video.m4s" #指定需要读取的视频文件
outputfile = r"/Users/airwolf/Desktop/81427329/output.mp4" #指定输出文件
video_in = VideoFileClip(videoFile) #读取视频文件
audio_in =AudioFileClip(audioFile) #读取音频文件
video_out = video_in.set_audio(audio) #video_out文件的输出是将音频文件合并到video_in文件的音轨中
video.write_videofile(outputfile) #输出video_out文件
此时我们用播放器打开输入文件output.mp4,发现音频文件已经合成到视频文件中了。
下一步我们就着手批量的文件合成,首先将音视频合成的方法封装成一个函数:
def set_audio(proc_file, output_path):
(file_name, audio_file, vedio_file) = proc_file
file_name = file_name.replace('.', '-').replace('“', "").replace('”', "")#对文件名称中含有的影响命名的特殊字符进行处理
original_vedio = VideoFileClip(vedio_file)
audio = AudioFileClip(audio_file)
video = original_vedio.set_audio(audio)
outputfile = os.path.join(output_path, file_name)+".mp4"#形成输出文件名
video.write_videofile(outputfile)
函数的输入有2个参数,参数proc_file表示要处理的文件信息,按照[文件名称,音频文件名,视频文件名]的列表形式输入,output_path为合成后的MP4文件输出路径。接着需要遍历视频专辑下所有的子目录,把待处理的视频放入一个proc_fileList列表中:
import os
import json
proc_fileLis=[]
def get_proList(init_path):
folder = os.listdir(init_path)
for subfolder in folder:
name_path = os.path.join(init_path, subfolder)
json_file = os.path.join(name_path, "entry.json")
if os.path.exists(json_file):
file_info = [] #用于封装带处理的文件信息,格式为:[文件名,音频文件名,视频文件名]
with open(json_file, 'r') as f: #从json文件中提取文件名
data = json.load(f)
file_name = data["page_data"]["part"]
file_info.append(file_name)
a_filename = os.path.join(name_path, "16/audio.m4s")
v_filename = os.path.join(name_path, "16/video.m4s")
file_info.append(a_filename)
file_info.append(v_filename)
proc_fileList.append(file_info)#将带处理文件加入到proc_fileList中
函数的输入参数init_path为待处理第一级目录,即上文所指的以8个数字命名的文件夹。
接着编写主函数:
import sys
if __name__ == "__main__":
init_path = sys.argv[1]
get_proList(init_path)
for proc_file in proc_fileList:
print(proc_file)
set_audio(proc_file,init_path)
最后保存为proc.py。
使用时打开终端,输入如下命令,即可完成视频的转换:
python proc.py 处理文件路径
作者简介:
Airwolf,非IT行业码农,国家嵌入式系统设计师。自小学6年级自学BASIC开启编程生涯,酷爱编程,喜欢用实用简洁的程序解决工作生活中的问题。
Python中文社区作为一个去中心化的全球技术社区,以成为全球20万Python中文开发者的精神部落为愿景,目前覆盖各大主流媒体和协作平台,与阿里、腾讯、百度、微软、亚马逊、开源中国、CSDN等业界知名公司和技术社区建立了广泛的联系,拥有来自十多个国家和地区数万名登记会员,会员来自以工信部、清华大学、北京大学、北京邮电大学、中国人民银行、中科院、中金、华为、BAT、谷歌、微软等为代表的政府机关、科研单位、金融机构以及海内外知名公司,全平台近20万开发者关注。