Files
bilibili-video-downloader/bilibili_downloader.py
T

390 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import requests
import time
import hashlib
import urllib.parse
import json
import os
import subprocess
from typing import Dict, List
#填写你的Cookie
sessdata = ""
class BilibiliAutoDownloader:
def __init__(self, ffmpeg_path="D:\\ff\\ffmpeg.exe"):
self.sessdata = sessdata
self.session = requests.Session()
self.ffmpeg_path = ffmpeg_path
# 设置完整的cookie和headers,模拟真实浏览器
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://www.bilibili.com',
'Origin': 'https://www.bilibili.com',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
'Priority': 'u=1, i'
})
if sessdata:
self.session.cookies.set('SESSDATA', sessdata, domain='.bilibili.com')
# 设置其他必要的cookies
self.session.cookies.set('buvid3', '填写你的Cookie', domain='.bilibili.com')
self.session.cookies.set('DedeUserID', '填写你的Cookie', domain='.bilibili.com')
self.session.cookies.set('bili_jct', '填写你的Cookie', domain='.bilibili.com')
self.session.cookies.set('CURRENT_FNVAL', '填写你的Cookie', domain='.bilibili.com')
def get_video_info(self, bvid: str) -> Dict:
"""获取视频信息"""
api_url = "https://api.bilibili.com/x/web-interface/view"
params = {'bvid': bvid}
response = self.session.get(api_url, params=params)
return response.json()
def _get_wbi_keys(self) -> tuple:
"""获取WBI加密密钥"""
nav_url = "https://api.bilibili.com/x/web-interface/nav"
response = self.session.get(nav_url)
data = response.json()
if data['code'] == 0:
img_url = data['data']['wbi_img']['img_url']
sub_url = data['data']['wbi_img']['sub_url']
img_key = img_url.split('/')[-1].split('.')[0]
sub_key = sub_url.split('/')[-1].split('.')[0]
return img_key, sub_key
return None, None
def _wbi_sign(self, params: Dict) -> Dict:
"""WBI签名算法"""
img_key, sub_key = self._get_wbi_keys()
if not img_key or not sub_key:
return params
mixin_key = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
]
# 添加时间戳
params['wts'] = int(time.time())
# 参数排序并过滤
params = {k: ''.join(filter(str.isalnum, str(v))) for k, v in params.items()
if k not in ['sign', 'csrf'] and v != ''}
params = dict(sorted(params.items()))
# 生成待签名字符串
query = urllib.parse.urlencode(params)
# 应用mixin_key
mixed = []
for key in mixin_key:
if key < len(query):
mixed.append(query[key])
mixed_str = ''.join(mixed)[:32]
# 生成w_rid
w_rid = hashlib.md5((query + mixed_str).encode()).hexdigest()
params['w_rid'] = w_rid
return params
def get_play_url_mp4(self, bvid: str, cid: int) -> Dict:
"""获取MP4格式的播放地址(非DASH)"""
# 先获取avid
video_info = self.get_video_info(bvid)
if not video_info or video_info.get('code') != 0:
return None
avid = video_info['data']['aid']
# 构建参数 - 不使用DASH格式
params = {
'qn': 112, # 高清1080P
'fnval': 0, # 设置为0表示不使用DASH格式
'fourk': 1,
'voice_balance': 1,
'gaia_source': 'pre-load',
'isGaiaAvoided': 'true',
'avid': avid,
'bvid': bvid,
'cid': cid,
'web_location': '1315873',
}
# 应用WBI签名
signed_params = self._wbi_sign(params)
# 使用原生API端点
api_url = "https://api.bilibili.com/x/player/wbi/playurl"
response = self.session.get(api_url, params=signed_params)
return response.json()
def find_best_mp4_url(self, vajson: Dict) -> tuple:
"""找到最高质量的MP4视频URL"""
if 'data' not in vajson or 'durl' not in vajson['data']:
return None, None
durls = vajson['data']['durl']
if not durls:
return None, None
# 获取视频质量信息
quality = vajson['data'].get('quality', 0)
format_info = vajson['data'].get('format', '')
description = vajson['data'].get('accept_description', [])
print(f"🎯 找到 {len(durls)} 个MP4视频片段")
print(f"📊 视频质量: {quality} - {format_info}")
if description:
print(f"📋 可用质量: {', '.join(description)}")
# 如果有多个片段,选择第一个(通常是完整的视频)
best_durl = durls[0]
video_url = best_durl.get('url', '')
backup_urls = best_durl.get('backup_url', [])
size = best_durl.get('size', 0)
print(f"🔥 选择视频片段 - 大小: {size/(1024*1024):.2f}MB")
# 测试主URL
if self.test_url_content(video_url):
print(f"✅ 主视频URL可用")
return video_url, f"MP4-{quality}"
# 测试备用URL
for i, backup_url in enumerate(backup_urls):
if self.test_url_content(backup_url):
print(f"✅ 备用视频URL {i+1} 可用")
return backup_url, f"MP4-{quality}"
return None, None
def test_url_content(self, url: str, min_size: int = 1024) -> bool:
"""测试URL是否返回有效内容"""
try:
headers = {
'Range': 'bytes=0-8191',
'Referer': 'https://www.bilibili.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
response = self.session.get(url, headers=headers, timeout=10, stream=True)
if response.status_code not in [200, 206]:
return False
content_length = response.headers.get('content-length')
if content_length and int(content_length) < min_size:
return False
return True
except Exception:
return False
def download_with_aria2(self, url: str, filename: str, file_type: str):
"""使用aria2多线程下载"""
print(f"🚀 开始高速下载{file_type}")
cmd = [
'aria2c',
'-x', '16',
'-s', '16',
'-k', '1M',
'--header=Referer: https://www.bilibili.com',
'--check-certificate=false',
'--continue=true',
'--max-tries=5',
'--retry-wait=3',
'-o', filename,
url
]
try:
result = subprocess.run(cmd, capture_output=True)
if result.returncode == 0:
print(f"✅ {file_type}下载完成")
return True
else:
return False
except FileNotFoundError:
return False
def download_fast_python(self, url: str, filename: str, file_type: str):
"""Python多线程下载"""
import threading
from concurrent.futures import ThreadPoolExecutor
print(f"🚀 开始多线程下载{file_type}")
def download_chunk(start_byte, end_byte, chunk_id):
headers = {
'Range': f'bytes={start_byte}-{end_byte}',
'Referer': 'https://www.bilibili.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
try:
response = self.session.get(url, headers=headers, stream=True, timeout=30)
with open(f"{filename}.part{chunk_id}", 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return True
except Exception:
return False
try:
response = self.session.head(url, headers={'Referer': 'https://www.bilibili.com'})
total_size = int(response.headers.get('content-length', 0))
except:
total_size = 0
if total_size == 0:
return self.download_single(url, filename, file_type)
num_threads = 8
chunk_size = total_size // num_threads
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = []
for i in range(num_threads):
start_byte = i * chunk_size
end_byte = start_byte + chunk_size - 1 if i < num_threads - 1 else total_size - 1
futures.append(executor.submit(download_chunk, start_byte, end_byte, i))
results = [f.result() for f in futures]
if all(results):
with open(filename, 'wb') as outfile:
for i in range(num_threads):
with open(f"{filename}.part{i}", 'rb') as infile:
outfile.write(infile.read())
os.remove(f"{filename}.part{i}")
print(f"✅ {file_type}下载完成")
return True
else:
return False
def download_single(self, url: str, filename: str, file_type: str):
"""单线程下载"""
print(f"📥 开始下载{file_type}")
max_retries = 3
for attempt in range(max_retries):
try:
headers = {
'Referer': 'https://www.bilibili.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
}
response = self.session.get(url, headers=headers, stream=True, timeout=30)
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
print(f"\r📊 下载进度: {percent:.1f}%", end='', flush=True)
if os.path.exists(filename) and os.path.getsize(filename) > 1024:
print(f"\n✅ {file_type}下载完成")
return True
else:
if os.path.exists(filename):
os.remove(filename)
except Exception:
if os.path.exists(filename):
os.remove(filename)
return False
def download_and_merge(self, bvid: str):
"""主下载函数 - 直接下载MP4格式"""
bvid = bvid.rstrip('/')
print(f"🚀 开始处理视频: {bvid}")
# 获取视频信息
video_info = self.get_video_info(bvid)
if not video_info or video_info.get('code') != 0:
print("❌ 无法获取视频信息")
return
data = video_info['data']
cid = data['cid']
title = data['title']
print(f"📺 视频标题: {title}")
print(f"🔢 视频CID: {cid}")
# 使用MP4 API获取播放信息
vajson = self.get_play_url_mp4(bvid, cid)
if not vajson or vajson.get('code') != 0:
print("❌ 无法获取MP4播放信息")
return
print("✅ 成功获取MP4播放信息")
# 找到MP4视频URL
video_url, quality_info = self.find_best_mp4_url(vajson)
if not video_url:
print("❌ 没有可用的MP4视频URL")
return
print(f"🎬 已选择MP4视频 - 质量: {quality_info}")
# 生成安全的文件名
safe_title = "".join(c for c in title if c.isalnum() or c in ('-', '_')).rstrip()
if not safe_title:
safe_title = "video"
output_file = f"{safe_title}.mp4"
# 选择下载方式
download_methods = [
('aria2', self.download_with_aria2),
('多线程', self.download_fast_python),
('单线程', self.download_single)
]
# 直接下载MP4文件
video_success = False
for method_name, method_func in download_methods:
print(f"\n尝试使用 {method_name} 下载MP4视频...")
if method_func(video_url, output_file, "MP4视频"):
video_success = True
break
if video_success:
print(f"\n🎉 下载完成!")
print(f"📹 最终文件: {output_file}")
print(f"🔧 视频格式: {quality_info}")
if os.path.exists(output_file):
file_size = os.path.getsize(output_file)
print(f"📁 文件大小: {file_size / (1024*1024):.2f} MB")
else:
print("❌ 下载失败")
# 🎯 主程序
if __name__ == "__main__":
downloader = BilibiliAutoDownloader(ffmpeg_path=r"D:\ff\ffmpeg.exe")
try:
bvid = input("📥 请输入BV号: ").strip()
if bvid:
downloader.download_and_merge(bvid)
except Exception as e:
print(f"💥 程序执行失败: {e}")
import traceback
traceback.print_exc()