👋 前言
当你想听周董的歌的时候,你会发现到处都在收费,好在我们还有良心的bilibili用户提供了很多优质的视频,可以将视频下载到本地,建立自己喜欢的音乐集,随时播放。
💡 正文
1 如何进行下载
我们可以使用python的requests库获取你想下载视频页面的资源,通过二进制文件来保存到本地即可。
2 网站分析
2.1 资源分析
打开一个视频网站,通过F12打开控制台,选择元素获取到网页源码,通过分析找到视频音频等资源保存在一个script标签中,所以我们可以通过源码中资源获取到我们想要的内容,本文通过正则进行提取。
2.2 地址分析
根据对地址的分析发现,我们访问网站的地址,如:https://www.bilibili.com/video/BV11p4y1b7ej/,只有路径中最后的BV开头的内容不一致,可以断定为BV11p4y1b7ej这个就是当前视频的地址,所以后续批量下载的时候,我们只需要修改这个值即可
3 规划技术
编程语言为python,这里使用的是3.7.3,需要用到第三方的库,其中moviepy用来进行音频视频合并的,loguru用来日志打印
pip install moviepy
pip install requests
pip install loguru
4 实现思路
4.1 数据封装
根据前面的2.2的分析,我们每次访问的时候,只需要修改请求中BV的值即可,所以单独把数据放在data.py
文件中,使用字典进行保存
chn_dict = {
'BV11p4y1b7ej': '周杰伦《一路向北》', 'BV1KE411b7by': '周杰伦《轨迹》', 'BV1nY4y1o7tv': '周杰伦《龙拳》',
'BV1TY41167Tm': '周杰伦《你听得到》', 'BV1qD4y1U7fs': '周杰伦《七里香》', 'BV1sP411H7Ce': '周杰伦《黑色毛衣》',
'BV1Xe4y187Gn': '周杰伦《乱舞春秋》', 'BV1iB4y1E7XE': '周杰伦《借口》', 'BV1KG411G7NK': '周杰伦《搁浅》'
}
其中chn_dict
表示一个字典,字典中的键位请求地址后面的BV值,字典的值为下载资源后保存的文件名称
4.2 资源获取
新建一个名为download_by_dict.py
文件,这里将具体实现download的操作。
4.2.1 导入用到的库
import json
import os
import re
import sys
import time
import requests
from moviepy.editor import VideoFileClip, AudioFileClip
from loguru import logger
from dataclasses import dataclass, field
4.2.2 定义一个下载类
@dataclass
class DownloadVideo:
# 全局参数
# 请求头
headers = {
"referer": "https://www.bilibili.com/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
}
# 标题
title = None
# 网页源码
source = None
# 初始化参数
# 资源列表
source_list: dict
# 文件保存路径
dir: str
# 是否合并视频
is_video: bool = False
# 基础路径
basic_url: str = field(init=False)
def __post_init__(self) -> None:
"""
初始化
:return:
"""
self.basic_url = 'https://www.bilibili.com/video/{}/'
if not os.path.exists(self.dir):
logger.info('创建目录:{}'.format(self.dir))
os.makedirs(self.dir)
- 首先这里使用
@dataclass
对类进行装饰,简化类的编写,类似java中lombok的作用,这里不过多赘述。 - 初始化一些变量,其中:
title
和source
是用来保存文件没和源码的全局变量source_list
:表示资源,就是data.py
中定义的字典dir
:下载后保存的路径,初始化的时候对路径进行了判断,如果不存在就创建is_video
:判断是否进行音频视频合并,默认False
(不合并,就下载音乐听听才会用)basic_url
:基础路径,路径中{}是后面格式化时使用的
4.2.3 获取网站资源
def get_source_code(self, url: str) -> str:
"""
获取源码
:param url:
:return:
"""
return requests.get(url=url, headers=self.headers).text
url为请求地址,通过循环data.py
中的字典,使用键对基础路径进行format操作,即可获取完整路径
4.2.4 获取音频视频资源
def get_video_or_audio_content(self, type: str) -> bytes:
"""
获取视频音频资源
:param type: 类型(audio音频,video视频)
:return:
"""
play_info = re.findall('<script>window.__playinfo__=(.*?)</script>', self.source)[0]
json_data = json.loads(play_info)
source_url = json_data['data']['dash'][type][0]['baseUrl']
# pprint.pprint(source_url)
content = requests.get(url=source_url, headers=self.headers).content
return content
根据2.1分析的,通过页面源码使用正则获取视频音频的资源
其中该方法需要传入参数,根据不通的参数来决定下载音频还是视频
self.source
是页面源码
4.3 资源下载
4.3.1 下载音频视频
def download_audio(self) -> None:
"""
下载音频
:return:
"""
audio = self.get_video_or_audio_content('audio')
# print(f"开始下载<{self.title}>音频")
with open(f'{self.dir}\\{self.title}.mp3', mode='wb') as f:
f.write(audio)
logger.info(f"{self.title}音频下载完毕!")
def download_video(self) -> None:
"""
下载视频
:return:
"""
video = self.get_video_or_audio_content('video')
# print(f"开始下载<{self.title}>视频")
with open(f'{self.dir}\\{self.title}.mp4', mode='wb') as f:
f.write(video)
logger.info(f"{self.title}视频下载完毕!")
下载的文件,根据视频和音频分为mp4和mp3
4.3.2 合并音频视频
def merge(self) -> None:
"""
合并视频音频
:return:
"""
logger.info(f"开始合并{self.title}的视频音频!")
start = time.time()
video = VideoFileClip(f'{self.dir}\\{self.title}.mp4')
audio = AudioFileClip(f'{self.dir}\\{self.title}.mp3')
final_clip = video.set_audio(audio)
final_clip.write_videofile(f'{self.dir}\\{self.title}output.mp4')
os.remove(f'{self.dir}\\{self.title}.mp3')
os.remove(f'{self.dir}\\{self.title}.mp4')
logger.info(f"合并{self.title}的视频音频完毕,<耗时: {round(time.time() - start, 4)}秒>")
该方法就是通过已经下载后的音频视频,来进行合并,对文件的拓展名都在方法中写死,可以不用去管
4.3.3 完整下载过程
def download(self) -> None:
"""
下载完整视频:分别下载视频音频,然后合并
:return:
"""
for k, v in self.source_list.items():
# 获取源码
self.source = self.get_source_code(self.basic_url.format(k))
# 获取标题
self.title = v + '-' + k + '-' + str(int(time.time() * 1000))
if not self.is_video:
self.download_audio()
continue
self.download_audio()
self.download_video()
self.merge()
主函数进行下载操作:
- 遍历字典获取当前视频的源码,标题
- 判断是否合并,如果初始化类的时候,使用默认参数则不合并,就只会下载音频
- 如果初始化指定参数
is_video
为True,则下载音频视频并且合并
4.3.4 开始下载
if __name__ == '__main__':
from data import chn_dict1
source_dict = chn_dict1
dir = r'D:\video'
start = time.time()
dv = DownloadVideo(source_dict, dir, is_video=True)
dv.download()
logger.info(f"全部下载完毕,共耗时: {round(time.time() - start, 5)}秒")
4.4 结果展示
因为下载视频音频后还需要合并,所以导致时间用的比较久,可以使用多线程来节省时间。如果对视频没有要求,只需要下载音频听歌的话,那么在初始化类的时候is_video
使用默认参数False即可,只下载音频速度是很快的。
PS:本文只做技术分享交流,禁止用于商用,私自使用该技术对BiliBili如造成损失,自行承担后果。如对BiliBili造成影响,请联系我删除该文章。联系邮箱442331998@qq.com。