B站視頻批量下載工具
工具是另一種財富。
概述
在 B 站遇到好的視頻總想下載下來。不過很多瀏覽器插件只能每次下一個,批量下載才省力。有了 AI 的輔助,其實編寫小工具已經(jīng)非常方便了。 不過我們還是需要對生成的工具有要求的。
(1) 命令行選項;(2)可復(fù)用。(3)可組合
這其實是從 Linux 系統(tǒng)得來的啟示。 命令行,就是可以通過命令行靈活定制行為,可復(fù)用,就是每個工具是小的但是可以復(fù)用的,當(dāng)需要時不必重寫或修改;可組合,是說命令行可以組合起來實現(xiàn)不同的工具。當(dāng)我們把任務(wù)仔細(xì)拆分之后,就能更容易達(dá)到這個目標(biāo)。
要批量下載視頻,需要三個工具和一個數(shù)據(jù)源:
(1)數(shù)據(jù)解析
- JSON PATH 語法
- HTML select 語法
(2)數(shù)據(jù)拼接
- URL 拼接
(3)下載工具
- you-get
- ffmpeg
(4) 數(shù)據(jù)源
- HTML 網(wǎng)頁
- API 返回的 JSON 數(shù)據(jù)。
本文主要講述如何解析 API 返回的 JSON 數(shù)據(jù),拼接成所需的 URL,然后用下載工具下載。
工具開發(fā)
數(shù)據(jù)解析
比如說,要下載個人收藏的視頻。
打開B站個人收藏,查看哪個URL 返回了頁面數(shù)據(jù)。

API 返回的 JSON 數(shù)據(jù)如下:這里面 bvid 就是B站視頻的標(biāo)識,加上前綴即是可訪問的B站視頻地址。
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"info": {
"id": 3133010051,
"fid": 31330100,
"mid": 183260251,
"attr": 22,
"title": "文藝",
"cover": "http://i2.hdslb.com/bfs/archive/843055b47a4cc46e80047ab9087b4bea06491f47.jpg",
"upper": {
"mid": 183260251,
"name": "琴水玉",
"face": "https://i1.hdslb.com/bfs/face/fd135f95e066ea357969cac54468c0273baea6a1.jpg",
"followed": false,
"vip_type": 2,
"vip_statue": 1
},
"cover_type": 2,
"cnt_info": {
"collect": 0,
"play": 0,
"thumb_up": 0,
"share": 0
},
"type": 11,
"intro": "",
"ctime": 1709329450,
"mtime": 1709329450,
"state": 0,
"fav_state": 0,
"like_state": 0,
"media_count": 26,
"is_top": false
},
"medias": [{
"id": 113892432876371,
"type": 2,
"title": "中式美學(xué):沙鷗徑去魚兒飽,野鳥相呼柿子紅。",
"cover": "http://i2.hdslb.com/bfs/archive/843055b47a4cc46e80047ab9087b4bea06491f47.jpg",
"intro": "中式美學(xué):沙鷗徑去魚兒飽,野鳥相呼柿子紅。",
"page": 1,
"duration": 99,
"upper": {
"mid": 1580957455,
"name": "中式美學(xué)分享",
"face": "https://i1.hdslb.com/bfs/face/f8e2fec9c18501d4e577bd7f60030f97c2c8fe54.jpg",
"jump_link": ""
},
"attr": 0,
"cnt_info": {
"collect": 5961,
"play": 104689,
"danmaku": 52,
"vt": 0,
"play_switch": 0,
"reply": 0,
"view_text_1": "10.5萬"
},
"link": "bilibili://video/113892432876371",
"ctime": 1737861192,
"pubtime": 1737861192,
"fav_time": 1738047373,
"bv_id": "BV1bvFAe2Efz",
"bvid": "BV1bvFAe2Efz",
"season": null,
"ogv": null,
"ugc": {
"first_cid": 28085587803
},
"media_list_link": "bilibili://music/playlist/playpage/3316270251?page_type=3\u0026oid=113892432876371\u0026otype=2"
}
]
}
}
請 AI 寫一個解析 JSON 文件的工具,能夠指定路徑來獲取指定數(shù)據(jù)。
import json
import jmespath
def get_value_by_path(data, path):
"""通過路徑獲取JSON中的值
Args:
data (dict): JSON數(shù)據(jù)
path (str): JsonPath表達(dá)式,如 'data.result[?result_type=='video'].data[]'
Returns:
list: 返回所有匹配的值的列表
"""
try:
# 解析并執(zhí)行JsonPath表達(dá)式
print(path)
result = jmespath.search(path, data)
return result
except Exception as e:
print(f"錯誤: 無法從路徑 '{path}' 獲取值: {str(e)}")
return []
def extract_values(json_file, path):
"""從指定的JSON文件中提取指定路徑的值
Args:
json_file (str): JSON文件的路徑
path (str): 以點號分隔的路徑
Returns:
list: 包含所有匹配值的列表
"""
try:
# 讀取JSON文件
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 獲取指定路徑的值
values = get_value_by_path(data, path)
# 確保返回值是列表
if not isinstance(values, list):
values = [values]
return values
except FileNotFoundError:
print(f"錯誤: 找不到文件 '{json_file}'")
return []
except json.JSONDecodeError:
print(f"錯誤: '{json_file}' 不是有效的JSON文件")
return []
import json
import argparse
from pytools.tools.dw_video import download_video
from pytools.common.jsonparse import extract_values
# ------------------------------------------------------------
# up 主視頻
# bf -f video.json -p 'data.list.vlist[*].bvid'
# ------------------------------------------------------------
def genurl(bvids):
urls = []
for bvid in bvids:
urls.append("https://www.bilibili.com/video/" + bvid)
return urls
def main():
# 設(shè)置命令行參數(shù)
parser = argparse.ArgumentParser(description='從JSON文件中提取指定路徑的值')
parser.add_argument('-f', '--file', required=True, help='JSON文件路徑')
parser.add_argument('-p', '--path', required=True, help='以點號分隔的JSON路徑,例如:data.list.vlist[*].bvid')
args = parser.parse_args()
# 提取值
values = extract_values(args.file, args.path)
print(values)
# 打印結(jié)果
if values:
print("\n".join(str(v) for v in values))
# 如果提取的是bvid,則下載視頻
urls = genurl(values)
for url in urls:
download_video(url)
if __name__ == '__main__':
main()
AI 很快就寫出來了。只要執(zhí)行 python3 bf.py -f video.json -p "data.list.vlist[*].bvid" 就可以生成該頁的所有視頻的 B 站網(wǎng)址。這里用到了 JsonPath 語法。可以去學(xué)習(xí)下:““JsonPath簡明教程”。須知軟件工程師要十八般武藝樣樣會一點。
為什么寫成這樣呢,因為這個是可以復(fù)用的。
咱們再來看搜索個人UP主視頻返回什么格式。她的 bvid 藏在 ?data.list.vlist[*].bvid? 里。
可以使用 python3 bf.py -f video.json -p "data.list.vlist[*].bvid"
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"list": {
"slist": [],
"tlist": {
"160": {
"tid": 160,
"count": 2,
"name": "生活"
},
"36": {
"tid": 36,
"count": 140,
"name": "知識"
}
},
"vlist": [{
"comment": 4,
"typeid": 228,
"play": 1688,
"pic": "http://i2.hdslb.com/bfs/archive/2bf4a4f7ed75aa137c2d999998db6d0123481ad2.jpg",
"subtitle": "",
"description": "中式美學(xué):正浪吟、不覺回橈,水花風(fēng)葉兩悠悠。",
"copyright": "1",
"title": "中式美學(xué):正浪吟、不覺回橈,水花風(fēng)葉兩悠悠。",
"review": 0,
"author": "中式美學(xué)分享",
"mid": 1580957455,
"created": 1747611930,
"length": "02:10",
"video_review": 1,
"aid": 114531426636976,
"bvid": "BV1kAESzuEXc",
"hide_click": false,
"is_pay": 0,
"is_union_video": 0,
"is_steins_gate": 0,
"is_live_playback": 0,
"is_lesson_video": 0,
"is_lesson_finished": 0,
"lesson_update_info": "",
"jump_url": "",
"meta": null,
"is_avoided": 0,
"season_id": 0,
"attribute": 8405120,
"is_charging_arc": false,
"elec_arc_type": 0,
"elec_arc_badge": "",
"vt": 0,
"enable_vt": 0,
"vt_display": "",
"playback_position": 0,
"is_self_view": false
},
{
"comment": 1,
"typeid": 228,
"play": 2122,
"pic": "http://i2.hdslb.com/bfs/archive/b5da93822dd42959ef4103b944ad089593cbc928.jpg",
"subtitle": "",
"description": "中式美學(xué):落日熔金,暮云合璧,人在何處。染柳煙濃,吹梅笛怨,春意知幾許。",
"copyright": "1",
"title": "中式美學(xué):落日熔金,暮云合璧,人在何處。染柳煙濃,吹梅笛怨,春意知幾許。",
"review": 0,
"author": "中式美學(xué)分享",
"mid": 1580957455,
"created": 1747434610,
"length": "01:40",
"video_review": 3,
"aid": 114519833580541,
"bvid": "BV1YSE8zKE4Q",
"hide_click": false,
"is_pay": 0,
"is_union_video": 0,
"is_steins_gate": 0,
"is_live_playback": 0,
"is_lesson_video": 0,
"is_lesson_finished": 0,
"lesson_update_info": "",
"jump_url": "",
"meta": null,
"is_avoided": 0,
"season_id": 0,
"attribute": 8405120,
"is_charging_arc": false,
"elec_arc_type": 0,
"elec_arc_badge": "",
"vt": 0,
"enable_vt": 0,
"vt_display": "",
"playback_position": 0,
"is_self_view": false
}
}
}
這里可以對 python3 bf.py 做了一個 alias ,在 ~/.zshrc 里添加
?alias bf="python3 /Users/qinshu/tools/pytools/pytools/tools/bf.py"?
添加到Path里,然后 source ~/.zshrc
就可以直接使用 bf -f video.json -p "data.list.vlist[*].bvid",這就是可復(fù)用的威力,不需要改代碼,就可以適應(yīng)不同的變化。
讀者還可以去看看根據(jù)關(guān)鍵字搜索B站視頻的JSON返回數(shù)據(jù),可以用 bss -f video.json -p 'data.result[?result_type=="video"].data[]'?來實現(xiàn)。可謂一個程序能解決三種場景。
視頻下載
#!/usr/bin/env python3
import subprocess
import shlex
from pathlib import Path
from typing import Optional, Union
import time
def download_video(
video_url: str,
output_dir: Union[str, Path] = Path.cwd(),
timeout: int = 3600, # 1小時超時
retries: int = 3,
verbose: bool = True
) -> Optional[Path]:
"""
使用 y 命令下載視頻
參數(shù):
video_url: 視頻URL (e.g. "https://www.bilibili.com/video/BV1xx411x7xx")
output_dir: 輸出目錄 (默認(rèn)當(dāng)前目錄)
timeout: 超時時間(秒)
retries: 重試次數(shù)
verbose: 顯示下載進(jìn)度
返回:
成功時返回下載的視頻路徑,失敗返回None
"""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
cmd = f"y {shlex.quote(video_url)}"
if verbose:
print(f"開始下載: {video_url}")
print(f"保存到: {output_dir.resolve()}")
print(f"執(zhí)行命令: {cmd}")
for attempt in range(1, retries + 1):
try:
start_time = time.time()
# 使用Popen實現(xiàn)實時輸出
process = subprocess.Popen(
cmd,
shell=True,
cwd=str(output_dir),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1
)
# 實時打印輸出
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output and verbose:
print(output.strip())
# 檢查超時
if time.time() - start_time > timeout:
process.terminate()
raise subprocess.TimeoutExpired(cmd, timeout)
# 檢查返回碼
if process.returncode == 0:
if verbose:
print(f"下載成功 (嘗試 {attempt}/{retries})")
return _find_downloaded_file(output_dir, video_url)
else:
raise subprocess.CalledProcessError(process.returncode, cmd)
except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
if attempt < retries:
wait_time = min(attempt * 10, 60) # 指數(shù)退避
if verbose:
print(f"嘗試 {attempt}/{retries} 失敗,{wait_time}秒后重試...")
print(f"錯誤: {str(e)}")
time.sleep(wait_time)
else:
if verbose:
print(f"下載失敗: {str(e)}")
return None
def _find_downloaded_file(directory: Path, video_url: str) -> Optional[Path]:
"""嘗試自動查找下載的文件"""
# 這里可以根據(jù)實際y命令的輸出文件名模式進(jìn)行調(diào)整
# 示例:查找最近修改的視頻文件
video_files = sorted(
directory.glob("*.mp4"),
key=lambda f: f.stat().st_mtime,
reverse=True
)
return video_files[0] if video_files else None
這里采用的是直接調(diào)用命令行,命令行 y 是如下文件。 加上 chmod +x y
--cookies 里的文件,是firefox 里用 Cookie-Editor 導(dǎo)出的數(shù)據(jù)復(fù)制進(jìn)去的。如果你有其它的下載工具,也可以替換為自己的下載工具。這里咱們就不單獨開發(fā)下載工具了。
link=$1
you-get $link --cookies "/Users/qinshu/privateqin/cookies.txt" -f -o /Users/qinshu/joy/dance/bili

獲取網(wǎng)絡(luò)數(shù)據(jù)
其實還有一步,獲取網(wǎng)絡(luò)數(shù)據(jù)源,可以通過 python requests 庫實現(xiàn)。不過B站加了風(fēng)控校驗,導(dǎo)致我之前的方法失效了。樂趣減少了很多。只能算做了一半工作。后續(xù)還要看看怎么突破網(wǎng)站防線拉取數(shù)據(jù)。
?
小結(jié)
工程師不比程序員,他需要的是十八般武藝樣樣都會一點,這樣會獲得更大的自由度。雖然學(xué)藝不精,那有什么關(guān)系呢?什么問題用什么工具最適合解決。對于手里沒錢的碼農(nóng),多寫一點工具讓自己的生活更輕松,未嘗不是一種選擇呢。

浙公網(wǎng)安備 33010602011771號