B站收藏视频统计:从API逆向到分页遍历的完整脚本开发指南 1. 项目缘起一个看似简单却藏着“坑”的需求那天在整理B站收藏夹看着那几千个视频突然冒出一个念头我到底收藏了多少个视频这个数字背后可能藏着我的兴趣变迁或者只是单纯的“松鼠病”发作。点开收藏夹B站默认只显示最近的一些想看到总数要么手动一页页翻要么就得想点别的办法。作为一个有点技术底子的用户我的第一反应不是去问客服而是能不能写个小工具自己算一下这就是“bilibili001”这个网页脚本诞生的起点。它的核心目标极其单纯自动统计你在Bilibili上收藏的视频总数。听起来是不是很简单无非就是打开网页模拟请求解析数据然后做个累加。但实际操作起来你会发现从登录认证、接口分析、分页处理到错误应对每一步都可能遇到意想不到的“惊喜”。尤其是当你面对一个大型平台的API时那些没有公开文档的细节、随时可能变动的参数、以及各种返回码背后的含义才是真正考验开发者功力的地方。这个项目非常适合前端初学者或者对网络爬虫、API调用感兴趣的朋友练手。它不涉及复杂的业务逻辑但完整覆盖了一个小型数据采集工具的核心流程环境准备、请求构造、数据处理和结果展示。通过完成它你不仅能学会如何使用JavaScript与一个真实的、复杂的Web API进行交互更能深刻理解在实际开发中如何处理那些官方文档里不会写的“潜规则”和边界情况。接下来我就带你一步步拆解这个需求并分享我在实现过程中踩过的坑和总结的经验。2. 环境与工具准备从零搭建一个安全的脚本环境在动手写代码之前搭建一个合适且安全的工作环境至关重要。我们不是在写一个需要部署的后端服务而是一个在浏览器环境下运行的“用户脚本”。这意味着我们的代码将直接在你的浏览器中执行并代表你与B站服务器进行交互。因此工具的选择不仅要考虑开发效率更要考虑安全性和便利性。2.1 核心工具用户脚本管理器直接修改网页源代码或者频繁在浏览器控制台里粘贴代码是低效且不持久的。我们需要一个工具来管理、注入和持久化我们的脚本。这里首推Tampermonkey俗称“油猴”或Violentmonkey。它们是浏览器扩展允许你安装和管理用户脚本UserScript。这些脚本可以在特定的网站加载时自动运行完美契合我们的需求。为什么选它们隔离与安全脚本在沙盒环境中运行与原始网页环境有一定隔离降低了因脚本错误导致网页崩溃的风险。持久化与管理一次编写永久生效。你可以方便地启用、禁用、编辑脚本无需每次手动操作。社区与生态有庞大的脚本库方便学习借鉴。安装步骤以Chrome浏览器和Tampermonkey为例打开Chrome网上应用店。搜索“Tampermonkey”。点击“添加到Chrome”进行安装。 安装完成后浏览器工具栏会出现Tampermonkey的图标点击它可以管理你的脚本。2.2 开发与调试环境浏览器开发者工具现代浏览器的开发者工具F12打开是我们最重要的开发伙伴。我们将频繁使用以下几个面板Console控制台运行JavaScript代码片段、查看日志输出和错误信息。这是我们测试API请求、查看返回数据的“主战场”。Network网络监控浏览器发出的所有网络请求。这是逆向分析B站API的钥匙。你可以在这里看到收藏夹页面加载时具体请求了哪些接口XHR/Fetch类型它们的URL、请求头、参数和响应内容。Sources源代码可以在这里直接编辑和调试Tampermonkey加载的脚本设置断点单步跟踪代码执行。Application应用查看和操作本地存储、Cookie等信息。B站的登录态SESSDATA等就存放在这里我们的脚本需要能读取到这些信息。2.3 脚本的基本骨架与元信息在Tampermonkey中新建一个脚本它会生成一个模板。我们需要修改顶部的元信息块UserScript。这部分定义了脚本的基本属性。// UserScript // name bilibili001 - 收藏视频统计器 // namespace http://tampermonkey.net/ // version 0.1 // description 自动计算Bilibili账号收藏的视频总数并显示在页面上。 // author You // match https://space.bilibili.com/*/favlist* // match https://www.bilibili.com/medialist/* // grant GM_xmlhttpRequest // connect api.bilibili.com // /UserScript关键元信息解析name和description脚本的名称和描述清晰明了即可。match指定脚本在哪些网址下运行。这里我们匹配了B站个人空间收藏夹页面和新版播单页面的URL模式。使用通配符*来匹配用户的UID。grant和connect这是安全与权限控制的核心。grant GM_xmlhttpRequest这是Tampermonkey提供的一个强大API用于发起跨域HTTP请求。与浏览器原生的fetch或XMLHttpRequest相比它更强大、更安全能更好地处理Cookie、请求头并且不受CORS跨域资源共享政策的严格限制。这对于调用api.bilibili.com下的接口至关重要。connect api.bilibili.com明确告知Tampermonkey脚本需要连接到此域名。这是一种安全声明让用户和管理器知道脚本的意图在某些严格模式下是必须的。注意强烈建议使用GM_xmlhttpRequest而非原生fetch。因为B站的接口通常需要携带登录Cookie而fetch在默认情况下发送请求时可能不会自动包含这些Cookie取决于credentials选项且更容易受到CORS策略的拦截。GM_xmlhttpRequest专为脚本设计能无缝集成浏览器的当前会话状态。3. 逆向工程寻找并理解收藏夹API这是整个项目最具挑战性也最有趣的部分。B站没有公开提供“获取收藏夹所有视频列表”的官方API文档我们需要自己从网页行为中把它“挖”出来。3.1 观察网页行为定位关键请求登录B站进入“我的收藏”页面https://space.bilibili.com/{你的UID}/favlist。打开浏览器开发者工具的Network网络面板并勾选上“Preserve log”保留日志。滚动收藏夹列表的滚动条触发加载更多。在网络请求列表中你会看到大量请求。我们需要筛选出那个获取收藏视频列表的请求。通常它具备以下特征类型 可能是XHR或Fetch。URL 域名很可能包含api.bilibili.com路径可能与fav、list相关。响应 预览Preview或响应Response内容是一段JSON里面包含archives视频档案数组和page分页信息。经过一番查找我找到了核心接口。这里以我分析时的一个接口为例请注意接口路径和参数可能随时间变化但分析方法通用GET https://api.bilibili.com/x/v3/fav/resource/list?media_idxxxpn1ps20keywordordermtimetype0tid0platformwebjsonpjsonp3.2 深度解析请求参数与响应结构让我们拆解这个请求请求参数 (Query Parameters):media_id:收藏夹的ID。这是最关键参数。每个收藏夹包括“默认收藏夹”和用户自建的都有一个唯一的media_id。你可以在收藏夹页面的URL中找到它例如favlist?fid12345中的fid有时就对应media_id但更可靠的方法是从页面初始化时的某个API响应里获取。pn:页码Page Number从1开始。ps:每页大小Page Size即每页返回多少条视频记录。B站通常默认为20最大可能支持到100但可能受限。ordermtime: 按修改时间排序。platformweb: 平台标识。其他参数如keyword搜索关键词、tid分区ID、type等用于筛选统计总数时通常留空或默认。响应体 (Response Body) 结构分析响应是一个JSON对象结构大致如下{ code: 0, message: 0, ttl: 1, data: { medias: [ // 注意可能是 medias 也可能是 archives不同接口命名不同 { id: 视频ID, type: 2, // 类型2通常代表视频 title: 视频标题, cover: 封面图URL, // ... 其他视频信息 }, // ... 更多视频 ], info: { id: 收藏夹ID, title: 收藏夹名称, media_count: 118 // **重点关注这个字段可能就是当前收藏夹的视频总数** }, has_more: true // 是否还有更多页 } }一个至关重要的发现在data.info对象里存在一个media_count字段。如果这个字段准确反映了收藏夹内的视频总数那我们岂不是不用遍历所有分页了先别急这里有一个大坑。3.3 关键陷阱media_count字段的不可靠性在实际测试中我发现media_count字段并不总是等于当前筛选条件下尤其是使用了keyword或tid筛选时的实际视频数量。它很可能表示的是该收藏夹内所有类型资源的总数或者是一个缓存值。当你使用关键词搜索收藏夹内容时medias数组的数量会变化但media_count可能不变。结论为了获得精确的、符合当前筛选条件的视频数量最可靠的方法仍然是遍历所有分页累加每一页返回的有效视频条目数。media_count可以作为一个参考或者用于估算、显示总上限但不能作为精确统计的唯一依据。我们的脚本逻辑必须建立在分页遍历之上。4. 核心实现分页遍历与精确计数逻辑基于以上的分析我们可以开始设计脚本的核心逻辑了。整个流程可以分为几个步骤获取必要参数、循环请求每一页数据、解析并计数、处理结束条件。4.1 第一步获取当前收藏夹的media_id脚本需要知道当前在看哪个收藏夹。最直接的方式是从页面URL或DOM中提取。(function() { use strict; // 等待页面主要内容加载完成 window.addEventListener(load, function() { // 尝试从URL中获取 fid (可能对应 media_id) const urlParams new URLSearchParams(window.location.search); let mediaId urlParams.get(fid); // 如果URL中没有尝试从页面某个隐藏元素或初始数据中获取需要进一步分析页面结构 // 这里是一个示例实际选择器需要根据B站当前页面HTML调整 if (!mediaId) { const initDataScript document.querySelector(script[typeapplication/json]); if (initDataScript) { try { const initData JSON.parse(initDataScript.textContent); mediaId initData?.data?.info?.id; // 这是一个可能的路径 } catch (e) { console.error(解析页面初始数据失败:, e); } } } if (mediaId) { console.log(获取到收藏夹ID: ${mediaId}); startCounting(mediaId); } else { console.warn(无法自动获取收藏夹ID请手动确认所在页面。); // 可以提供一个输入框让用户手动输入 media_id } }); // 核心统计函数 async function startCounting(mediaId) { // 后续代码将写在这里 } })();实操心得B站的页面结构可能改版导致选择器失效。更稳健的方法是监听网络请求直接从第一个发出的list接口请求中捕获media_id。这可以通过Tampermonkey的GM_xmlhttpRequest拦截或重写fetch/XMLHttpRequest来实现但复杂度较高。对于初级脚本从URL或已知的DOM节点获取是更简单的起点。4.2 第二步实现可靠的分页请求函数我们将使用GM_xmlhttpRequest来发起请求。它支持Promise封装便于我们使用async/await进行异步控制。function fetchFavListPage(mediaId, pageNum, pageSize 20) { return new Promise((resolve, reject) { const apiUrl https://api.bilibili.com/x/v3/fav/resource/list?media_id${mediaId}pn${pageNum}ps${pageSize}ordermtimeplatformwebjsonpjsonp; GM_xmlhttpRequest({ method: GET, url: apiUrl, headers: { // 可以添加一些通用头但注意不要覆盖关键头如 Cookie User-Agent: navigator.userAgent, }, onload: function(response) { if (response.status 200 response.status 300) { try { const result JSON.parse(response.responseText); if (result.code 0) { resolve(result.data); // 成功返回 data 部分 } else { reject(new Error(API错误: code${result.code}, message${result.message})); } } catch (e) { reject(new Error(解析JSON响应失败: e.message)); } } else { reject(new Error(HTTP请求失败: status${response.status})); } }, onerror: function(error) { reject(new Error(网络请求错误: ${error})); }, // 非常重要携带当前站点的Cookie以维持登录态 anonymous: false, }); }); }关键点说明anonymous: false这个配置项确保了请求会携带当前B站域下的Cookie包括SESSDATA这样API才能识别出已登录的用户返回该用户的收藏数据。如果设为true请求将是匿名的会得到未登录或无权限的错误。错误处理我们检查了HTTP状态码和API返回的code字段。B站接口通常code: 0表示成功非零表示各种错误如未登录、收藏夹不存在、参数错误等。jsonpjsonp参数这是一个历史遗留参数用于兼容旧的JSONP调用方式对于现代XHR/Fetch请求保留它通常无害。4.3 第三步循环遍历与计数逻辑现在我们可以实现startCounting函数了。逻辑是从第1页开始请求累加每页的medias数组长度直到has_more为false或medias为空。async function startCounting(mediaId) { const pageSize 100; // 尝试每页拉取最大值减少请求次数 let currentPage 1; let totalCount 0; let hasError false; const startTime Date.now(); // 在页面上创建一个UI元素来显示进度和结果 const resultDiv createResultUI(); try { while (true) { updateStatus(resultDiv, 正在请求第 ${currentPage} 页...); const pageData await fetchFavListPage(mediaId, currentPage, pageSize); const medias pageData.medias || pageData.archives || []; const pageCount medias.length; totalCount pageCount; updateStatus(resultDiv, 第 ${currentPage} 页获取到 ${pageCount} 个视频当前累计: ${totalCount}); // 判断是否还有下一页 if (!pageData.has_more || pageCount 0) { updateStatus(resultDiv, 遍历完成); break; } currentPage; // 礼貌性延迟避免请求过快给服务器造成压力或触发反爬 await delay(500); } const elapsedTime ((Date.now() - startTime) / 1000).toFixed(2); showFinalResult(resultDiv, totalCount, elapsedTime); } catch (error) { hasError true; console.error(统计过程中发生错误:, error); updateStatus(resultDiv, span stylecolor: red;错误: ${error.message}/span, true); } } // 简单的延迟函数 function delay(ms) { return new Promise(resolve setTimeout(resolve, ms)); } // 创建结果显示UI的函数 function createResultUI() { const div document.createElement(div); div.style.cssText position: fixed; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.95); border: 2px solid #00a1d6; border-radius: 8px; padding: 15px; max-width: 300px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 9999; font-family: sans-serif; ; div.innerHTML h4 stylemargin-top:0; color: #00a1d6;收藏视频统计器/h4p正在初始化.../p; document.body.appendChild(div); return div; } function updateStatus(container, message, isHtml false) { const p container.querySelector(p:last-of-type) || container.appendChild(document.createElement(p)); if (isHtml) { p.innerHTML message; } else { p.textContent message; } } function showFinalResult(container, total, time) { const html pstrong统计完成/strong/p p收藏视频总数span stylefont-size: 1.5em; color: #f25d8e;${total}/span/p p耗时${time} 秒/p psmall此统计基于分页遍历结果精确/small/p button idcloseStatsBtn stylemargin-top:10px; padding:5px 10px;关闭/button ; container.innerHTML html; container.querySelector(#closeStatsBtn).addEventListener(click, () container.remove()); }4.4 边界情况与健壮性处理一个健壮的程序必须考虑边界情况空收藏夹第一页的medias数组就为空has_more为false。我们的逻辑能正确处理pageCount 0。API限流或错误在fetchFavListPage函数中我们已经做了基础的错误处理reject。在startCounting的catch块中我们捕获了错误并显示给用户。更进阶的做法可以实现指数退避重试机制。网络中断GM_xmlhttpRequest的onerror会捕获网络层面的错误。登录态失效如果Cookie失效API会返回特定的code例如-101表示未登录。我们的代码会统一显示“API错误”。可以进一步解析message提示用户“登录已过期请刷新页面”。请求频率控制在循环中加入了delay(500)这是一个简单的频率控制。对于大量收藏的用户遍历所有页面可能需要几十秒甚至几分钟适当的延迟是必要的网络礼仪也能降低被临时限制的风险。5. 进阶优化与功能扩展基础功能实现后我们可以让这个脚本变得更强大、更好用。5.1 性能优化并发请求与进度预估对于收藏了成千上万个视频的用户逐页顺序请求耗时很长。我们可以尝试有限并发来提速。async function fetchAllPagesConcurrently(mediaId, totalPages, pageSize, concurrency 3) { const pagePromises []; const results new Array(totalPages).fill(null); let completed 0; // 创建一个工作池 const worker async (pageNum) { try { const data await fetchFavListPage(mediaId, pageNum, pageSize); results[pageNum - 1] data; // 存入数组对应位置 } catch (error) { results[pageNum - 1] { error }; // 存储错误 } finally { completed; updateConcurrentProgress(completed, totalPages); } }; // 初始化先并发发起前 concurrency 个请求 const initialPages Math.min(concurrency, totalPages); for (let i 1; i initialPages; i) { pagePromises.push(worker(i)); } // 动态添加后续请求 let nextPage concurrency 1; const addNextPage () { if (nextPage totalPages) { pagePromises.push(worker(nextPage)); nextPage; // 当一个请求完成时尝试添加下一个 Promise.race(pagePromises.map((p, idx) p.then(() idx))).then(() addNextPage()); } }; // 启动动态添加流程简化版实际需更严谨的队列控制 // 更稳妥的做法是使用一个固定的并发队列库或自己实现一个简单的任务队列。 await Promise.allSettled(pagePromises); return results; // 返回包含所有页面数据或错误的数组 }注意并发请求虽然快但风险极高。极易触发服务器的反爬机制导致IP或账号被临时限制。对于B站这类大型平台强烈建议不要使用高并发顺序请求加延迟是最稳妥的方式。上述代码仅作为技术思路展示。5.2 数据持久化与可视化我们可以将每次统计的结果保存下来使用GM_setValue并绘制一个简单的收藏数量增长趋势图使用canvas或引入轻量图表库如Chart.js。// 保存历史记录 function saveHistoryRecord(totalCount) { const history JSON.parse(GM_getValue(fav_history, [])); history.push({ date: new Date().toISOString(), count: totalCount }); // 只保留最近30次记录 if (history.length 30) history.shift(); GM_setValue(fav_history, JSON.stringify(history)); } // 在 showFinalResult 中调用 showFinalResult(container, total, time); saveHistoryRecord(total);然后可以新增一个按钮点击后显示一个弹窗展示历史折线图。5.3 处理多收藏夹与分类统计真正的需求可能更复杂用户有多个收藏夹想统计所有收藏夹的总和或者想按收藏夹分类统计。获取收藏夹列表首先需要调用另一个API如https://api.bilibili.com/x/v3/fav/folder/created/list?up_mid{UID}来获取用户创建的所有收藏夹列表及其id即media_id。遍历每个收藏夹对列表中的每一个media_id执行我们上面编写的startCounting逻辑。汇总与展示将每个收藏夹的统计结果以表格形式展示并计算总和。这会将脚本的复杂度提升一个等级但结构是清晰的从一个“单任务循环”升级为一个“主任务获取列表 N个子任务统计每个收藏夹”。6. 实战中遇到的“坑”与解决方案在开发和测试这个脚本的过程中我遇到了几个典型问题这里分享出来希望能帮你避开。6.1 接口变更与参数失效这是最大的风险。B站后端接口并非一成不变路径、参数名、响应格式都可能调整。现象脚本某天突然不工作了GM_xmlhttpRequest返回错误或者解析数据时找不到预期的字段。排查打开Network面板手动操作页面滚动加载找到最新的、有效的收藏列表请求。对比脚本中使用的URL和参数与最新请求的差异。检查响应JSON的结构是否变化。解决更新脚本中的API URL和参数解析逻辑。这也是为什么在代码中把API URL集中定义在一个函数里的原因便于维护。预防在脚本开头或注释中注明当前适配的接口地址和日期。考虑加入一个简单的版本检测或错误上报机制需谨慎处理用户隐私。6.2GM_xmlhttpRequest的兼容性与异步陷阱GM_xmlhttpRequest是回调风格的API我们用Promise包装它时要注意错误处理的传递。坑在onerror或onload中如果直接throw error无法被外部的try...catch捕获。必须通过reject将错误传递出去。解决正如我们在fetchFavListPage函数中所做确保所有错误路径网络错误、HTTP错误、API业务错误、JSON解析错误都最终调用reject。6.3 登录态Cookie问题脚本运行在用户的浏览器环境中理论上可以访问到当前页面的Cookie。但有时会出现权限问题。现象脚本发出的请求返回code: -101未登录或code: -403权限不足。检查确认GM_xmlhttpRequest的anonymous选项设为false。在Console中尝试用document.cookie查看是否能获取到SESSDATA等关键Cookie注意安全不要泄露。检查脚本的match规则是否正确是否运行在正确的域名下bilibili.com。解决确保用户已登录B站并且脚本在B站域名下执行。如果问题持续可以尝试在请求头中手动设置Cookie但注意隐私和安全不推荐在公开脚本中硬编码。6.4 反爬机制与请求频率即使我们是模拟正常用户行为过于频繁的请求也可能被暂时限制。现象请求开始返回非200状态码如429 Too Many Requests或者code为一些限流相关的错误。解决增加延迟这是最有效的方法。我在循环中加入了500毫秒的延迟对于几百个视频的收藏夹耗时在可接受范围内。如果视频数量极大可以适当增加延迟如1秒。添加随机延迟将固定延迟改为一个随机范围如300ms ~ 1000ms使请求模式更接近真人操作。优雅降级如果检测到限流错误自动暂停一段时间如30秒后再重试或提示用户稍后再试。7. 完整代码整合与使用指南将上述所有模块整合并添加更完善的错误处理和用户交互就得到了一个相对健壮的脚本。由于篇幅限制这里不贴出全部代码但核心结构如下元信息块定义脚本名称、匹配规则、权限。主函数入口在页面加载后执行尝试获取media_id。核心统计函数 (startCounting)包含分页循环、UI更新、错误处理。请求函数 (fetchFavListPage)封装GM_xmlhttpRequest。UI工具函数创建、更新状态面板。(可选) 工具函数如delay,saveHistoryRecord等。使用指南安装Tampermonkey扩展。新建脚本将完整代码粘贴进去。保存脚本CtrlS确保其处于启用状态。打开B站并登录进入任意一个收藏夹页面。脚本会自动运行或在页面角落出现一个统计按钮取决于UI设计。稍等片刻即可看到统计结果悬浮在页面一角。这个项目从一个小想法出发贯穿了前端脚本开发、API逆向、异步编程、错误处理和基础UI交互的多个环节。它没有用到高深的算法但非常贴近实际开发场景。当你看到那个最终的数字出现在屏幕上时那种通过自己写的代码解决实际问题的成就感正是编程乐趣的来源之一。希望这个详细的拆解过程不仅能帮你实现这个统计功能更能让你掌握一套分析问题、解决问题的通用方法。