1. 项目概述:一个B站收藏夹的“离线档案馆”
如果你和我一样,是个重度B站用户,那么你的收藏夹里一定塞满了各种宝藏视频:从硬核的技术教程、精彩的影视剪辑,到让你捧腹的搞笑片段,再到深夜助眠的ASMR。这些视频构成了我们数字生活的一部分,是宝贵的知识库和回忆录。然而,依赖在线平台保存这些内容,总让人心里不踏实。视频可能因为UP主删除、版权问题、平台审核等原因突然“失效”,变成灰色的“视频不见了”。那种点开收藏链接却看到一片空白的感觉,就像精心整理的实体书架突然被清空了一格。
“sunrisever/bilibili-favorites”这个项目,就是为了解决这个痛点而生的。它是一个开源工具,核心功能就是帮你把B站(bilibili)收藏夹里的视频,批量、自动化地下载到本地,并整理成结构清晰的离线档案。你可以把它理解为一个为你私人收藏夹打造的“离线档案馆”或“数字保险箱”。它不仅仅是一个简单的下载器,更是一个注重元数据保存和本地化管理的解决方案。
这个工具适合谁呢?首先是内容创作者和研究者,他们可能需要将B站上的参考资料永久保存以备查阅;其次是学习爱好者,希望将教程视频离线保存,在没有网络的环境(如通勤、出差)也能学习;再者是纯粹的收藏癖和数字资产管理者,他们对数据拥有欲和安全性有更高的要求。简单来说,任何不希望自己珍贵的B站收藏因平台变动而丢失的人,都是这个项目的潜在用户。
2. 核心设计思路:为何选择“元数据优先”的架构
市面上视频下载工具不少,那这个项目有什么特别之处?它的核心设计思路,可以概括为“元数据优先,结构化归档”。这不仅仅是把视频文件(.mp4, .flv)拖下来那么简单。
2.1 超越单纯下载:保存完整的上下文信息
一个视频的价值,除了其二进制内容,还包含大量围绕它的“上下文信息”:标题、UP主、简介、分P列表、发布时间、播放量、点赞投币收藏数,甚至包括封面图、字幕文件(如果有的话)。普通的下载器往往只保存视频文件,顶多把标题作为文件名。而bilibili-favorites在设计上,致力于保存这些完整的元数据。
为什么这很重要?想象一下,五年后你打开本地文件夹,看到一个名为video_123456.mp4的文件,你很可能完全想不起这是什么内容。但如果同时有一个结构化的info.json文件,里面记录了标题《三小时入门机器学习:从零到Kaggle实战》、UP主“AI魔术师”、简介和标签,甚至还有你当初添加收藏时的备注,那么这个文件的长期可读性和可用性就大大提升了。这个项目正是通过保存这些元数据,在本地“复刻”了B站页面的核心信息结构。
2.2 以收藏夹为单位的批量操作逻辑
项目的另一个核心设计是以“收藏夹”为操作单元,而不是单个视频。这符合大多数用户的使用习惯——我们通常是以收藏夹来分类管理兴趣的(比如“编程学习”、“美食制作”、“电影盘点”)。工具会先通过B站API或模拟请求,获取你指定收藏夹的所有视频列表(包括公开收藏夹和需要登录的私有收藏夹),然后根据这个列表进行批量下载。
这种设计带来了几个优势:
- 自动化程度高:一次性配置,即可归档整个收藏夹,无需逐个视频操作。
- 保持原有结构:本地文件夹可以按照收藏夹名称创建,内部视频保持列表中的顺序,便于回顾。
- 增量同步:理论上,工具可以设计为只下载收藏夹中新增的视频,实现本地档案与线上收藏的同步更新。
2.3 技术栈选型考量:平衡效率与可维护性
虽然项目代码的具体实现需要查看其GitHub仓库,但我们可以推断其技术选型会围绕几个核心需求展开:
- 网络请求与解析:需要处理B站网页或API。Python的
requests库配合BeautifulSoup或直接解析JSON API是常见选择,高效且生态成熟。考虑到B站页面动态加载,也可能用到Selenium或Playwright进行模拟,但后者更重,通常作为备选。 - 视频流下载:这是核心功能。可能会集成
you-get、youtube-dl(或其分支yt-dlp)这类强大的命令行下载工具。它们本身支持B站,能处理多种画质、格式,并且支持下载字幕和封面。项目本身可能作为这些工具的一个“调度器”和“元数据增强包装器”。 - 元数据管理与持久化:将获取到的视频信息(JSON格式)和封面图片等保存到本地指定目录。Python的标准库(
json,os,pathlib)足以胜任。 - 配置与日志:使用配置文件(如
config.ini或config.json)来保存用户设置(如B站Cookie、下载路径、下载质量)。完善的日志系统对于长时间运行的批量任务至关重要,便于排查失败原因。
注意:任何涉及爬取和下载的行为,都必须严格遵守目标网站的
robots.txt协议,并尊重版权。此工具应仅用于个人备份在合理使用范围内的、自己已收藏的内容,严禁用于批量盗录、分发或其他侵犯版权的行为。高频、大规模的请求可能会对B站服务器造成压力,也可能触发反爬机制导致账号风险,使用时务必设置合理的请求间隔(如time.sleep)。
3. 实操部署与核心配置详解
假设我们已经在本地环境(Windows/macOS/Linux)准备好Python(建议3.7+),接下来看看如何让这个“离线档案馆”运转起来。
3.1 环境准备与依赖安装
通常,这类开源项目会提供一个requirements.txt文件来管理依赖。我们的第一步是克隆或下载项目代码,并安装必要的库。
# 1. 克隆项目代码(假设项目托管在GitHub) git clone https://github.com/sunrisever/bilibili-favorites.git cd bilibili-favorites # 2. 创建并激活一个虚拟环境(强烈推荐,避免污染系统Python环境) python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 3. 安装项目依赖 pip install -r requirements.txt如果项目没有提供requirements.txt,我们根据其设计思路,可能需要手动安装一些核心库:
pip install requests beautifulsoup4 yt-dlp # yt-dlp 是 youtube-dl 的一个更活跃的分支,对B站支持通常更好3.2 关键配置解析:Cookie与下载参数
配置是工具运行的核心,通常在一个config.ini或config.json文件中。最关键的配置项莫过于B站登录Cookie。
1. 如何获取B站Cookie?Cookie是你登录状态的凭证,工具需要用它来访问你的私有收藏夹。获取方法如下:
- 在浏览器中登录B站(www.bilibili.com)。
- 打开开发者工具(F12),切换到
Network(网络)标签页。 - 刷新页面,在第一个请求(通常是
www.bilibili.com)的Request Headers(请求头)中找到Cookie字段。 - 复制整个
Cookie字符串(很长,包含SESSDATA、bili_jct、DedeUserID等键值对)。
2. 配置Cookie示例(config.ini格式):
[bilibili] cookie = 你的完整Cookie字符串 # 例如:SESSDATA=xxxxxx; bili_jct=xxxxxx; DedeUserID=xxxxxx; [download] save_path = ./downloads # 下载保存的基础路径 quality = 80 # 优先下载的画质代码,80通常是1080P,64是720P,32是480P threads = 3 # 同时下载的视频线程数,不宜过高,建议2-5 interval = 2 # 请求间隔秒数,避免请求过快quality参数:B站画质有对应的数字代码。如果不确定,可以设置为best(最佳)或worst(最差),让yt-dlp自动选择。指定代码可以精确控制体积和清晰度。threads和interval:这是平衡效率和风险的关键。线程数太多、间隔太短,极易被B站风控,可能导致IP或账号临时被封禁。对于个人备份,保守一点更安全。
3. 收藏夹ID的获取:工具需要知道你要备份哪个收藏夹。B站的每个收藏夹都有一个唯一的ID,通常包含在收藏夹的URL中。 例如,你的收藏夹URL是:https://space.bilibili.com/123456/favlist?fid=12345那么,fid=12345中的12345就是该收藏夹的ID。你需要将这个ID作为运行命令或配置的参数。
3.3 运行流程与目录结构生成
配置完成后,运行主程序(假设为main.py),并传入收藏夹ID。
python main.py --fid 12345 # 或者如果设计为交互式,程序可能会提示你输入一个设计良好的工具,其运行流程应该是:
- 初始化:读取配置,检查网络和Cookie有效性。
- 获取列表:使用Cookie访问B站API,获取收藏夹
fid=12345下的所有视频列表(包括BV号/AV号、标题、UP主等信息)。 - 解析元数据:对列表中的每个视频,进一步获取其详细信息(简介、分P、封面图URL等)。
- 创建本地结构:在
save_path下创建以收藏夹名命名的文件夹(如“我的编程学习库”),并在其内为每个视频创建子文件夹(以[BV号]_[简短标题]命名,避免长文件名和非法字符问题)。 - 下载资源:按顺序或并行下载每个视频的子资源:
- 视频文件(通过
yt-dlp调用)。 - 封面图片(通过
requests下载)。 - 字幕文件(如果存在且工具支持)。
- 将视频的元数据(JSON格式)保存到该视频文件夹内,命名为
info.json。
- 视频文件(通过
- 生成索引:所有视频下载完成后,可能会生成一个汇总的
index.html或index.json文件,方便在本地浏览器中浏览整个收藏夹的目录。
最终生成的本地目录结构会非常清晰:
downloads/ ├── 我的编程学习库/ │ ├── BV1xx411c7Xu_Python入门教程/ │ │ ├── info.json # 视频元数据 │ │ ├── cover.jpg # 封面图 │ │ ├── video.mp4 # 视频文件 │ │ └── [可选] chs.srt # 中文字幕 │ ├── BV1yx411c7Yv_数据结构精讲/ │ │ ├── info.json │ │ ├── cover.jpg │ │ └── video.mp4 │ └── index.html # 本地浏览索引 └── 美食烹饪合集/ ├── ...4. 深入核心环节:元数据抓取与下载策略
4.1 元数据抓取的稳定性保障
B站的页面结构或API可能会变动,因此元数据抓取的代码需要一定的鲁棒性。常见的策略是多源获取与优雅降级。
- 主源:官方API:B站有未公开但相对稳定的API接口,通过分析XHR请求可以找到。例如,获取收藏夹列表的API可能形如
https://api.bilibili.com/x/v3/fav/resource/list?media_id={fid}&...。使用API返回的是结构化JSON数据,解析效率高、稳定性好,是首选方案。 - 备选源:页面解析:如果API失效或无法使用,则退回到解析HTML页面。使用
BeautifulSoup定位页面中的<script>标签,其中往往含有window.__INITIAL_STATE__这样的变量,里面包含了页面初始化的完整JSON数据。这是一种经典的“数据藏在JS变量里”的爬虫方案。 - 异常处理与重试:网络请求必须包裹在
try...except中,并设置重试机制(如tenacity库)。对于单个视频信息获取失败,应记录日志并跳过,避免影响整个批处理任务。
4.2 视频下载的进阶策略
直接调用yt-dlp是最简单可靠的方式。但为了更好的控制,我们可以深入其参数:
import yt_dlp ydl_opts = { 'outtmpl': f'{video_dir}/video.%(ext)s', # 输出模板,固定文件名便于管理 'format': 'bestvideo[height<=1080]+bestaudio/best[height<=1080]', # 限制最高1080P 'writethumbnail': True, # 下载封面 'writesubtitles': True, # 下载字幕 'subtitleslangs': ['zh-Hans', 'zh-Hant', 'en'], # 优先中英文字幕 'quiet': False, # 显示进度 'no_warnings': False, 'http_headers': { # 携带Cookie和UA,模拟浏览器 'User-Agent': '你的浏览器UA', 'Cookie': config.get('bilibili', 'cookie') }, 'sleep_interval_requests': 5, # 请求间隔,礼貌爬虫 } with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([video_url])outtmpl:这里我们固定了输出文件名为video.mp4,因为视频文件夹已经用BV号和标题命名了,这样更简洁。元数据信息保存在info.json里,无需体现在文件名中。format:这是一个过滤器,bestvideo[height<=1080]+bestaudio表示选择分辨率不超过1080p的最佳视频流和最佳音频流,然后合并。这是控制画质和文件大小的关键。http_headers:添加Cookie和User-Agent至关重要,特别是对于需要登录才能观看或下载高画质的视频。
4.3 并发控制与断点续传
对于成百上千个视频的收藏夹,顺序下载太慢。我们需要并发,但必须可控。
- 使用线程池或异步IO:Python的
concurrent.futures.ThreadPoolExecutor是一个简单选择。将视频下载任务提交到线程池,通过max_workers参数控制并发数(与配置中的threads对应)。 - 任务队列与状态记录:更健壮的做法是引入一个任务队列(如
queue.Queue),主线程负责生产任务(视频URL),工作线程消费。同时,将任务状态(待下载、下载中、已完成、失败)实时记录到一个状态文件(如status.json)或小型数据库中。这样,即使程序意外中断,重启后也能读取状态文件,跳过已完成的,继续下载未完成的,实现“断点续传”效果。 - 资源限制:除了控制线程数,还要注意磁盘I/O和网络带宽。如果同时下载多个高清视频,可能会写满磁盘或拖慢网络。可以在代码中监控磁盘剩余空间,或在配置中提供“最大同时下载数”选项。
5. 实战中常见问题与排查实录
即使工具设计得再完善,在实际运行中也会遇到各种问题。下面是我在类似项目中踩过的坑和解决方案。
5.1 Cookie失效与风控应对
问题现象:程序运行后,获取收藏夹列表为空,或直接返回错误码-403(禁止访问)。
原因与排查:
- Cookie过期:B站Cookie(尤其是
SESSDATA)有有效期(通常几个月)。解决方法:重新登录B站,获取新的Cookie字符串更新到配置中。 - 环境异常:B站的风控系统可能检测到异常的请求模式(如固定IP高频请求、无浏览器指纹的请求头)。
yt-dlp或requests的默认请求头可能过于简单。
解决方案:
- 模拟更真实的请求头:不要只用简单的
User-Agent,可以从浏览器开发者工具里复制一个完整的请求头,包括Accept、Accept-Language、Referer等字段。headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 'Accept-Encoding': 'gzip, deflate, br', 'Referer': 'https://www.bilibili.com/', 'Cookie': your_cookie_string } - 增加请求间隔与随机延迟:在请求之间加入
sleep,并且时间可以有一定随机性(如time.sleep(2 + random.random()*3)),模仿人类操作。 - 使用IP代理池(高级):如果收藏夹非常大,请求非常密集,考虑使用可靠的代理IP轮询,避免单一IP被限制。但这对于个人备份通常不是必须的,且增加了复杂性和成本。
5.2 视频下载失败或格式错误
问题现象:某个视频下载卡住、速度极慢,或下载后的文件无法播放。
原因与排查:
- 网络问题或视频源失效:部分老视频的源地址可能失效。
yt-dlp版本过旧或解析失败:B站的流媒体格式可能更新,导致旧版yt-dlp无法正确解析。- 格式选择冲突:指定的
format参数在当前视频上不可用。
解决方案:
- 更新工具:首先确保
yt-dlp是最新版本(pip install -U yt-dlp)。 - 简化格式参数:将
format参数暂时改为'best',让yt-dlp自动选择最可用的格式。 - 查看详细错误:运行
yt-dlp时添加-v(verbose)参数,查看详细的错误信息和它尝试提取的流列表,这能提供关键的排查线索。 - 手动指定URL:对于极个别顽固视频,可以尝试在B站网页端打开视频,使用浏览器的“检查元素”->“网络”工具,过滤
m4s或flv请求,手动找到视频流地址,用其他下载器(如IDM)下载。但这只是权宜之计。
5.3 存储空间管理与文件整理
问题现象:下载了几个收藏夹后,硬盘空间告急;或者视频文件夹命名混乱,包含非法字符导致程序出错。
解决方案与心得:
- 预算与规划:在开始大规模备份前,先估算所需空间。一个1080P的30分钟视频大约300-500MB。一个包含100个视频的收藏夹可能需要30-50GB。确保目标磁盘有足够空间,或者使用外置硬盘。
- 智能命名与清理:在创建视频文件夹时,要对标题进行清洗,移除Windows/Linux文件名不允许的字符(如
\ / : * ? " < > |),并将过长标题截断。可以使用正则表达式或字符串替换函数。import re def sanitize_filename(filename): # 移除非法字符,替换为下划线 filename = re.sub(r'[\\/*?:"<>|]', '_', filename) # 截断过长文件名(例如保留前100个字符) if len(filename) > 100: filename = filename[:97] + '...' return filename - 按需下载与过滤:工具可以增加过滤功能,例如只下载某段时间后收藏的视频,或只下载特定UP主的视频,避免一次性下载全部。
- 定期维护:本地档案也需要维护。可以编写一个简单的脚本,定期检查
info.json和视频文件的完整性(如文件大小是否异常小),或者根据在线收藏夹的变动(删除)来同步清理本地文件。
5.4 法律与道德边界
这是最重要也最容易被忽视的“问题”。
- 版权尊重:你备份的视频版权仍属于创作者和B站。这些备份仅限于个人学习、研究、欣赏之用,切勿用于公开传播、二次分发、商业用途或任何可能侵害版权方利益的行为。
- 账号安全:不要使用你的主账号Cookie在不可信的服务器或第三方工具上运行此类程序。理论上,获取了你Cookie的人可以操作你的账号(发弹幕、评论、甚至修改密码)。尽量在你自己可控的电脑上运行。
- 访问频率:将请求间隔设置得足够长(如5-10秒),做一个“礼貌”的爬虫。你的目的是备份,不是攻击服务器。
6. 扩展思路:让离线档案馆更强大
基础功能稳定后,我们可以思考如何让这个“档案馆”更好用。这里分享几个我实践过或认为有价值的扩展方向。
6.1 集成全文搜索与标签管理
本地保存了几百个视频后,如何快速找到某个特定内容?靠翻文件夹效率太低。我们可以利用保存的info.json元数据,构建一个轻量级的本地搜索引擎。
- 方案一:SQLite数据库:将所有视频的元数据(标题、UP主、简介、标签)导入一个SQLite数据库。然后就可以用SQL语句进行灵活的查询,例如“查找所有标题包含‘Python’且收藏时间在2023年以后的视频”。
- 方案二:Whoosh或MiniSearch:使用Python的轻量级全文检索引擎库(如
whoosh)。它可以对标题、简介等文本字段建立索引,实现类似百度、谷歌的关键词快速搜索,并支持结果排序。 - 方案三:与本地媒体管理软件集成:例如,使用
Emby、Jellyfin或Plex这类媒体服务器软件。你可以将下载的视频文件夹作为媒体库添加进去。这些软件能自动刮削元数据(虽然对B站视频可能识别不准),但更重要的是它们提供了强大的跨设备播放、分类和搜索界面。你可以手动或通过脚本,利用info.json中的信息来重命名视频文件,以帮助媒体服务器正确识别。
6.2 自动化与定期同步
备份不应该是一次性的劳动。理想状态是,每当我在B站收藏了新视频,我的本地档案馆能自动或半自动地将其归档。
- 计划任务:在Windows上使用“任务计划程序”,在macOS/Linux上使用
cron,定期(如每周日凌晨2点)运行一次下载脚本。脚本可以设计为“增量模式”,只处理自上次同步后新增到收藏夹的视频。 - 如何识别新增视频?最简单的办法是记录上次同步时最后一个视频的收藏时间(
info.json里可以记录fav_time),下次只下载收藏时间晚于这个点的视频。或者更鲁棒一点,记录已下载视频的BV号列表,只下载不在列表中的新BV号。 - 通知机制:同步完成后,脚本可以发送一封邮件、一条Telegram/Bark消息,或者只是在日志中标记,告诉你本次同步的结果(成功下载X个,失败Y个)。
6.3 处理B站特有的内容形式
B站不止有单视频,还有合集、课程、番剧等。一个完善的工具可能需要考虑这些形式。
- 合集与播放列表:一个合集(Collection)包含多个视频。下载时,应该创建一个合集文件夹,里面再为每个视频创建子文件夹。同时,还需要下载合集本身的封面和描述。
- 分P视频:一个视频有多个分P(Part)。
yt-dlp通常能很好地处理,将所有分P下载到一个文件夹内,并自动命名(如video - P1.mp4,video - P2.mp4)。我们需要确保元数据info.json里也记录了分P信息。 - 互动视频:这类视频结构特殊,下载下来的可能只是主线流程。目前还没有完美的离线保存方案,这是工具的一个局限性。
最后,我想说的是,sunrisever/bilibili-favorites这类项目代表的是一种理念:在云时代,我们仍然需要对自己认为重要的数字内容保持一定的物理控制力。它不是一个鼓励盗版的工具,而是一个用于数字遗产个人备份的“保险绳”。在使用的过程中,我最大的体会是“自动化”和“结构化”带来的安心感。一旦配置好,它就在后台默默工作,将你在互联网上零散的、易逝的兴趣点,汇聚成一份稳固的、属于你自己的数字收藏。这个过程本身,也是对个人知识体系和兴趣轨迹的一次有趣梳理。