一、項目概述
1.項目背景
有一天,我突然想找點事做,想起一直想學但是沒有學的C語言,就決定來學一下。可是怎麼學呢?看書的話太無聊,報班學呢又快吃土了沒錢,不如去B站看看?果然,關鍵字C語言搜索,出現了很多C語言的講課視頻:

B站C語言講課視頻節選
B站是一個很神奇的地方,簡直就是一個無所不有的寶庫,幾乎可以滿足你一切的需求和視覺欲。不管你是想看動畫、番劇 ,還是遊戲、鬼畜 ,亦或科技和各類教學視頻 ,只要你能想到的,基本上都可以在B站找到。對於程序猿或即將成為程序猿的人來說,B站上的編程學習資源是學不完的,可是B站沒有提供下載的功能,如果想保存下載在需要的時候看,那就是一個麻煩了。我也遇到了這個問題,於是研究怎麼可以實現一鍵下載視頻,最終用Python這門神奇的語言實現了。
當然了,項目實現之後,不是想學習編程、而是想看其他類別視頻的小夥伴也可以用這款工具進行下載了。
2.環境配置
這次項目不需要太多的環境配置,最主要的是有ffmpeg(一套可以用來記錄、轉換數字音頻、視頻,並能將其轉化為流的開源計算機程序)並設置環境變量就可以了。ffmpeg主要是用於將下載下來的視頻和音頻進行合併形成完整的視頻。
下載ffmpeg
可點擊或進入官網進行下載,並解壓到你想保存的目錄。
設置環境變量:
(1)複製ffmpeg的bin路徑,如xxx
ffmpeg-20190921-ba24b24-win64-sharedbin
(2)此電腦右鍵點擊屬性,進入控制面板系統和安全系統
(3)點擊高級系統設置→進入系統屬性彈窗→點擊環境變量→進入環境變量彈窗→選擇系統變量下的Path→點擊編輯點擊→進入編輯環境變量彈窗
(4)點擊新建→粘貼之前複製的bin路徑
(5)點擊確定,逐步保存退出
動態操作示例如下:

ffmpeg 設置環境變量
除了ffmpeg,還需要安裝pyinstaller庫用於程序打包。可用以下命令進行安裝:
pip install pyinstaller
如果遇到安裝失敗或下載速度較慢,可換源:
pip install pyinstaller -i https://pypi.doubanio.com/simple/
二、項目實施
1.導入需要的庫
import json
import os
import re
import shutil
import ssl
import time
import requests
from concurrent.futures import ThreadPoolExecutor
from lxml import etree
導入的庫包括用於爬取和解析網頁的庫,還包括創建線程池的庫和進行其他處理的庫,大多數都是Python自帶的,如有未安裝的庫,可使用pip install xxx命令進行安裝。
2.設置請求參數
# 設置請求頭等參數,防止被反爬
headers = {
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36'
}
params = {
'from': 'search',
'seid': '9698329271136034665'
}
設置請求頭等參數,減少被反爬的可能。
3.基本處理
def re_video_info(text, pattern):
'''利用正則表達式匹配出視頻信息並轉化成json'''
match = re.search(pattern, text)
return json.loads(match.group(1))
def create_folder(aid):
'''創建文件夾'''
if not os.path.exists(aid):
os.mkdir(aid)
def remove_move_file(aid):
'''刪除和移動文件'''
file_list = os.listdir('./')
for file in file_list:
# 移除臨時文件
if file.endswith('_video.mp4'):
os.remove(file)
pass
elif file.endswith('_audio.mp4'):
os.remove(file)
pass
# 保存最終的視頻文件
elif file.endswith('.mp4'):
if os.path.exists(aid + '/' + file):
os.remove(aid + '/' + file)
shutil.move(file, aid)
主要包括兩方面的基本處理,為正式爬取下載做準備:
- 利用正則表達式提取信息
通過requests庫請求得到請求後的網頁,屬於文本,通過正則表達式提取得到關於將要下載的視頻的有用信息,便於後一步處理。 - 文件處理
將下載視頻完成後的相關文件進行處理,包括刪除生成的臨時的音視頻分離的文件和移動最終視頻文件到指定文件夾。
4.下載視頻
def download_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%st視頻大小:' % (index, short_name),
round(int(video_content.headers.get('content-length', 0)) / 1024 / 1024, 2), 'tMB')
received_video = 0
with open('%s_video.mp4' % video_name, 'ab') as output:
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%st音頻大小:' % (index, short_name),
round(int(audio_content.headers.get('content-length', 0)) / 1024 / 1024, 2), 'tMB')
received_audio = 0
with open('%s_audio.mp4' % video_name, 'ab') as output:
headers['Range'] = 'bytes=' + str(received_audio) + '-'
response = requests.get(audio_url, headers=headers)
output.write(response.content)
received_audio += len(response.content)
return video_name, index
def download_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('%st視頻大小:' % video_name, round(int(video_content.headers.get('content-length', 0)) / 1024 / 1024, 2), 'tMB')
received_video = 0
with open('%s_video.mp4' % video_name, 'ab') as output:
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('%st音頻大小:' % video_name, round(int(audio_content.headers.get('content-length', 0)) / 1024 / 1024, 2), 'tMB')
received_audio = 0
with open('%s_audio.mp4' % video_name, 'ab') as output:
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.視頻和音頻合併成完整的視頻
def video_audio_merge_batch(result):
'''使用ffmpeg批量視頻音頻合併'''
video_name = result.result()[0]
index = result.result()[1]
import subprocess
video_final = video_name.replace('video', 'video_final')
command = 'ffmpeg -i "%s_video.mp4" -i "%s_audio.mp4" -c copy "%s.mp4" -y -loglevel quiet' % (
video_name, video_name, video_final)
subprocess.Popen(command, shell=True)
print("%d.t視頻下載結束:%s" % (index, video_name.split('/')[2]))
def video_audio_merge_single(video_name):
'''使用ffmpeg單個視頻音頻合併'''
print("視頻合成開始:%s" % video_name)
import subprocess
command = 'ffmpeg -i "%s_video.mp4" -i "%s_audio.mp4" -c copy "%s.mp4" -y -loglevel quiet' % (
video_name, video_name, video_name)
subprocess.Popen(command, shell=True)
print("視頻合成結束:%s" % video_name)
這個過程也是批量和單個分開,大致原理差不多,都是調用subprogress模塊來生成子進程,Popen類來執行shell命令,由於已經將ffmpeg加入環境變量,所以shell命令可以直接調用ffmpeg來合併音視頻。
6.3種下載方式的分別實現
def batch_download():
'''使用多線程批量下載視頻'''
# 提示輸入需要下載的系列視頻對應的id
aid = input('請輸入要下載的視頻id(舉例:鏈接https://www.bilibili.com/video/av91748877?p=1中id為91748877),默認為91748877t')
if aid:
pass
else:
aid = '91748877'
# 提示選擇清晰度
quality = input('請選擇清晰度(1代表高清,2代表清晰,3代表流暢),默認高清t')
if quality == '2':
pass
elif quality == '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)))
for i, video_content in enumerate(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)
# 整理視頻信息
if os.path.exists(title):
shutil.rmtree(title)
os.rename('video_final', title)
try:
shutil.rmtree('video')
except:
shutil.rmtree('video')
def multiple_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,則輸入89592082 68716174 87216317 83200644 88252843n默認為89592082 68716174 87216317 83200644 88252843t')
if aid_str:
pass
else:
aid_str = '89592082 68716174 87216317 83200644 88252843'
if os.path.exists(aid_str):
shutil.rmtree(aid_str)
aids = aid_str.split(' ')
# 提示選擇視頻質量
quality = input('請選擇清晰度(1代表高清,2代表清晰,3代表流暢),默認高清t')
if quality == '2':
pass
elif quality == '3':
pass
else:
quality = '1'
acc_quality = int(quality) - 1
# 創建文件夾
create_folder(aid_str)
# 創建線程池,執行多任務
pool = ThreadPoolExecutor(3)
for aid in aids:
# 將任務加入線程池
pool.submit(single_download, aid, acc_quality)
pool.shutdown(wait=True)
time.sleep(5)
# 刪除臨時文件,移動文件
remove_move_file(aid_str)
def single_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)
def single_input():
'''單個文件下載,獲取參數'''
# 獲取視頻aid
aid = input('請輸入要下載的視頻id(舉例:鏈接https://www.bilibili.com/video/av89592082中id為89592082),默認為89592082t')
if aid:
pass
else:
aid = '89592082'
# 提示選擇視頻質量
quality = input('請選擇清晰度(1代表高清,2代表清晰,3代表流暢),默認高清t')
if quality == '2':
pass
elif quality == '3':
pass
else:
quality = '1'
acc_quality = int(quality) - 1
# 調用函數進行下載
single_download(aid, acc_quality)
在一般情形下,下載的需求包含3種情況:
(1)單個視頻的下載:
只有一個視頻,沒有和它屬於同一個系列的其他視頻,如下圖

單個視頻下載
此時,除了右下方的相關推薦中的視頻,沒有其他視頻,右上方只有彈幕列表、沒有視頻列表。為了代碼的復用,將單個視頻下載時提示用戶輸入需求的代碼單獨提取出來作為single_input(),下載的函數另外作為single_download(aid, acc_quality)函數實現,在該函數中:
通過視頻鏈接如
解析網頁,得到相應的字符串並轉化成json,如下:

視頻信息json轉化
字符串json格式化可使用進行在線轉化。
獲取到視頻的標題、根據輸入確定的視頻質量、持續時長、視頻鏈接和音頻鏈接,並調用download_video_single()函數下載該視頻。
(2)多個視頻的下載:
這裡,多個視頻之間是沒有關係的,多個視頻的下載實際上是先獲取到所有的aid,並進行循環,對每個視頻鏈接傳入參數調用單個視頻下載的函數即可。同時設立線程池,大小為3,既不會對資源有太大的要求,也能實現多任務、提高下載效率。
(3)系列視頻的下載
此時,多個視頻屬於同一系列,如是一個課程系列,如下:

系列視頻
顯然,此時右上方有視頻列表,標明了有65個子視頻,每個視頻用p標識,如第2個視頻就是
。對於所有視頻,先獲取到視頻的相關信息,再加入進程池進行下載,並在任務結束之後回調函數video_audio_merge_batch()合併音視頻,並進行文件整理。
7.主函數
def main():
'''主函數,提示用戶進行三種下載模式的選擇'''
download_choice = input('請輸入您需要下載的類型:n1代表下載單個視頻,2代表批量下載系列視頻,3代表批量下載多個不同視頻,默認下載單個視頻t')
# 批量下載系列視頻
if download_choice == '2':
batch_download()
# 批量下載多個單個視頻
elif download_choice == '3':
multiple_download()
# 下載單個視頻
else:
single_input()
if __name__ == '__main__':
'''調用主函數'''
main()
主函數中實現3種下載方式對應的函數的分別調用。
三、項目分析和說明
1.結果測試
對3種方式進行測試的效果如下:

項目測試一:單個視頻測試

項目測試二:系列視頻測試

項目測試三:多個不同視頻測試
3種下載情景的測試效果均較好,下載速度也能與一般的下載速度相媲美。
代碼可點擊
進行下載。
改進說明
B站網站也一直在變化,所以對於下載可能也會有一些變化,所以將改進的地方在下面列舉出來:
(1)網址參數變化
舉例說明:
這段時間發現B站一個視頻系列的鏈接變成,即是無規律的字符串(可能是經過某種算法編碼或加密得到的),現在從鏈接中不能得到視頻(系列)的aid,這時候可以藉助瀏覽器工具抓包查看數據來找到該視頻的aid,如下:

B站視頻aid轉bvid
在左側尋找stat開頭的請求,後邊的參數即為aid,該請求api的完整鏈接為
,所以可以直接在該鏈接中獲取aid,也可以查看該請求的具體內容,可以看到第一個數據就是aid,我們也可以看到隨機字符串就是bvid,可能是建立了aid和bvid的一一映射,找到aid就可以正常下載了。
2.軟件打包
在命令行中,使路徑位於代碼所在路徑運行
pyinstaller bilibili_downloader_1.py
打印
136 INFO: PyInstaller: 3.6
137 INFO: Python: 3.7.4
138 INFO: Platform: Windows-10-10.0.18362-SP0
140 INFO: wrote xxxxBili_Video_Batch_Downloadbilibili_downloader_1.spec
205 INFO: UPX is not available.
209 INFO: Extending PYTHONPATH with paths
['xxxxBili_Video_Batch_Download',
'xxxxBili_Video_Batch_Download']
210 INFO: checking Analysis
211 INFO: Building Analysis because Analysis-00.toc is non existent
211 INFO: Initializing module dependency graph...
218 INFO: Caching module graph hooks...
247 INFO: Analyzing base_library.zip ...
5499 INFO: Caching module dependency graph...
5673 INFO: running Analysis Analysis-00.toc
5702 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
required by xxxpythonpython37python.exe
6231 INFO: Analyzing xxxxBili_Video_Batch_Downloadbilibili_downloader_1.py
7237 INFO: Processing pre-safe import module hook urllib3.packages.six.moves
10126 INFO: Processing pre-safe import module hook six.moves
14287 INFO: Processing module hooks...
14288 INFO: Loading module hook "hook-certifi.py"...
14296 INFO: Loading module hook "hook-cryptography.py"...
14936 INFO: Loading module hook "hook-encodings.py"...
15093 INFO: Loading module hook "hook-lxml.etree.py"...
15097 INFO: Loading module hook "hook-pydoc.py"...
15099 INFO: Loading module hook "hook-xml.py"...
15330 INFO: Looking for ctypes DLLs
15334 INFO: Analyzing run-time hooks ...
15339 INFO: Including run-time hook 'pyi_rth_multiprocessing.py'
15344 INFO: Including run-time hook 'pyi_rth_certifi.py'
15355 INFO: Looking for dynamic libraries
15736 INFO: Looking for eggs
15737 INFO: Using Python library xxxpythonpython37python37.dll
15757 INFO: Found binding redirects:
[]
15776 INFO: Warnings written to xxxxBili_Video_Batch_Downloadbuildbilibili_downloader_1war
n-bilibili_downloader_1.txt
15942 INFO: Graph cross-reference written to xxxxBili_Video_Batch_Downloadbuildbilibili_dow
nloader_1xref-bilibili_downloader_1.html
15967 INFO: checking PYZ
15968 INFO: Building PYZ because PYZ-00.toc is non existent
15968 INFO: Building PYZ (ZlibArchive) xxxxBili_Video_Batch_Downloadbuildbilibili_downloade
r_1PYZ-00.pyz
16944 INFO: Building PYZ (ZlibArchive) xxxxBili_Video_Batch_Downloadbuildbilibili_downloade
r_1PYZ-00.pyz completed successfully.
16980 INFO: checking PKG
16981 INFO: Building PKG because PKG-00.toc is non existent
16981 INFO: Building PKG (CArchive) PKG-00.pkg
17030 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
17034 INFO: Bootloader xxxpythonpython37libsite-packagesPyInstallerbootloaderWindows-64bitrun.exe
17034 INFO: checking EXE
17035 INFO: Building EXE because EXE-00.toc is non existent
17035 INFO: Building EXE from EXE-00.toc
17037 INFO: Appending archive to EXE xxxxBili_Video_Batch_Downloadbuildbilibili_downloader_
1bilibili_downloader_1.exe
17046 INFO: Building EXE from EXE-00.toc completed successfully.
17053 INFO: checking COLLECT
17053 INFO: Building COLLECT because COLLECT-00.toc is non existent
17055 INFO: Building COLLECT COLLECT-00.toc
出現INFO: Building EXE from EXE-00.toc completed successfully. 即打包成功。在當前路徑下找到dist或build目錄下的bilibili_downloader_1目錄下的bilibili_downloader_1.exe,即是打包後的軟件。點擊打開即可進行選擇和輸入,開始下載相應視頻。測試示例如下:

項目打包下載測試
在bilibili_downloader_1.exe的同級目錄下可以看到下載保存的視頻。
3.改進分析
該項目是小編進行B站視頻下載的首次嘗試,難免有很多不足,在實現的過程中和後期的總結中,可以看出還存在一些問題:
- 還不能下載B站上的所有視頻,目前局限於各種普通視頻教程,不能下載直播視頻、大會員番劇等,可以在後期進一步優化;
- 代碼過於繁瑣,有不少功能類似的重複代碼,可以進一步簡化、提高代碼的復用性;
- 沒有採取適當的措施應對B站的反爬,可能會因為請求過多而無法正常下載。
可以在後期進行優化,使整個程序更加健壯。
4.合法性說明
- 本項目的出發點是方便地下載B站上的學習視頻,可以更好地學習各類教程,這對程序猿來說也是一種福利,但是絕不用與其他商業目的,所有讀者可以參考執行思路和程序代碼,但不能用於惡意和非法目的(惡意頻繁下載視頻、非法盈利等),如有違者請自行負責。
- 本項目在實施的過程中可能參考了其他大佬的實現思路,如有侵犯他人利益,請聯繫更改或刪除。
- 本項目是B站視頻批量下載系列的第一篇,有很多尚待改進的地方,後期會繼續更新,歡迎各位讀者交流指正,以期不斷改進。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/304029.html