B站收藏夹这东西,越攒越乱,越乱越不敢动。我自己的账号大概收藏了两千多条视频,前阵子想整理,发现靠网页端一条条复制链接根本不现实,翻了三页就放弃了。后来仔细研究了一轮,把B站收藏夹快速导出的方案彻底跑通了:从API原理到最终拿到一份整理好的Excel表格,全程可复现。这篇文章就把完整操作和踩过的坑记录下来,给同样有备份、整理、迁移需求的兄弟们一个参考。不管你是单纯想备份,还是想分析自己的收藏习惯,这篇都能直接照搬。
1. 万事开头难:先把导出方案定下来
1.1 为什么B站官方不做“一键导出”
B站目前没有提供任何“导出收藏夹”的官方按钮,这是平台策略决定的,不只是技术做不到。收藏数据一旦沉淀在某个平台上,就相当于用户的隐形资产;平台不提供导出,本质上就是在提升用户迁移的成本。这个逻辑很容易理解,类似很多笔记软件、视频平台、音乐APP,入口做得很顺,出口却永远模模糊糊。
但对用户来说,这个限制很麻烦。收藏夹里的内容不只是“看过的东西”,很多是稍微晚一点就要用的教程、值得反复看的纪录片、想学习的公开课。我一直担心一件事:哪天某个视频被UP主删除,或者平台调整分区策略,收藏夹里那些内容就彻底消失了。与其被动等,不如主动做一份本地备份。
所以“导出收藏夹”这件事,核心价值有两个:一是备份,把自己的数据真正掌握在手里;二是整理,导出的数据可以做成表格,按时间、UP主、分区去复盘自己到底收藏了什么,信息价值远超在网页端干巴巴地翻页。
1.2 三条路线的优劣对比与选型建议
了解完需求,再来看可行的方案。目前想把B站收藏夹导出来,主要有三条路:手动复制、现成的开源工具、自己写脚本。这三条路我都试过,先说结论:收藏量在100条以内,手动处理勉强能忍;几百上千条,只有后两条路靠谱。
手动复制最大的问题不是慢,是容易漏。B站收藏夹有分页加载,翻到后面还会出现滚动懒加载的情况,你很难确认到底有没有全部复制完。而且复制下来的内容通常只有标题和链接,收藏时间、UP主这些信息丢了,后续做数据分析就无从谈起。
现成的开源工具确实省事,GitHub上有不少“B站收藏夹导出”相关的项目,有些甚至带GUI界面。但它们的通病是更新慢,B站接口只要升级一次,工具就可能失效,而且把账号Cookie交给第三方工具本身就有安全风险,我更推荐自己动手写脚本。
自己写脚本听起来门槛高,其实拆开看就是“调两个API、处理几页JSON、写进一张表格”,完全在能力范围内。而且脚本方案的最大优势是可控:Cookie只保存在本地、字段按需定制、导出的格式自己选。这篇文章后面提到的代码,就是我实测可用的最终版本,可以直接复制使用。
2. 核心解密:B站收藏夹API与请求参数
2.1 两个必须了解的官方API接口
B站的官方API是现成的,不需要任何申请密钥,只要带上自己账号的登录态即可访问。要完成收藏夹导出,只需要用到两个核心接口。
第一个是获取“收藏夹列表”。这个接口会返回当前账号下创建的所有收藏夹信息,包括收藏夹ID、名称、内容数量:
https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid={你的UID}第二个是获取“某个收藏夹内的视频列表”。这里需要用到上一步返回的收藏夹ID,同时控制分页参数:
https://api.bilibili.com/x/v3/fav/resource/list?media_id={收藏夹ID}&pn={页码}&ps={每页数量}&platform=web关于这两个接口的细节,有几个点需要说明一下。
up_mid就是B站个人空间地址里那一串数字。比如你打开自己的空间,地址类似https://space.bilibili.com/1234567,那1234567就是你的UID。接口里的media_id是收藏夹的唯一标识,这个ID和你在网页端看到的“收藏夹ID”是两回事,必须通过第一个接口去拿,不要自己猜。
分页参数里,pn表示页码,从1开始;ps表示每页条数。B站对ps的上限控制比较严格,实测传20最稳妥,传50偶尔会被拒绝。不要觉得20太少,一个收藏夹就算有1000条,也就50次请求,配合后面的延时控制,几十秒就跑完。
2.2 Cookie获取方法:只有登录态能拿到完整数据
调用这些接口时,绝大部分字段都要求传入登录后的Cookie信息。别嫌麻烦,这一步是整个导出流程中最重要的准备工作。Cookie本质上是你的临时身份凭证,B站在服务端根据Cookie判断“这个请求来自谁”,没有这个凭证,接口只会返回“未登录”的错误。
我自己最推荐的Cookie获取方法是:用浏览器开发者工具复制。
打开Chrome或Edge,先登录B站网页版。然后按F12打开开发者工具,切到“网络(Network)”标签页,刷新一下B站任意页面。此时能在请求列表里看到大量请求,在过滤框里输入api.bilibili.com,随便点开一个请求,在“请求头(Request Headers)”里找到Cookie:这一行,后面那一长串就是你要的内容,全部复制下来。
有朋友会问,能不能在浏览器控制台里直接敲document.cookie来拿?我试过,这个方法只能拿到部分非HttpOnly字段,关键的SESSDATA字段是HttpOnly属性,JS代码根本读不到,所以还是老老实实用开发者工具复制最靠谱。
还有一点必须提醒:Cookie就是账号的钥匙,泄露给任何人就相当于把账号借出去了。建议用完之后,在B站网页端退出登录再重新登录一次,让旧的Cookie失效;或者至少在脚本运行完就立刻从代码文件里删掉Cookie,不要存到云端笔记里。
2.3 返回字段说明与数据清洗思路
请求接口后,返回的数据是一个JSON对象,结构有点层层嵌套,但核心内容都在data.medias这个数组里。每条视频的数据包含这些关键字段:
| 字段名 | 含义 | 用途 |
|---|---|---|
id | 视频BV号 | 生成视频链接 |
title | 视频标题 | 导出核心内容 |
cover | 封面图链接 | 可选字段 |
pub_date | 视频发布时间 | 判断内容时效性 |
fav_time | 收藏时间 | 查看收藏轨迹 |
upper.name | UP主名称 | 按作者分类 |
link | 视频短链接 | 直达视频页 |
这里有个细节值得注意:接口返回的fav_time和pub_date都是Unix时间戳,也就是一串秒数。直接看这串数字是看不出时间的,需要转换成人类可读格式。Python里用datetime.fromtimestamp(时间戳)一行代码就能搞定,下面的脚本里已经写好了。
另外,有些收藏内容可能是互动视频、剧集或者已经失效的视频,返回的字段会有些差异。写脚本的时候最好对title为空的情况做兜底处理,避免导出的表格里出现一堆空行。
3. 手动实操:用Python脚本把收藏夹完整导出来
3.1 环境准备与安装依赖
先交代一下环境:脚本基于Python 3.8+,理论上Windows/macOS/Linux都能跑。除了Python标准库之外,只要额外装一个requests库。
安装命令很简单:
pip install requests如果你用的是Python 3.8以上版本,其实也可以用自带的urllib来发请求,少装一个库更清爽。但requests在易用性上明显更好,代码也更简洁,我这里统一用requests演示。如果你连Python都没装,去官网下个最新稳定版,安装时记得勾选“Add Python to PATH”,否则命令行里敲不了python命令。
整个脚本不需要GUI界面,命令行走天下。考虑到不是所有人都熟悉命令行,后面的步骤我会尽量写得细一些,照着一行行敲就行。
3.2 完整脚本实现(可复制)
下面这个脚本是我实跑过的版本,做了三件事:读取你配置的Cookie和UID、获取所有收藏夹列表、遍历每个收藏夹分页拉取视频信息,最后统一输出两个文件:一个bili_favorites.csv用Excel打开,一个bili_favorites.json保留完整原始数据备用。
import requests import json import time import csv from datetime import datetime # ===== 需要手动配置的部分 ===== UID = "你的UID" COOKIE = "你的完整Cookie" # ============================= HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36", "Referer": "https://www.bilibili.com/", "Cookie": COOKIE, } def ts_to_str(ts): """把Unix时间戳转成可读时间""" if not ts: return "" return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") def get_fav_folders(): """获取当前账号下所有收藏夹""" url = f"https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid={UID}" resp = requests.get(url, headers=HEADERS, timeout=10) data = resp.json() if data.get("code") != 0: print(f"获取收藏夹列表失败:{data}") return [] folders = data.get("data", {}).get("list", []) print(f"共发现 {len(folders)} 个收藏夹") return folders def get_fav_medias(media_id, total): """分页获取某个收藏夹内的所有视频,返回完整列表""" all_medias = [] ps = 20 pn = 1 while pn * ps <= total + ps: url = ( f"https://api.bilibili.com/x/v3/fav/resource/list" f"?media_id={media_id}&pn={pn}&ps={ps}&platform=web" ) resp = requests.get(url, headers=HEADERS, timeout=10) data = resp.json() if data.get("code") != 0: print(f" 第 {pn} 页请求失败:{data}") break medias = data.get("data", {}).get("medias", []) if not medias: break all_medias.extend(medias) print(f" 已抓取 {len(all_medias)} / {total} 条") pn += 1 time.sleep(0.6) return all_medias def main(): folders = get_fav_folders() if not folders: return all_rows = [] all_media_raw = {} for folder in folders: fid = folder.get("id") fname = folder.get("title") total = folder.get("media_count", 0) print(f"\n处理收藏夹:{fname}(共 {total} 条)") if total == 0: continue medias = get_fav_medias(fid, total) all_media_raw[fname] = medias for m in medias: upper = m.get("upper") or {} row = { "收藏夹": fname, "标题": m.get("title", ""), "BV号": m.get("bvid", ""), "链接": m.get("link", ""), "UP主": upper.get("name", ""), "发布时间": ts_to_str(m.get("pub_date")), "收藏时间": ts_to_str(m.get("fav_time")), "简介": (m.get("intro") or "")[:50], } all_rows.append(row) if not all_rows: print("没有导出任何数据,请检查Cookie和UID") return # 输出CSV(UTF-8 BOM,避免Excel打开乱码) csv_file = "bili_favorites.csv" with open(csv_file, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=all_rows[0].keys()) writer.writeheader() writer.writerows(all_rows) # 输出JSON原始数据 json_file = "bili_favorites.json" with open(json_file, "w", encoding="utf-8") as f: json.dump(all_media_raw, f, ensure_ascii=False, indent=2) print(f"\n导出完成!CSV文件:{csv_file},JSON文件:{json_file},共 {len(all_rows)} 条记录") if __name__ == "__main__": main()把这个脚本保存成export_bili_fav.py,运行之前记得先做好两处配置:把UID替换成你的个人空间ID,把COOKIE替换成2.2节复制的完整Cookie字符串。别把这两个字段留空,否则只会浪费一次报错时间。
3.3 运行、验证与结果解读
配置好之后,在命令行进入脚本所在目录,执行:
python export_bili_fav.py正常情况下的输出会是这样的:
共发现 3 个收藏夹 处理收藏夹:默认收藏夹(共 428 条) 已抓取 20 / 428 条 已抓取 40 / 428 条 ... 已抓取 428 / 428 条 处理收藏夹:学习资料(共 156 条) ... 导出完成!CSV文件:bili_favorites.csv,JSON文件:bili_favorites.json,共 xxxx 条记录看到 “导出完成” 之后,用Excel打开bili_favorites.csv检查一下。如果打开发现中文乱码,别慌,这不是数据问题,是编码问题;脚本里已经用utf-8-sig格式输出,就是为了避免这个坑,如果你用的还是乱码,说明可能用了记事本打开CSV,换成WPS或Excel就好。每一行的标题、链接、UP主、收藏时间都在,基本就能确认导出的数据是完整的。
JSON文件的作用是留底,里面保存了所有原始返回字段。万一以后想扩展更多字段,不用重新跑接口,直接解析JSON文件就好。关于字段的详细含义,可以回看前文2.3节的表格。
4. 踩坑实录:高频问题与排查方法
4.1 马上能用的“常见报错速查表”
自己写代码跑数据,出问题是常态,关键是能快速定位。我把实测过程中遇到的高频问题整理成了一张速查表,按“症状-原因-解法”的路径来,看一眼就能直接上手排查。
| 问题现象 | 大概率原因 | 解决方法 |
|---|---|---|
返回code: -101 | Cookie未正确携带或已过期 | 重新复制Cookie,确认请求头里有完整的SESSDATA |
返回code: -352 | 风控拦截,请求太频繁 | 把睡眠时间从0.6提高到1-2秒;不要开多线程并发 |
| 只返回20条就停了 | 没有翻页或翻页逻辑错误 | 检查while循环条件,是否按total计算了总页数 |
| 所有字段为空 | 复制的Cookie不完整 | 用开发者工具方式复制,而不是document.cookie |
报错Invalid URL | UID或链接拼接出错 | 打印URL检查,看up_mid和media_id是否被正确替换 |
| CSV中文乱码 | 编码问题 | 确认是用utf-8-sig写入CSV,而不是普通utf-8 |
| HTTP 412 | B站认为客户端异常 | 请求头补全Referer和正常的User-Agent |
这里重点说一下-352这个问题。B站的风控策略是动态的,短时间内请求频率过高就容易触发。我最初跑的时候没有加延时,结果在第3个收藏夹就触发了风控,整个脚本直接卡死。后来改成每次请求之间至少停0.6秒,情况才好转。如果收藏夹特别多,建议把延时调到1秒以上,安全优先。
另外,-101这个报错几乎90%是因为Cookie复制不全。Cookie字符串很长,复制的时候容易漏掉末尾的字符。粘贴后最好也检查一下前后有没有多余的空格,空格虽然不会导致100%报错,但有时候就是这种小细节让人抓狂。
4.2 实用扩展:从“导出数据”到“使用数据”
脚本跑通之后,导出只是起点,怎么用这批数据才体现价值。我这段时间用导出的数据做了几件事,感觉受益挺大的。
第一件事,筛出“已失效”的视频。把CSV里的链接批量用脚本检测一遍,凡是返回404或者在网页端提示“视频不可用”的单独列一张表,决定是取消收藏还是找替代资源。B站的视频有时候只是因为版权原因被限制,过段时间又会恢复,这时候别急着把本地记录删掉,先保留着。
第二件事,按“收藏时间”做趋势分析。用CSV里的收藏时间字段,很容易看到自己在哪些时间段集中收藏了一堆视频,反而暴露出“收藏从未停止,学习从未开始”的囤积习惯。我现在每季度会跑一次导出,对比本季度的收藏趋势,决定下一阶段的学习重心。
第三件事,把导出的JSON转成适用于其他工具的格式。比如导入Obsidian做个人知识库,或者写个简单的转存脚本把“学习资料”收藏夹里的视频信息同步成一份结构化清单。有了完整的数据字段,这些都是顺手的事,关键是你不再被平台限制住了。
最后还有一个小建议:养成定期导出的习惯。不用太频繁,一个月一次就够。B站不会通知你哪条视频即将消失,但你的本地表格里会清清楚楚记录下每条内容曾经存在过。这一点,我深有体会。