B站收藏夹视频统计脚本开发指南:从API调用到油猴脚本实现

B站收藏夹视频统计脚本开发指南:从API调用到油猴脚本实现 1. 项目概述为什么我们需要一个B站收藏夹统计脚本作为一名重度B站用户我的收藏夹早就变成了一个“数字黑洞”。从编程教程到美食探店从游戏攻略到纪录片看到感兴趣的视频就习惯性地点下那个五角星。久而久之收藏夹里塞满了上千个视频但真正回看过多少我自己心里也没数。这种“收藏即学会”的错觉让我萌生了一个想法我得知道自己到底“囤”了多少货。手动去数显然不现实。B站网页版虽然提供了收藏夹管理功能能看到视频列表但并没有一个直接的统计数字告诉你总数。尤其是当收藏夹分门别类或者视频数量庞大时靠肉眼滑动滚动条来估算既不准又累人。这就是“bilibili001计算自己收藏了多少视频”这个网页脚本诞生的最直接动机——用一个自动化工具快速、准确地回答一个简单的问题“我的收藏夹里到底有多少个视频”更深层次的需求其实是对个人数字资产的一次“盘点”。通过这个脚本我们不仅能得到一个数字更能为后续的数据分析打下基础。比如你可以分析自己收藏视频的类型分布知识区占比多少生活区占比多少或者统计一下你关注的UP主谁的作品被你收藏得最多。这些数据能帮你更清晰地了解自己的兴趣偏好甚至成为你内容消费习惯的一份“体检报告”。这个项目本质上是一个面向特定网站Bilibili的浏览器用户脚本。它不依赖任何复杂的后端服务仅仅通过运行在浏览器中的JavaScript代码调用B站官方提供的网页接口API模拟用户操作来获取数据并在页面上呈现结果。技术栈非常纯粹HTML/CSS/JavaScript再加上对浏览器开发者工具的熟练运用。对于前端开发者或编程爱好者来说这是一个绝佳的练手项目涉及网络请求、数据解析、DOM操作等核心技能。2. 核心思路与技术选型解析要实现自动统计核心思路就是“让程序代替人工去翻页和计数”。B站收藏夹页面是动态加载的往下滚动会不断加载新视频这背后是前端通过Ajax技术向服务器请求数据。我们的脚本需要模拟这个过程。2.1 逆向工程找到数据源头首先我们需要找到B站页面加载收藏视频数据的真实接口。这里不会涉及任何破解或违规操作仅仅是使用浏览器自带的“开发者工具”按F12打开来观察网络请求。打开收藏夹页面登录B站进入“我的收藏”页面。开启网络监控在开发者工具中切换到“Network”网络标签页并勾选“Fetch/XHR”来过滤出Ajax请求。触发数据加载在页面上向下滚动触发加载更多视频。分析请求观察网络面板中新增的请求。你会很快发现一个格式类似的请求例如https://api.bilibili.com/x/v3/fav/resource/list?media_idxxxpn1ps20。这个media_id就是你的收藏夹IDpn是页码page numberps是每页大小page size。通过查看这个请求的“Response”响应我们能得到结构化的JSON数据里面包含了当前页的视频列表、总数等信息。这就是我们脚本的数据来源。注意B站的接口可能会更新路径或参数名可能发生变化。编写脚本时不能写死而应该通过分析当前页面的请求动态获取接口URL和关键参数如media_id这样脚本的健壮性会更强。2.2 技术方案对比油猴脚本 vs 浏览器插件获取到接口后我们需要一个载体来运行我们的JavaScript代码。主要有两种主流方案用户脚本UserScript通常通过“Tampermonkey”油猴或“Violentmonkey”这类浏览器扩展来管理。脚本可以直接注入到指定的网页中运行。优点开发部署极其简单一个.user.js文件搞定更新方便用户一键安装专注于页面功能增强与浏览器本身耦合度低。缺点功能受限于浏览器API和页面环境权限控制相对严格。浏览器扩展Extension如Chrome Extension、Firefox Add-on。优点功能强大可以访问更多浏览器API如书签、历史记录可以有自己的弹出页面Popup和独立后台。缺点开发复杂度高需要处理manifest文件、多文件结构发布需要上架商店审核流程更长。对于“统计收藏视频数”这个单一、具体的需求用户脚本是更轻量、更快捷的选择。我们不需要一个常驻的扩展图标或后台页面只需要在访问B站收藏夹时自动执行统计逻辑并展示结果即可。油猴脚本完美契合这个场景。2.3 核心逻辑设计脚本的核心工作流程可以分解为以下几步环境判断仅在B站收藏夹页面https://www.bilibili.com/medialist/detail/*或https://space.bilibili.com/*/favlist触发。定位接口通过监听网络请求或分析页面现有元素找到获取收藏列表的API地址和必要的参数如media_id,pn,ps。循环请求从第一页pn1开始循环调用该接口直到返回的列表为空或达到已知总数。数据累加在每次请求成功后解析JSON响应累加当前页的视频数量。结果展示在页面的合适位置例如收藏夹标题附近插入一个显示框动态更新并最终展示总数量。3. 脚本实现细节与关键代码剖析下面我们以Tampermonkey用户脚本的形式一步步实现这个功能。我们将采用一种更稳健的方式不直接硬编码接口URL而是从页面已有的元素或初始请求中提取关键信息。3.1 脚本元信息与框架首先创建一个新的用户脚本头部是Tampermonkey必需的元信息块。// UserScript // name Bilibili收藏夹视频统计器 // namespace http://tampermonkey.net/ // version 1.0 // description 自动计算当前B站收藏夹内的视频总数 // author You // match https://www.bilibili.com/medialist/detail/* // match https://space.bilibili.com/*/favlist* // grant GM_xmlhttpRequest // connect api.bilibili.com // /UserScript (function() { use strict; // 你的代码将写在这里 })();match: 指定脚本生效的页面URL模式覆盖了两种常见的收藏夹页面格式。grant GM_xmlhttpRequest: 声明需要使用油猴提供的跨域请求API这比原生的fetch或XMLHttpRequest在用户脚本环境中更可靠、权限更高。connect api.bilibili.com: 声明需要连接的域名这是使用GM_xmlhttpRequest所必需的。3.2 获取收藏夹ID (media_id)收藏夹ID是请求接口的核心参数。我们可以从当前页面的URL或DOM中提取。方法一从URL提取适用于/medialist/detail/格式这种格式的URL类似https://www.bilibili.com/medialist/detail/ml123456789其中ml123456789就是media_id。function getMediaIdFromUrl() { const pathMatch window.location.pathname.match(/\/medialist\/detail\/(ml\d)/); if (pathMatch pathMatch[1]) { return pathMatch[1]; } return null; }方法二从页面元素属性提取更通用观察收藏夹页面HTML经常可以在某个根元素如#page-favlist上找到>function getMediaIdFromPage() { // 尝试多种可能的选择器 const selectors [ [data-media-id], #page-favlist[data-media-id], .fav-list-container[data-media-id] ]; for (const selector of selectors) { const el document.querySelector(selector); if (el el.dataset.mediaId) { return el.dataset.mediaId; } } return null; }在脚本主逻辑中我们可以优先尝试方法二如果失败再尝试方法一并做好错误处理。3.3 构建并发送API请求获得media_id后我们就可以构建请求。B站这个接口需要携带Cookie来标识用户身份GM_xmlhttpRequest会自动处理。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}; GM_xmlhttpRequest({ method: GET, url: apiUrl, headers: { // 可以添加一些通用头模拟浏览器行为 Accept: application/json, text/plain, */*, Origin: https://www.bilibili.com, Referer: window.location.href, }, onload: function(response) { if (response.status 200 response.status 300) { try { const data JSON.parse(response.responseText); resolve(data); } catch (e) { reject(new Error(解析JSON响应失败: e.message)); } } else { reject(new Error(API请求失败状态码: ${response.status})); } }, onerror: function(error) { reject(new Error(网络请求错误: error.statusText)); }, timeout: 10000 // 10秒超时 }); }); }实操心得使用Promise封装异步请求可以让后续的循环调用逻辑用async/await来写代码更清晰避免“回调地狱”。设置合理的timeout超时和错误处理是生产级脚本的必备项。3.4 循环请求与总数统计这是脚本的核心逻辑。我们需要循环请求每一页直到没有更多数据。接口返回的数据中通常会有data.medias当前页视频列表和data.info.media_count收藏夹总视频数字段。注意不能完全依赖media_count因为有些情况下如大量已失效视频这个总数可能不准确或者接口可能不返回这个字段。最可靠的方式是循环请求直到medias数组为空。async function countTotalVideos(mediaId) { let totalCount 0; let currentPage 1; const pageSize 20; // 每页数量最大可能为20 let hasMore true; // 创建并插入一个临时提示元素到页面 const indicator createProgressIndicator(); while (hasMore) { try { updateProgressIndicator(indicator, 正在统计第 ${currentPage} 页...); const result await fetchFavListPage(mediaId, currentPage, pageSize); if (result.code ! 0) { throw new Error(API返回错误: ${result.message} (code: ${result.code})); } const medias result.data?.medias; if (!Array.isArray(medias)) { throw new Error(响应数据结构异常未找到视频列表); } const pageCount medias.length; totalCount pageCount; // 判断是否还有下一页如果当前页返回的数量小于pageSize或者返回的列表为空则认为没有更多了 if (pageCount pageSize || pageCount 0) { hasMore false; } else { currentPage; // 礼貌性延迟避免请求过快给服务器造成压力 await delay(300); } } catch (error) { updateProgressIndicator(indicator, 统计出错: ${error.message}, true); console.error(统计收藏视频时发生错误:, error); hasMore false; // 出错时终止循环 // 可以选择将已统计的部分结果展示出来 } } updateProgressIndicator(indicator, 统计完成共收藏 ${totalCount} 个视频。, false, totalCount); return totalCount; } // 一个简单的延迟函数 function delay(ms) { return new Promise(resolve setTimeout(resolve, ms)); }3.5 创建用户界面与进度展示良好的用户体验需要反馈。我们可以在收藏夹标题附近添加一个不起眼但信息明确的指示器。function createProgressIndicator() { const container document.createElement(div); container.id bili-fav-counter-indicator; container.style.cssText position: fixed; top: 70px; right: 20px; background: rgba(0, 0, 0, 0.8); color: #fff; padding: 10px 15px; border-radius: 6px; font-size: 14px; z-index: 9999; box-shadow: 0 2px 8px rgba(0,0,0,0.2); max-width: 300px; transition: opacity 0.3s; ; const textSpan document.createElement(span); container.appendChild(textSpan); document.body.appendChild(container); return { container, textSpan }; } function updateProgressIndicator(indicator, message, isError false, finalCount null) { const { container, textSpan } indicator; textSpan.textContent message; if (isError) { container.style.backgroundColor rgba(255, 50, 50, 0.9); } else if (finalCount ! null) { container.style.backgroundColor rgba(0, 150, 0, 0.9); // 5秒后淡出消失 setTimeout(() { container.style.opacity 0; setTimeout(() container.remove(), 300); }, 5000); } }3.6 脚本入口与触发最后我们需要在页面加载完成后自动触发统计流程或者提供一个手动触发的按钮。async function main() { // 等待页面基本元素加载 await waitForPageReady(); const mediaId getMediaIdFromPage() || getMediaIdFromUrl(); if (!mediaId) { console.warn(未能在当前页面找到收藏夹ID脚本终止。); // 可以尝试在页面上添加一个手动输入ID的按钮 return; } console.log(开始统计收藏夹 ${mediaId} ...); try { const total await countTotalVideos(mediaId); console.log(统计结束总数: ${total}); } catch (error) { console.error(主流程错误:, error); } } // 简单的页面就绪判断 function waitForPageReady() { return new Promise(resolve { if (document.readyState complete) { resolve(); } else { window.addEventListener(load, resolve); } }); } // 启动脚本可以设置一个短延时确保DOM完全加载 setTimeout(main, 1000);4. 进阶优化与功能扩展基础功能实现后我们可以让这个脚本变得更强大、更实用。4.1 性能优化并发请求与速率限制逐页请求在收藏视频很多时比如几千个会显得较慢。我们可以尝试有限度的并发请求来提速但必须非常谨慎避免对B站服务器造成过大压力否则可能导致IP被临时限制。async function countTotalVideosConcurrently(mediaId, concurrentLimit 3) { // 1. 先快速请求第一页获取总页数估算如果接口返回 const firstPage await fetchFavListPage(mediaId, 1, 20); const totalMedia firstPage.data?.info?.media_count; let totalPages totalMedia ? Math.ceil(totalMedia / 20) : null; // 如果不返回总数则退化为顺序请求探测 if (!totalPages) { return await countTotalVideos(mediaId); // 调用之前的顺序版本 } // 2. 创建所有页面的请求Promise数组 const pagePromises []; for (let pn 2; pn totalPages; pn) { // 第一页已获取 pagePromises.push(fetchFavListPage(mediaId, pn, 20)); // 控制并发每发起concurrentLimit个请求就等待一个最短间隔 if (pagePromises.length % concurrentLimit 0) { await delay(500); // 500毫秒间隔 } } // 3. 并发执行所有请求 const results await Promise.allSettled(pagePromises); let totalCount firstPage.data.medias.length; // 第一页的数量 // 4. 汇总结果 for (const result of results) { if (result.status fulfilled result.value.code 0) { totalCount result.value.data.medias.length; } else { console.warn(某个页面请求失败:, result.reason || result.value); } } return totalCount; }重要警告并发请求务必设置较低的并发数如2-3和请求间隔并做好错误处理。滥用并发可能导致你的请求被B站风控系统拦截返回403或429错误。对于个人使用顺序请求加短延迟通常是更稳妥、更礼貌的方式。4.2 数据持久化与变化追踪统计一次总数后你可能还想知道收藏夹的增长情况。这需要将结果存储下来。function saveCountToStorage(mediaId, count, date new Date()) { const key bili_fav_count_${mediaId}; const history JSON.parse(localStorage.getItem(key) || []); history.push({ date: date.toISOString().split(T)[0], count }); // 只保留最近30次记录 if (history.length 30) history.shift(); localStorage.setItem(key, JSON.stringify(history)); } function loadCountHistory(mediaId) { const key bili_fav_count_${mediaId}; return JSON.parse(localStorage.getItem(key) || []); }结合这些数据你可以在页面上绘制一个简单的折线图使用canvas或引入轻量图表库如Chart.js直观展示收藏夹随时间的增长趋势。4.3 丰富统计维度除了总数我们可以解析每个视频的详细信息提供更多维度的统计分区统计分析data.medias[i].tname视频分区名称得出你在知识区、生活区、游戏区各收藏了多少。UP主统计根据data.medias[i].upper.name统计哪位UP主最受你青睐。时长分布根据data.medias[i].duration计算你收藏视频的总时长甚至分析时长区间分布。收藏时间线根据data.medias[i].fav_time分析你在哪段时间收藏欲望最旺盛。实现这些只需要在循环中增加相应的数据分类和累加逻辑即可。结果可以展示在一个可折叠的详细面板里。5. 常见问题排查与实战技巧在实际编写和运行过程中你可能会遇到以下问题5.1 问题脚本在油猴中启用但打开收藏夹页面没有任何反应。排查步骤检查脚本管理器图标确认脚本是否真的处于“已启用”状态。检查脚本的match规则是否与当前页面URL匹配。在油猴仪表板中编辑脚本match行需要匹配你的收藏夹URL。打开浏览器开发者工具F12切换到“Console”控制台标签页查看是否有JavaScript报错红色错误信息。这是最重要的调试手段。在脚本开头添加console.log(脚本已加载);刷新页面看控制台是否有输出以判断脚本是否被注入。5.2 问题控制台出现“GM_xmlhttpRequest is not defined”或跨域错误。原因与解决确保脚本头部正确声明了// grant GM_xmlhttpRequest和// connect api.bilibili.com。油猴脚本的GM_*API是异步的确保在GM_xmlhttpRequest的回调函数或使用其Promise封装版本中进行操作。如果使用原生的fetch在用户脚本环境中可能会因为内容安全策略CSP而失败这就是为什么推荐使用GM_xmlhttpRequest。5.3 问题能收到API响应但data.medias是空数组或者media_count为0但实际上收藏夹有视频。可能原因接口已更新B站可能更改了API的路径、参数或响应格式。你需要重新按3.1节的步骤用开发者工具抓取最新的请求。私有收藏夹或需要鉴权某些收藏夹如默认的“稍后再看”或设为私密的收藏夹可能需要额外的鉴权参数。检查你抓取的请求头中是否包含Authorization等字段并在你的脚本请求中模拟添加。分页参数问题尝试调整ps每页大小参数有时最大值可能不是20。5.4 问题请求一段时间后突然失败返回412、429等错误码。原因与解决429 Too Many Requests请求过于频繁触发速率限制。这是最可能的情况。务必在循环请求中增加delay如300-500毫秒并避免使用高并发。412 Precondition Failed可能缺少必要的请求头如User-Agent、Referer等。确保你的GM_xmlhttpRequest的headers部分尽可能模拟浏览器发出的请求。通用解决方案在请求函数中加入重试机制。function fetchWithRetry(url, options, maxRetries 3, retryDelay 1000) { return new Promise((resolve, reject) { const attempt (retryCount) { GM_xmlhttpRequest({ ...options, onload: function(response) { if (response.status 429 retryCount maxRetries) { // 遇到429等待一段时间后重试 console.warn(遇到429第${retryCount 1}次重试...); setTimeout(() attempt(retryCount 1), retryDelay * (retryCount 1)); } else if (response.status 200 response.status 300) { resolve(response); } else { reject(response); } }, onerror: function(error) { if (retryCount maxRetries) { setTimeout(() attempt(retryCount 1), retryDelay * (retryCount 1)); } else { reject(error); } } }); }; attempt(0); }); }5.5 实战技巧如何让脚本更“聪明”自动检测页面更新如果你的收藏夹是动态更新的比如你边看脚本边收藏新视频可以监听页面变化或提供一个“重新统计”按钮。可以使用MutationObserver监听收藏列表容器的变化然后自动重新统计。提供配置选项通过油猴脚本的// grant GM_getValue和// grant GM_setValue可以为脚本增加设置菜单让用户自定义是否自动运行、请求延迟时间、并发数等。优雅降级如果主要API失效可以尝试降级方案例如通过解析页面DOM来数li标签。虽然慢且不稳定但作为备用方案能提高脚本的鲁棒性。编写这样一个脚本的过程远比得到一个简单的数字更有价值。它迫使你去理解一个现代Web应用的前后端交互方式思考如何编写健壮、用户友好的客户端代码并处理各种网络和环境的异常情况。当你看到那个统计数字出现在页面上时它不仅仅是一个计数更是你通过代码与庞大互联网服务进行的一次成功对话。