手机b站视频如何下载到b站上 (还在为上不了王者而烦恼吗)

一、项目概述

1.项目背景

有一天,我突然想找点事做,想起一直想学但是没有学的C语言,就决定来学一下。可是怎么学呢?看书的话太无聊,报班学呢又快吃土了没钱,不如去B站看看?果然,关键字C语言搜索,出现了很多C语言的讲课视频:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

B站C语言讲课视频节选

B站https://www.bilibili.com/是一个很神奇的地方,简直就是一个无所不有的宝库,几乎可以满足你一切的需求和视觉欲。不管你是想看动画、番剧 ,还是游戏、鬼畜 ,亦或科技和各类教学视频 ,只要你能想到的,基本上都可以在B站找到。对于程序猿或即将成为程序猿的人来说,B站上的编程学习资源是学不完的,可是B站没有提供*载下**的功能,如果想保存*载下**在需要的时候看,那就是一个麻烦了。我也遇到了这个问题,于是研究怎么可以实现一键*载下**视频,最终用Python这门神奇的语言实现了。

当然了,项目实现之后,不是想学习编程、而是想看其他类别视频的小伙伴也可以用这款工具进行*载下**了。

2.环境配置

这次项目不需要太多的环境配置,最主要的是有ffmpeg(一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序)并设置环境变量就可以了。ffmpeg主要是用于将*载下**下来的视频和音频进行合并形成完整的视频。

*载下**ffmpeg

可点击https://download.csdn.net/download/CUFEECR/12234789或进入官网http://ffmpeg.org/download.html进行*载下**,并解压到你想保存的目录。

设置环境变量:

(1)复制ffmpeg的bin路径,如xxx\ffmpeg-20190921-ba24b24-win64-shared\bin

(2)此电脑右键点击属性,进入控制面板\系统和安全\系统

(3)点击高级系统设置→进入系统属性弹窗→点击环境变量→进入环境变量弹窗→选择系统变量下的Path→点击编辑点击→进入编辑环境变量弹窗

(4)点击新建→粘贴之前复制的bin路径

(5)点击确定,逐步保存退出

动态操作示例如下:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

ffmpeg 设置环境变量

除了ffmpeg,还需要安装pyinstaller库用于程序打包。可用以下命令进行安装:

pipinstallpyinstaller

如果遇到安装失败或*载下**速度较慢,可换源:

pipinstallpyinstaller-ihttps://pypi.doubanio.com/simple/

二、项目实施

1.导入需要的库

importjson
importos
importre
importshutil
importssl
importtime
importrequests
fromconcurrent.futuresimportThreadPoolExecutor
fromlxmlimportetree

导入的库包括用于爬取和解析网页的库,还包括创建线程池的库和进行其他处理的库,大多数都是Python自带的,如有未安装的库,可使用pip install xxx命令进行安装。

2.设置请求参数

#设置请求头等参数,防止被反爬
headers={
'Accept':'*/*',
'Accept-Language':'en-US,en;q=0.5',
'User-Agent':'Mozilla/5.0(WindowsNT10.0;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/80.0.3987.116Safari/537.36'
}
params={
'from':'search',
'seid':'9698329271136034665'
}

设置请求头等参数,减少被反爬的可能。

3.基本处理

defre_video_info(text,pattern):
'''利用正则表达式匹配出视频信息并转化成json'''
match=re.search(pattern,text)
returnjson.loads(match.group(1))


defcreate_folder(aid):
'''创建文件夹'''
ifnotos.path.exists(aid):
os.mkdir(aid)


defremove_move_file(aid):
'''删除和移动文件'''
file_list=os.listdir('./')
forfileinfile_list:
#移除临时文件
iffile.endswith('_video.mp4'):
os.remove(file)
pass
eliffile.endswith('_audio.mp4'):
os.remove(file)
pass
#保存最终的视频文件
eliffile.endswith('.mp4'):
ifos.path.exists(aid+'/'+file):
os.remove(aid+'/'+file)
shutil.move(file,aid)

主要包括两方面的基本处理,为正式爬取*载下**做准备:

  • 利用正则表达式提取信息通过requests库请求得到请求后的网页,属于文本,通过正则表达式提取得到关于将要*载下**的视频的有用信息,便于后一步处理。
  • 文件处理将*载下**视频完成后的相关文件进行处理,包括删除生成的临时的音视频分离的文件和移动最终视频文件到指定文件夹。

4.*载下**视频

defdownload_video_batch(referer_url,video_url,audio_url,video_name,index):
'''批量*载下**系列视频'''
#更新请求头
headers.update({"Referer":referer_url})
#获取文件名
short_name=video_name.split('/')[2]
print("%d.\t视频*载下**开始:%s"%(index,short_name))
#*载下**并保存视频
video_content=requests.get(video_url,headers=headers)
print('%d.\t%s\t视频大小:'%(index,short_name),
round(int(video_content.headers.get('content-length',0))/1024/1024,2),'\tMB')
received_video=0
withopen('%s_video.mp4'%video_name,'ab')asoutput:
headers['Range']='bytes='+str(received_video)+'-'
response=requests.get(video_url,headers=headers)
output.write(response.content)
#*载下**并保存音频
audio_content=requests.get(audio_url,headers=headers)
print('%d.\t%s\t音频大小:'%(index,short_name),
round(int(audio_content.headers.get('content-length',0))/1024/1024,2),'\tMB')
received_audio=0
withopen('%s_audio.mp4'%video_name,'ab')asoutput:
headers['Range']='bytes='+str(received_audio)+'-'
response=requests.get(audio_url,headers=headers)
output.write(response.content)
received_audio+=len(response.content)
returnvideo_name,index


defdownload_video_single(referer_url,video_url,audio_url,video_name):
'''单个视频*载下**'''
#更新请求头
headers.update({"Referer":referer_url})
print("视频*载下**开始:%s"%video_name)
#*载下**并保存视频
video_content=requests.get(video_url,headers=headers)
print('%s\t视频大小:'%video_name,round(int(video_content.headers.get('content-length',0))/1024/1024,2),'\tMB')
received_video=0
withopen('%s_video.mp4'%video_name,'ab')asoutput:
headers['Range']='bytes='+str(received_video)+'-'
response=requests.get(video_url,headers=headers)
output.write(response.content)
#*载下**并保存音频
audio_content=requests.get(audio_url,headers=headers)
print('%s\t音频大小:'%video_name,round(int(audio_content.headers.get('content-length',0))/1024/1024,2),'\tMB')
received_audio=0
withopen('%s_audio.mp4'%video_name,'ab')asoutput:
headers['Range']='bytes='+str(received_audio)+'-'
response=requests.get(audio_url,headers=headers)
output.write(response.content)
received_audio+=len(response.content)
print("视频*载下**结束:%s"%video_name)
video_audio_merge_single(video_name)

这部分包括系列视频的批量*载下**和单个视频的*载下**,两者的大体实现原理近似,但是由于两个函数的参数有差别,因此分别实现。在具体的实现中,首先更新请求头,请求视频链接并保存视频(无声音),再请求音频链接并保存音频,在这个过程中得到相应的视频和音频文件的大小。

5.视频和音频合并成完整的视频

defvideo_audio_merge_batch(result):
'''使用ffmpeg批量视频音频合并'''
video_name=result.result()[0]
index=result.result()[1]
importsubprocess
video_final=video_name.replace('video','video_final')
command='ffmpeg-i"%s_video.mp4"-i"%s_audio.mp4"-ccopy"%s.mp4"-y-loglevelquiet'%(
video_name,video_name,video_final)
subprocess.Popen(command,shell=True)
print("%d.\t视频*载下**结束:%s"%(index,video_name.split('/')[2]))


defvideo_audio_merge_single(video_name):
'''使用ffmpeg单个视频音频合并'''
print("视频合成开始:%s"%video_name)
importsubprocess
command='ffmpeg-i"%s_video.mp4"-i"%s_audio.mp4"-ccopy"%s.mp4"-y-loglevelquiet'%(
video_name,video_name,video_name)
subprocess.Popen(command,shell=True)
print("视频合成结束:%s"%video_name)

这个过程也是批量和单个分开,大致原理差不多,都是调用subprogress模块来生成子进程,Popen类来执行shell命令,由于已经将ffmpeg加入环境变量,所以shell命令可以直接调用ffmpeg来合并音视频。

6.3种*载下**方式的分别实现

defbatch_download():
'''使用多线程批量*载下**视频'''
#提示输入需要*载下**的系列视频对应的id
aid=input('请输入要*载下**的视频id(举例:链接https://www.bilibili.com/video/av91748877?p=1中id为91748877),默认为91748877\t')
ifaid:
pass
else:
aid='91748877'
#提示选择清晰度
quality=input('请选择清晰度(1代表高清,2代表清晰,3代表流畅),默认高清\t')
ifquality=='2':
pass
elifquality=='3':
pass
else:
quality='1'
acc_quality=int(quality)-1
#ssl模块,处理https请求失败问题,生成证书上下文
ssl._create_default_https_context=ssl._create_unverified_context
#获取视频主题
url='https://www.bilibili.com/video/av{}?p=1'.format(aid)
html=etree.HTML(requests.get(url,params=params,headers=headers).text)
title=html.xpath('//*[@id="viewbox_report"]/h1/span/text()')[0]
print('您即将*载下**的视频系列是:',title)
#创建临时文件夹
create_folder('video')
create_folder('video_final')
#定义一个线程池,大小为3
pool=ThreadPoolExecutor(3)
#通过api获取视频信息
res_json=requests.get('https://api.bilibili.com/x/player/pagelist?aid={}'.format(aid)).json()
video_name_list=res_json['data']
print('共*载下**视频{}个'.format(len(video_name_list)))
fori,video_contentinenumerate(video_name_list):
video_name=('./video/'+video_content['part']).replace("","-")
origin_video_url='https://www.bilibili.com/video/av{}'.format(aid)+'?p=%d'%(i+1)
#请求视频,获取信息
res=requests.get(origin_video_url,headers=headers)
#解析出视频详情的json
video_info_temp=re_video_info(res.text,'__playinfo__=(.*?)</script><script>')
video_info={}
#获取视频品质
quality=video_info_temp['data']['accept_description'][acc_quality]
#获取视频时长
video_info['duration']=video_info_temp['data']['dash']['duration']
#获取视频链接
video_url=video_info_temp['data']['dash']['video'][acc_quality]['baseUrl']
#获取音频链接
audio_url=video_info_temp['data']['dash']['audio'][acc_quality]['baseUrl']
#计算视频时长
video_time=int(video_info.get('duration',0))
video_minute=video_time//60
video_second=video_time%60
print('{}.\t当前视频清晰度为{},时长{}分{}秒'.format(i+1,quality,video_minute,video_second))
#将任务加入线程池,并在任务完成后回调完成视频音频合并
pool.submit(download_video_batch,origin_video_url,video_url,audio_url,video_name,i+1).add_done_callback(
video_audio_merge_batch)
pool.shutdown(wait=True)
time.sleep(5)
#整理视频信息
ifos.path.exists(title):
shutil.rmtree(title)
os.rename('video_final',title)
try:
shutil.rmtree('video')
except:
shutil.rmtree('video')


defmultiple_download():
'''批量*载下**多个独立视频'''
#提示输入所有aid
aid_str=input(
'请输入要*载下**的所有视频id,id之间用空格分开\n举例:有5个链接https://www.bilibili.com/video/av89592082、https://www.bilibili.com/video/av68716174、https://www.bilibili.com/video/av87216317、\nhttps://www.bilibili.com/video/av83200644和https://www.bilibili.com/video/av88252843,则输入8959208268716174872163178320064488252843\n默认为8959208268716174872163178320064488252843\t')
ifaid_str:
pass
else:
aid_str='8959208268716174872163178320064488252843'
ifos.path.exists(aid_str):
shutil.rmtree(aid_str)
aids=aid_str.split('')
#提示选择视频质量
quality=input('请选择清晰度(1代表高清,2代表清晰,3代表流畅),默认高清\t')
ifquality=='2':
pass
elifquality=='3':
pass
else:
quality='1'
acc_quality=int(quality)-1
#创建文件夹
create_folder(aid_str)
#创建线程池,执行多任务
pool=ThreadPoolExecutor(3)
foraidinaids:
#将任务加入线程池
pool.submit(single_download,aid,acc_quality)
pool.shutdown(wait=True)
time.sleep(5)
#删除临时文件,移动文件
remove_move_file(aid_str)


defsingle_download(aid,acc_quality):
'''单个视频实现*载下**'''
#请求视频链接,获取信息
origin_video_url='https://www.bilibili.com/video/av'+aid
res=requests.get(origin_video_url,headers=headers)
html=etree.HTML(res.text)
title=html.xpath('//*[@id="viewbox_report"]/h1/span/text()')[0]
print('您当前正在*载下**:',title)
video_info_temp=re_video_info(res.text,'__playinfo__=(.*?)</script><script>')
video_info={}
#获取视频质量
quality=video_info_temp['data']['accept_description'][acc_quality]
#获取视频时长
video_info['duration']=video_info_temp['data']['dash']['duration']
#获取视频链接
video_url=video_info_temp['data']['dash']['video'][acc_quality]['baseUrl']
#获取音频链接
audio_url=video_info_temp['data']['dash']['audio'][acc_quality]['baseUrl']
#计算视频时长
video_time=int(video_info.get('duration',0))
video_minute=video_time//60
video_second=video_time%60
print('当前视频清晰度为{},时长{}分{}秒'.format(quality,video_minute,video_second))
#调用函数*载下**保存视频
download_video_single(origin_video_url,video_url,audio_url,title)


defsingle_input():
'''单个文件*载下**,获取参数'''
#获取视频aid
aid=input('请输入要*载下**的视频id(举例:链接https://www.bilibili.com/video/av89592082中id为89592082),默认为89592082\t')
ifaid:
pass
else:
aid='89592082'
#提示选择视频质量
quality=input('请选择清晰度(1代表高清,2代表清晰,3代表流畅),默认高清\t')
ifquality=='2':
pass
elifquality=='3':
pass
else:
quality='1'
acc_quality=int(quality)-1
#调用函数进行*载下**
single_download(aid,acc_quality)

在一般情形下,*载下**的需求包含3种情况:

(1)单个视频的*载下**:只有一个视频,没有和它属于同一个系列的其他视频,如下图

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

单个视频*载下**

此时,除了右下方的相关推荐中的视频,没有其他视频,右上方只有弹幕列表、没有视频列表。为了代码的复用,将单个视频*载下**时提示用户输入需求的代码单独提取出来作为single_input(),*载下**的函数另外作为single_download(aid, acc_quality)函数实现,在该函数中:通过视频链接如https://www.bilibili.com/video/av89592082解析网页,得到相应的字符串并转化成json,如下:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

视频信息json转化

字符串json格式化可使用https://www.sojson.com/editor.html进行在线转化。获取到视频的标题、根据输入确定的视频质量、持续时长、视频链接和音频链接,并调用download_video_single()函数*载下**该视频。

(2)多个视频的*载下**:这里,多个视频之间是没有关系的,多个视频的*载下**实际上是先获取到所有的aid,并进行循环,对每个视频链接传入参数调用单个视频*载下**的函数即可。同时设立线程池,大小为3,既不会对资源有太大的要求,也能实现多任务、提高*载下**效率。

(3)系列视频的*载下**此时,多个视频属于同一系列,如https://www.bilibili.com/video/av91748877是一个课程系列,如下:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

系列视频

显然,此时右上方有视频列表,标明了有65个子视频,每个视频用p标识,如第2个视频就是https://www.bilibili.com/video/av91748877?p=2。对于所有视频,先获取到视频的相关信息,再加入进程池进行*载下**,并在任务结束之后回调函数video_audio_merge_batch()合并音视频,并进行文件整理。

7.主函数

defmain():
'''主函数,提示用户进行三种*载下**模式的选择'''
download_choice=input('请输入您需要*载下**的类型:\n1代表*载下**单个视频,2代表批量*载下**系列视频,3代表批量*载下**多个不同视频,默认*载下**单个视频\t')
#批量*载下**系列视频
ifdownload_choice=='2':
batch_download()
#批量*载下**多个单个视频
elifdownload_choice=='3':
multiple_download()
#*载下**单个视频
else:
single_input()


if__name__=='__main__':
'''调用主函数'''
main()

主函数中实现3种*载下**方式对应的函数的分别调用。

三、项目分析和说明

1.结果测试

对3种方式进行测试的效果如下:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

项目测试一:单个视频测试

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

项目测试二:系列视频测试

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

项目测试三:多个不同视频测试

3种*载下**情景的测试效果均较好,*载下**速度也能与一般的*载下**速度相媲美。代码可点击https://download.csdn.net/download/CUFEECR/12243122https://github.com/corleytd/Python_Crawling/blob/master/bilibili_downloader_1.py进行*载下**。

改进说明

B站网站也一直在变化,所以对于*载下**可能也会有一些变化,所以将改进的地方在下面列举出来:

(1)网址参数变化举例说明:这段时间发现B站一个视频系列的链接变成https://www.bilibili.com/video/BV1x7411M74h?p=65,即是无规律的字符串(可能是经过某种算法编码或加密得到的),现在从链接中不能得到视频(系列)的aid,这时候可以借助浏览器工具抓包查看数据来找到该视频的aid,如下:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

B站视频aid转bvid

在左侧寻找stat开头的请求,后边的参数即为aid,该请求api的完整链接为https://api.bilibili.com/x/web-interface/archive/stat?aid=91748877,所以可以直接在该链接中获取aid,也可以查看该请求的具体内容,可以看到第一个数据就是aid,我们也可以看到随机字符串就是bvid,可能是建立了aid和bvid的一一映射,找到aid就可以正常*载下**了。

2.软件打包

在命令行中,使路径位于代码所在路径运行

pyinstallerbilibili_downloader_1.py

打印

136INFO:PyInstaller:3.6
137INFO:Python:3.7.4
138INFO:Platform:Windows-10-10.0.18362-SP0
140INFO:wrotexxxx\Bili_Video_Batch_Download\bilibili_downloader_1.spec
205INFO:UPXisnotavailable.
209INFO:ExtendingPYTHONPATHwithpaths
['xxxx\\Bili_Video_Batch_Download',
'xxxx\\Bili_Video_Batch_Download']
210INFO:checkingAnalysis
211INFO:BuildingAnalysisbecauseAnalysis-00.tocisnonexistent
211INFO:Initializingmoduledependencygraph...
218INFO:Cachingmodulegraphhooks...
247INFO:Analyzingbase_library.zip...
5499INFO:Cachingmoduledependencygraph...
5673INFO:runningAnalysisAnalysis-00.toc
5702INFO:AddingMicrosoft.Windows.Common-Controlstodependentassembliesoffinalexecutable
requiredbyxxx\python\python37\python*ex.e**
6231INFO:Analyzingxxxx\Bili_Video_Batch_Download\bilibili_downloader_1.py
7237INFO:Processingpre-safeimportmodulehookurllib3.packages.six.moves
10126INFO:Processingpre-safeimportmodulehooksix.moves
14287INFO:Processingmodulehooks...
14288INFO:Loadingmodulehook"hook-certifi.py"...
14296INFO:Loadingmodulehook"hook-cryptography.py"...
14936INFO:Loadingmodulehook"hook-encodings.py"...
15093INFO:Loadingmodulehook"hook-lxml.etree.py"...
15097INFO:Loadingmodulehook"hook-pydoc.py"...
15099INFO:Loadingmodulehook"hook-xml.py"...
15330INFO:LookingforctypesDLLs
15334INFO:Analyzingrun-timehooks...
15339INFO:Includingrun-timehook'pyi_rth_multiprocessing.py'
15344INFO:Includingrun-timehook'pyi_rth_certifi.py'
15355INFO:Lookingfordynamiclibraries
15736INFO:Lookingforeggs
15737INFO:UsingPythonlibraryxxx\python\python37\python37.dll
15757INFO:Foundbindingredirects:
[]
15776INFO:Warningswrittentoxxxx\Bili_Video_Batch_Download\build\bilibili_downloader_1\war
n-bilibili_downloader_1.txt
15942INFO:Graphcross-referencewrittentoxxxx\Bili_Video_Batch_Download\build\bilibili_dow
nloader_1\xref-bilibili_downloader_1.html
15967INFO:checkingPYZ
15968INFO:BuildingPYZbecausePYZ-00.tocisnonexistent
15968INFO:BuildingPYZ(ZlibArchive)xxxx\Bili_Video_Batch_Download\build\bilibili_downloade
r_1\PYZ-00.pyz
16944INFO:BuildingPYZ(ZlibArchive)xxxx\Bili_Video_Batch_Download\build\bilibili_downloade
r_1\PYZ-00.pyzcompletedsuccessfully.
16980INFO:checkingPKG
16981INFO:BuildingPKGbecausePKG-00.tocisnonexistent
16981INFO:BuildingPKG(CArchive)PKG-00.pkg
17030INFO:BuildingPKG(CArchive)PKG-00.pkgcompletedsuccessfully.
17034INFO:Bootloaderxxx\python\python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run*ex.e**
17034INFO:checkingEXE
17035INFO:BuildingEXEbecauseEXE-00.tocisnonexistent
17035INFO:BuildingEXEfromEXE-00.toc
17037INFO:AppendingarchivetoEXExxxx\Bili_Video_Batch_Download\build\bilibili_downloader_
1\bilibili_downloader_1*ex.e**
17046INFO:BuildingEXEfromEXE-00.toccompletedsuccessfully.
17053INFO:checkingCOLLECT
17053INFO:BuildingCOLLECTbecauseCOLLECT-00.tocisnonexistent
17055INFO:BuildingCOLLECTCOLLECT-00.toc

出现INFO: Building EXE from EXE-00.toc completed successfully. 即打包成功。在当前路径下找到distbuild目录下的bilibili_downloader_1目录下的bilibili_downloader_1*ex.e**,即是打包后的软件。点击打开即可进行选择和输入,开始*载下**相应视频。测试示例如下:

如果手机不能下载2个b站怎么办,还在为不会侧方停车而苦恼吗

项目打包*载下**测试

bilibili_downloader_1*ex.e**的同级目录下可以看到*载下**保存的视频。

3.改进分析

该项目是小编进行B站视频*载下**的首次尝试,难免有很多不足,在实现的过程中和后期的总结中,可以看出还存在一些问题:

  • 还不能*载下**B站上的所有视频,目前局限于各种普通视频教程,不能*载下**直播视频、大会员番剧等,可以在后期进一步优化;
  • 代码过于繁琐,有不少功能类似的重复代码,可以进一步简化、提高代码的复用性;
  • 没有采取适当的措施应对B站的反爬,可能会因为请求过多而无法正常*载下**。可以在后期进行优化,使整个程序更加健壮。

4.合法性说明

  • 本项目的出发点是方便地*载下**B站上的学习视频,可以更好地学习各类教程,这对程序猿来说也是一种福利,但是绝不用与其他商业目的,所有读者可以参考执行思路和程序代码,但不能用于恶意和非法目的(恶意频繁*载下**视频、非法盈利等),如有违者请自行负责。
  • 本项目在实施的过程中可能参考了其他大佬的实现思路,如有侵犯他人利益,请联系更改或删除。
  • 本项目是B站视频批量*载下**系列的第一篇,有很多尚待改进的地方,后期会继续更新,欢迎各位读者交流指正,以期不断改进。