Python构建B站视频下载器:从M3U8解析到FFmpeg合并的完整实践
2026/8/5 15:10:11 网站建设 项目流程

1. 项目缘起:为什么我们需要一个自己的B站视频下载工具?

作为一名经常需要处理视频素材的创作者,我发现自己越来越依赖B站这个内容宝库。无论是寻找技术教程的片段、收集创意灵感,还是想离线观看喜欢的UP主系列视频,一个稳定、高效的下载工具几乎是刚需。然而,市面上的各种在线解析网站和桌面软件,要么广告满天飞,要么突然失效,要么对视频清晰度和格式限制颇多,用起来总有种“受制于人”的不踏实感。更关键的是,很多工具的工作原理成谜,你根本不知道你的视频链接和数据去了哪里,这对于注重隐私和版权的我来说,是个不小的顾虑。

于是,我决定自己动手,用Python打造一个专属于我的B站视频下载器。我的目标很明确:透明、可控、灵活。透明是指每一步操作我都清楚其原理;可控是指我能自由选择清晰度、下载位置,甚至处理一些特殊格式(如杜比视界、HDR);灵活是指这个工具能适应B站前端或API的微小变化,方便我随时维护。这个过程,远不止是学会调用一个库那么简单,它涉及到对网络请求的深刻理解、对多媒体封装格式的认识,以及如何优雅地处理一个大型平台的反爬机制。接下来,我将把这个从零构建的过程,以及其中积累的经验和踩过的坑,毫无保留地分享给你。

2. 核心原理拆解:B站视频是如何被获取的?

在动手写代码之前,我们必须先搞清楚B站网页或App上的视频,到底是以何种形式存在,以及我们如何能合法、合规地获取到它。这里强调“合规”,是指我们仅讨论用于个人学习、研究欣赏,且不侵犯UP主版权、不进行非法传播的技术原理。理解这个原理,是构建任何下载工具的基础。

2.1 从网页到流媒体:M3U8与MP4

当你打开一个B站视频页面,浏览器并不会直接加载一个完整的、几个G的MP4文件。那样做用户体验会极差(加载慢、卡顿)。现代流媒体网站普遍采用自适应比特率流媒体(Adaptive Bitrate Streaming)技术。B站主要使用的是一种基于HTTP的动态流媒体协议,其核心是一个叫做M3U8的文本文件。

M3U8文件本质上是一个播放列表(Playlist)。它里面不包含视频数据,而是包含了多个不同清晰度(如1080P、720P、480P)对应的流媒体片段(TS文件)的地址列表,以及一些元信息。播放器(如浏览器)会根据你的网络速度,动态选择最合适的清晰度流,并依次下载这些TS片段进行播放。我们的下载任务,就变成了:找到这个M3U8文件,然后解析它,获取所有TS片段的真实地址,最后将它们下载并合并成一个完整的视频文件。

除了M3U8,B站对一些较短的视频或低清晰度选项,也可能直接提供完整的MP4FLV文件地址。但高清晰度(如1080P高码率、4K)以及大会员专享的清晰度,几乎无一例外都是通过M3U8方式提供的。

2.2 关键步骤:如何找到视频的“门牌号”?

那么,如何找到这个关键的M3U8文件地址呢?这通常需要通过分析网络请求来获得。以浏览器开发者工具(F12)的“网络(Network)”面板为例:

  1. 过滤请求:在播放视频时,清空网络请求记录,然后刷新页面或开始播放。在请求类型中筛选“XHR”或“媒体”(Media)。
  2. 寻找特征:你会看到一系列请求。其中可能包含playurl?api.bilibili.com/x/player/playurl等字样的请求,这些通常是B站内部API,返回的是包含视频/音频流地址的JSON数据。更直接的是,你可能会找到直接以.m3u8结尾的请求,这就是我们想要的播放列表文件。
  3. 分析请求参数:点击这个请求,查看它的“标头(Headers)”。你会看到请求的URL非常长,里面包含了许多参数,如avid(视频AV号)、bvid(视频BV号)、cid(分P的ID)、qn(清晰度标识)、fnval(流格式标识,如80代表M3U8格式)、fourk(是否4K)等。此外,请求头(Request Headers)中通常包含一个至关重要的字段:CookieUser-AgentCookie包含了你的登录会话信息,用于获取高清晰度或大会员专享流;User-Agent用于标识客户端类型。

注意:直接使用浏览器的Cookie存在隐私和安全风险,且Cookie会过期。在自动化脚本中,更常见的做法是通过模拟登录或使用其他授权方式获取access_keySESSDATA来构造凭证。但本文为简化流程,侧重于原理讲解,后续示例会使用一种更通用的、无需登录即可获取普通清晰度的方法。

2.3 音画分离与合并

细心的你可能会发现,在播放列表或API返回的数据中,视频(Video)和音频(Audio)的地址常常是分开的。这是为了更灵活地适配不同设备和带宽,也便于进行多语言音轨切换。因此,一个完整的下载流程通常包括:

  1. 获取视频流的M3U8地址。
  2. 获取音频流的M3U8地址。
  3. 分别下载视频流和音频流的所有TS片段,并合并成独立的视频文件(.video.ts)和音频文件(.audio.ts)。
  4. 使用音视频处理工具(如FFmpeg)将独立的视频文件和音频文件混合(Mux)成一个最终的MP4或MKV文件。

理解了这些,我们就掌握了下载B站视频的“地图”。接下来,就是按照地图,用Python工具来一步步执行了。

3. 环境准备与工具选型:构建我们的“施工队”

工欲善其事,必先利其器。我们需要选择合适的Python库来帮助我们完成网络请求、数据解析、文件下载和音视频处理等一系列任务。

3.1 Python库的选择与安装

我们将使用以下核心库,你可以通过pip命令一键安装:

pip install requests beautifulsoup4 lxml
  • requests:这是Python中最简单易用的HTTP库,我们将用它来发送网络请求,获取网页源码、API数据和M3U8文件。它比Python内置的urllib更人性化。
  • beautifulsoup4lxml:这对组合用于解析HTML网页。beautifulsoup4(简称bs4)是一个优秀的HTML/XML解析库,而lxml是它的一个解析器后端,速度更快。我们需要用它们从B站视频页面中提取出关键的bvidcid等参数。

对于音视频下载与合并,我们有两种主流方案:

方案一:纯Python实现(m3u8库 +aiohttp+FFmpeg

  • m3u8:一个专门用于解析M3U8文件的库,能方便地提取出TS片段地址。
  • aiohttp:一个支持异步HTTP请求的库。当需要下载成百上千个TS片段时,异步IO能极大提升下载速度,避免漫长的等待。
  • FFmpeg:这不是一个Python库,而是一个强大的命令行音视频处理工具。我们需要在系统层面安装它,然后在Python中用subprocess模块调用它来完成最终的音视频合并。这是最专业、最通用的方案。

方案二:使用集成度更高的第三方封装库(如you-getyoutube-dlyou-get这样的库,其内部已经封装了从解析到下载的完整逻辑,对于B站、油管等众多网站提供了开箱即用的支持。但为了彻底理解原理,并实现更定制化的功能(如自定义请求头、代理设置、特定清晰度选择),我们本次选择方案一。这能让我们掌控每一个细节。

因此,请确保你的系统已经安装了FFmpeg,并已将其添加到系统环境变量PATH中。你可以在命令行输入ffmpeg -version来验证是否安装成功。

3.2 构造请求头:让自己看起来像个“浏览器”

直接使用requests.get()而不做任何伪装,很容易被服务器识别为爬虫并拒绝服务。因此,我们需要构造一个合理的请求头(Headers)。最基本的需要包含User-Agent

import requests headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://www.bilibili.com/', # 表明请求来源,对于获取视频流地址通常是必需的 }

实操心得User-Agent可以从你电脑浏览器的开发者工具中直接复制。Referer字段非常重要,很多视频网站会校验这个头,如果缺失或不正确,即使拿到了正确的视频地址也可能返回403错误。对于B站,通常设置为视频所在页面的URL或首页即可。

4. 第一步:解析视频页面,获取关键参数(bvid, cid)

B站的每个视频都有两个重要ID:bvidcid

  • bvid:即视频的BV号,是视频的唯一标识,体现在视频的URL中,如https://www.bilibili.com/video/BV1xx411c7mD中的BV1xx411c7mD
  • cid:即分P的ID。一个视频(尤其是长视频、课程)可能分为多个部分(P1, P2...),每个部分都有一个独立的cid。即使是单P视频,也有一个对应的cid。这个参数在请求视频流时是必须的。

我们的首要任务就是从视频页面HTML中提取出这两个参数。

import requests from bs4 import BeautifulSoup import re def get_bvid_cid(url): """ 从B站视频页面URL中提取bvid和cid """ resp = requests.get(url, headers=headers) resp.raise_for_status() # 检查请求是否成功 soup = BeautifulSoup(resp.text, 'lxml') # 方法1:从<script>标签的window.__playinfo__或__INITIAL_STATE__中提取(更可靠) # 页面初始化数据通常包含在特定的<script>标签里 script_text = '' for script in soup.find_all('script'): if 'window.__playinfo__' in script.text: script_text = script.text break if not script_text: # 如果没找到__playinfo__,尝试找__INITIAL_STATE__ for script in soup.find_all('script'): if 'window.__INITIAL_STATE__' in script.text: script_text = script.text break if script_text: # 使用正则表达式提取JSON字符串 import json # 提取 __playinfo__ 或 __INITIAL_STATE__ 的值 match = re.search(r'window\.__playinfo__\s*=\s*({.*?})</script>', script_text, re.DOTALL) if not match: match = re.search(r'window\.__INITIAL_STATE__\s*=\s*({.*?});', script_text, re.DOTALL) if match: data_json = match.group(1) data = json.loads(data_json) # 从不同位置尝试获取cid cid = None if 'videoData' in data: cid = data['videoData'].get('cid') elif 'cid' in data: cid = data['cid'] elif 'videoInfo' in data and 'cid' in data['videoInfo']: cid = data['videoInfo']['cid'] # bvid 可以从URL直接解析,也可以从数据中取 bvid_match = re.search(r'/video/(BV[0-9A-Za-z]+)', url) bvid = bvid_match.group(1) if bvid_match else None if cid and bvid: return bvid, cid # 方法2:备用方案,从页面其他元素或API获取(如果方法1失效) # 这里可以尝试模拟页面中的另一个API请求,例如获取视频信息的接口 # 由于B站前端可能变化,此处省略具体代码,原则是寻找包含cid的JSON数据 raise Exception("无法从页面中提取bvid和cid,页面结构可能已更新。") # 使用示例 video_url = "https://www.bilibili.com/video/BV1GJ411x7h7" bvid, cid = get_bvid_cid(video_url) print(f"bvid: {bvid}, cid: {cid}")

踩坑记录:早期很多教程教人用正则表达式在HTML正文里硬搜cid,这种方法极其脆弱,B站前端稍作改动就会失效。现在更可靠的方法是解析window.__INITIAL_STATE__window.__playinfo__这两个JavaScript对象中包含的初始化数据。它们以JSON格式存储了视频的几乎所有信息,是我们获取参数最稳定的来源。如果这两个都没有,可能需要考虑页面是动态渲染的(如Vue/React),此时可能需要用到SeleniumPlaywright这类浏览器自动化工具来获取渲染后的页面源码,但这会大大增加复杂度和运行时间。

5. 第二步:请求视频流信息API,获取M3U8地址

拿到bvidcid后,我们就可以构造请求,向B站的播放地址API索取视频流的具体信息了。这个API返回一个JSON,里面包含了不同清晰度对应的视频和音频流地址。

B站有多个API接口,一个相对稳定且常用的接口是:

https://api.bilibili.com/x/player/playurl

我们需要用GET方法传递一系列参数:

import json def get_playurl(bvid, cid, quality=80): """ 获取视频播放地址信息 quality: 清晰度标识,80代表1080P(需登录),64代表720P,32代表480P等。 fnval=80 表示请求M3U8格式流。 """ params = { 'bvid': bvid, 'cid': cid, 'qn': quality, # 清晰度选择 'fnval': 80, # 流格式标识,80代表M3U8格式 'fnver': 0, 'fourk': 1, # 是否允许4K,1为允许 } api_url = 'https://api.bilibili.com/x/player/playurl' # 注意:此接口通常需要携带Cookie才能获取高清晰度流 # 对于无需登录的清晰度,可以不传Cookie,但可能被限制。 resp = requests.get(api_url, params=params, headers=headers) resp.raise_for_status() data = resp.json() if data['code'] != 0: raise Exception(f"API请求失败: {data['message']}") # 解析返回的JSON,获取视频和音频的M3U8地址 play_info = data['data'] video_url = None audio_url = None # dash格式(fnval=80时返回dash流) if 'dash' in play_info: dash = play_info['dash'] # 视频流 if 'video' in dash and len(dash['video']) > 0: # 选择第一个视频流(通常是我们请求的清晰度) video_info = dash['video'][0] video_url = video_info.get('baseUrl') or video_info.get('backupUrl')[0] # 可能有备用地址 # 音频流 if 'audio' in dash and len(dash['audio']) > 0: audio_info = dash['audio'][0] audio_url = audio_info.get('baseUrl') or audio_info.get('backupUrl')[0] # 非dash格式(老格式或低清晰度,直接返回url) elif 'durl' in play_info and len(play_info['durl']) > 0: # 这种情况下,durl里可能是直接的MP4/FLV地址,也可能是M3U8地址 video_url = play_info['durl'][0].get('url') # 注意:非dash格式通常音画一体,没有单独的音频流 audio_url = None if not video_url: raise Exception("未能从API响应中提取到视频流地址。") return video_url, audio_url # 使用示例 video_m3u8_url, audio_m3u8_url = get_playurl(bvid, cid, quality=64) # 请求720P清晰度 print(f"视频流地址: {video_m3u8_url[:100]}...") # 打印前100字符 if audio_m3u8_url: print(f"音频流地址: {audio_m3u8_url[:100]}...")

核心细节解析

  1. qn(quality) 参数:这个值决定了你请求的清晰度。常见值有:120(4K), 116(1080P 60帧), 112(1080P+ 高码率), 80(1080P), 64(720P), 32(480P)等。但高清晰度(如80及以上)通常需要登录Cookie(即大会员或已登录账号)才能获取。如果没有提供有效的Cookie,API可能只会返回低清晰度的流,或者直接返回错误。
  2. fnval参数:这是关键。fnval=80表示我们请求DASH格式的流,它通常意味着音视频分离的M3U8格式,这是获取高清晰度的前提。如果设为其他值(如0或16),可能会返回音画一体的FLV或MP4地址,但清晰度选项可能受限。
  3. fourk参数:设置为1表示请求4K流(如果有的话)。
  4. Cookie问题:这是最大的门槛。为了获取高清晰度或大会员视频,必须在请求头中携带有效的Cookie。这个Cookie需要你从已登录B站网页版的浏览器中手动复制出来,并注意其有效期和安全风险。切勿在公开代码或网络上泄露你的Cookie!对于学习和测试,可以先用无需登录的清晰度(如qn=64)跑通流程。
  5. 备用地址(backupUrl):API返回的流地址可能有一个backup_url列表,这是B站提供的备用CDN地址。当主地址(base_url)下载失败时,可以尝试备用地址,提高下载成功率。

6. 第三步:下载与合并音视频流(TS片段)

现在我们拿到了M3U8地址。如果是DASH格式,我们会得到两个地址:一个纯视频,一个纯音频。我们需要分别处理它们。

6.1 解析M3U8文件并下载TS片段

我们使用m3u8库来解析M3U8文件。由于TS片段数量可能很多,我们将使用aiohttp进行异步下载以提升效率。

首先,安装异步下载所需的库:

pip install aiohttp aiofiles

然后,编写异步下载函数:

import aiohttp import aiofiles import asyncio import os from m3u8 import M3U8 async def download_ts_segment(session, url, filepath, headers): """ 异步下载单个TS片段 """ try: async with session.get(url, headers=headers) as resp: if resp.status == 200: content = await resp.read() async with aiofiles.open(filepath, 'wb') as f: await f.write(content) print(f"下载成功: {os.path.basename(filepath)}") return True else: print(f"下载失败 {resp.status}: {url}") return False except Exception as e: print(f"下载异常 {url}: {e}") return False async def download_m3u8(m3u8_url, output_filename, headers, max_concurrent=5): """ 下载整个M3U8流的所有TS片段并合并为一个文件 """ # 1. 获取并解析M3U8文件 resp = requests.get(m3u8_url, headers=headers) m3u8_obj = M3U8(resp.text, base_uri=m3u8_url.rsplit('/', 1)[0] + '/') # 2. 准备下载目录 temp_dir = f"temp_{output_filename}" os.makedirs(temp_dir, exist_ok=True) # 3. 构建所有TS片段的完整URL和本地保存路径 segments = [] for i, segment in enumerate(m3u8_obj.segments): # 处理相对路径或绝对路径 if segment.uri.startswith('http'): ts_url = segment.uri else: ts_url = m3u8_obj.base_uri + segment.uri ts_filename = os.path.join(temp_dir, f"seg_{i:05d}.ts") segments.append((ts_url, ts_filename)) print(f"开始下载 {output_filename},共 {len(segments)} 个片段...") # 4. 异步下载所有片段 connector = aiohttp.TCPConnector(limit=max_concurrent) # 限制并发连接数 async with aiohttp.ClientSession(connector=connector, headers=headers) as session: tasks = [] for ts_url, ts_filename in segments: task = asyncio.create_task(download_ts_segment(session, ts_url, ts_filename, headers)) tasks.append(task) results = await asyncio.gather(*tasks, return_exceptions=True) # 5. 检查下载结果并合并TS文件 success_count = sum(1 for r in results if r is True) if success_count != len(segments): print(f"警告:部分片段下载失败。成功 {success_count}/{len(segments)}") # 在实际应用中,这里可以加入重试逻辑 # 6. 按顺序合并所有TS文件 merged_file = output_filename + '.ts' with open(merged_file, 'wb') as outfile: for i in range(len(segments)): ts_filename = os.path.join(temp_dir, f"seg_{i:05d}.ts") if os.path.exists(ts_filename): with open(ts_filename, 'rb') as infile: outfile.write(infile.read()) else: print(f"片段 {ts_filename} 缺失,合并结果可能不完整。") print(f"合并完成: {merged_file}") # 7. 清理临时文件(可选) # import shutil # shutil.rmtree(temp_dir) return merged_file # 注意:由于 asyncio 在Jupyter或某些环境下的运行方式,你可能需要使用 asyncio.run() 来调用异步函数。 # 例如,在主程序中: async def main_download(): headers = {...} # 你的请求头,务必包含Referer video_url, audio_url = get_playurl(bvid, cid, quality=64) video_file = await download_m3u8(video_url, 'video_temp', headers) if audio_url: audio_file = await download_m3u8(audio_url, 'audio_temp', headers) else: audio_file = None return video_file, audio_file # 运行异步主函数 # video_ts, audio_ts = asyncio.run(main_download())

6.2 使用FFmpeg合并音视频

现在我们有了一堆.ts文件,或者已经合并成了video_temp.tsaudio_temp.ts。最后一步,也是最关键的一步,就是使用FFmpeg将它们合成为一个标准的MP4文件。

FFmpeg的命令行功能非常强大,我们通过Python的subprocess模块来调用它。

import subprocess import os def merge_av_with_ffmpeg(video_input, audio_input, output_filename): """ 使用FFmpeg合并视频和音频流 :param video_input: 输入视频文件路径(.ts或其它) :param audio_input: 输入音频文件路径(.ts或其它),如果为None则只复制视频流 :param output_filename: 输出文件路径(.mp4) """ cmd = ['ffmpeg', '-y'] # -y 表示覆盖已存在文件 cmd.extend(['-i', video_input]) if audio_input and os.path.exists(audio_input): cmd.extend(['-i', audio_input]) # 映射流:-map 0:v 表示第一个输入文件(视频)的视频流,-map 1:a 表示第二个输入文件(音频)的音频流 cmd.extend(['-c:v', 'copy', '-c:a', 'aac', '-map', '0:v:0', '-map', '1:a:0']) else: # 如果没有单独的音频流,可能视频流本身包含音频,或者我们只需要视频 # 这里假设视频流已包含音频,直接复制所有流 cmd.extend(['-c', 'copy']) cmd.append(output_filename) print(f"执行命令: {' '.join(cmd)}") try: result = subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8') print("FFmpeg合并成功!") print(result.stdout) except subprocess.CalledProcessError as e: print("FFmpeg合并失败!") print("标准错误输出:", e.stderr) raise # 使用示例 # 假设我们已经下载并合并得到了 video_temp.ts 和 audio_temp.ts merge_av_with_ffmpeg('video_temp.ts', 'audio_temp.ts', 'final_output.mp4')

FFmpeg参数详解与避坑指南

  • -c:v copy-c:a aac-c:v copy表示视频流(video)直接复制(不重新编码),这能保证速度最快且画质无损。-c:a aac表示音频流(audio)使用AAC编码器重新编码。为什么音频不直接复制?因为从TS文件里解封装出来的音频流编码格式可能是MPEG Audio等,直接复制进MP4容器可能兼容性不好。AAC是MP4标准兼容的通用格式,重新编码能确保最大兼容性,虽然会有极轻微的音质损失,但对绝大多数场景无感知。如果你确认源音频就是AAC且想绝对无损,可以尝试-c:a copy
  • -map参数:当有多个输入文件(如一个视频文件、一个音频文件)时,必须用-map指定使用哪个文件的哪个流。0:v:0表示第一个输入文件(索引0)的第一个视频流(stream 0)。1:a:0表示第二个输入文件(索引1)的第一个音频流。
  • 编码错误处理:如果合并失败,FFmpeg会输出详细的错误信息到stderr。常见错误包括:编码器不支持、时间戳问题、流格式不匹配等。根据错误信息搜索解决方案,通常可以通过添加-avoid_negative_ts make_zero-fflags +genpts等参数来解决时间戳问题。
  • 性能考虑:重新编码(尤其是视频编码)极其耗时。务必使用-c:v copy来避免视频重新编码。音频的AAC编码很快,影响不大。

7. 第四步:整合与优化,打造健壮的下载脚本

将以上所有步骤整合起来,我们就得到了一个完整的、可运行的B站视频下载脚本。但一个健壮的工具还需要考虑更多细节。

7.1 完整脚本框架与错误处理

下面是一个整合后的简化框架,包含了基本的错误处理和日志输出:

import requests import re import json import asyncio import aiohttp import aiofiles import subprocess import os from bs4 import BeautifulSoup from m3u8 import M3U8 class BilibiliVideoDownloader: def __init__(self, headers=None): self.headers = headers or { 'User-Agent': 'Mozilla/5.0...', 'Referer': 'https://www.bilibili.com/', } self.session = requests.Session() self.session.headers.update(self.headers) def get_bvid_cid(self, url): # ... 实现上述解析函数 ... pass def get_playurl(self, bvid, cid, quality=80, cookie_str=None): # ... 实现上述API请求函数,支持传入Cookie字符串 ... params = {...} if cookie_str: self.headers['Cookie'] = cookie_str # ... 发送请求并解析 ... pass async def _download_m3u8_async(self, m3u8_url, output_name): # ... 实现上述异步下载函数 ... pass def download_video(self, url, quality=64, cookie=None, output_dir='./downloads'): """ 主下载函数 """ print(f"开始处理: {url}") try: # 1. 获取bvid和cid bvid, cid = self.get_bvid_cid(url) print(f"获取到 bvid: {bvid}, cid: {cid}") # 2. 获取播放地址 video_url, audio_url = self.get_playurl(bvid, cid, quality, cookie) print(f"获取到视频流地址 (前100字符): {video_url[:100]}...") if audio_url: print(f"获取到音频流地址 (前100字符): {audio_url[:100]}...") # 3. 创建输出目录 os.makedirs(output_dir, exist_ok=True) base_name = f"{bvid}_p{cid}_q{quality}" video_ts_path = os.path.join(output_dir, f"{base_name}_video.ts") audio_ts_path = os.path.join(output_dir, f"{base_name}_audio.ts") if audio_url else None final_mp4_path = os.path.join(output_dir, f"{base_name}.mp4") # 4. 异步下载 print("开始下载视频流...") loop = asyncio.get_event_loop() video_task = self._download_m3u8_async(video_url, video_ts_path) audio_task = None if audio_url: print("开始下载音频流...") audio_task = self._download_m3u8_async(audio_url, audio_ts_path) # 等待下载完成 if audio_task: loop.run_until_complete(asyncio.gather(video_task, audio_task)) else: loop.run_until_complete(video_task) # 5. 使用FFmpeg合并 print("开始合并音视频...") self.merge_with_ffmpeg(video_ts_path, audio_ts_path, final_mp4_path) print(f"视频下载并合并完成: {final_mp4_path}") # 6. 清理临时TS文件(可选) # os.remove(video_ts_path) # if audio_ts_path and os.path.exists(audio_ts_path): # os.remove(audio_ts_path) except Exception as e: print(f"下载过程发生错误: {e}") import traceback traceback.print_exc() def merge_with_ffmpeg(self, video_input, audio_input, output_path): # ... 实现上述FFmpeg合并函数 ... pass if __name__ == '__main__': downloader = BilibiliVideoDownloader() # 示例:下载一个720P视频(无需Cookie) # 注意:将 YOUR_BILIBILI_COOKIE 替换成你自己的Cookie字符串(如果需要高清晰度) # cookie = "SESSDATA=xxxxxx; bili_jct=xxxxxx; ..." cookie = None # 不使用Cookie,下载普通清晰度 video_url = "https://www.bilibili.com/video/BV1GJ411x7h7" downloader.download_video(video_url, quality=64, cookie=cookie, output_dir='./videos')

7.2 进阶优化与注意事项

  1. 清晰度选择与登录态:如前所述,获取高清晰度(1080P以上、60帧、杜比视界等)必须提供有效的登录Cookie。你可以通过浏览器插件(如EditThisCookie)导出Cookie字符串。请妥善保管,不要泄露。
  2. 并发控制与速率限制:异步下载虽然快,但过高的并发请求可能会被B站的服务器限制或封禁IP。max_concurrent参数不宜设置过大(建议5-10)。可以考虑在请求间增加随机延时。
  3. 断点续传与错误重试:上述脚本没有实现断点续传。一个更健壮的版本应该记录已下载的片段,并在下次启动时跳过。对于下载失败的片段,应该实现重试机制(例如重试3次)。
  4. 代理设置:如果你的网络环境需要,可以在aiohttp.ClientSessionrequests.Session中设置代理。
  5. 法律与道德风险本工具及技术分享仅限用于个人学习、研究、欣赏之目的。请务必尊重UP主的版权和劳动成果,下载后的视频不应用于任何商业用途或未经授权的二次传播。技术是一把双刃剑,请务必在法律和道德框架内使用它。
  6. B站API变更:B站的后端接口和前端结构可能会更新。如果某天脚本突然不能用了,最可能的原因是获取bvid/cid的页面解析方法或playurlAPI的参数发生了变化。此时需要重新打开开发者工具,分析最新的网络请求,调整解析逻辑和API参数。

通过这“四步”——解析页面、请求API、下载片段、合并文件——我们不仅实现了一个B站视频下载工具,更深入理解了现代流媒体网站的工作机制、网络爬虫的基本伦理以及Python处理多媒体任务的强大能力。这个过程充满挑战,但解决问题的乐趣和最终成功的成就感,正是技术人前进的动力。希望这篇超详细的指南能为你打开一扇窗,让你在需要时,能拥有一个自己掌控的、可靠的解决方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询