猜测、实现 B 站在看人数
猜测、实现 B 站在看人数
- 猜测
- 找到接口
- 参数
- 总结
- 实现
猜测
找到接口
浏览器打开一个 B 站视频,比如 《黑神话:悟空》最终预告 | 8月20日,重走西游_黑神话悟空 (bilibili.com) ,打开 F12 开发者工具,经过观察,发现每 30 秒就会有一个如下的请求:
https://api.bilibili.com/x/player/online/total?aid=1056417986&cid=1641689875&bvid=BV1oH4y1c7Kk&ts=57523354{"code": 0,"message": "0","ttl": 1,"data": {"total": "239","count": "182","show_switch": {"total": true,"count": true},"abtest": {"group": "b"}}
}
返回值中的 data.total 就是在看人数,如下:

参数
请求有 4 个参数:
aid=1056417986
cid=1641689875
bvid=BV1oH4y1c7Kk
ts=57523354
aid、bvid 是稿件的编号,cid 是视频的编号,一个稿件可能有多个视频。通过三者可定位到唯一的视频。
ts 从命名上来看应该是时间戳,比如 57523353、57523354 ,但显然太短了,应该是经过处理的,最后发现是时间戳(秒)除以 30 向上取整的结果:
calcTs = function(date) {// 时间戳(秒)const timestamp_second = date.getTime() / 1000;// 除以 30 向上取整const ts = Math.ceil(timestamp_second / 30);console.log(ts)return ts;
}
下图是两个请求的参数以及请求的时间:


在浏览器控制台验证猜想,通过 calcTs 函数可计算出 ts,与请求参数完全吻合:

总结
B 站的实现思路应该是:aid、bvid、cid 作为唯一编号,以 30 秒为一个时间窗口进行统计,在这 30s 中的请求都会使窗口值加 1,每次累加完后返回最新值即可。
但同时还发现在多个标签页中打开同一个视频时,比如 5 个标签页,一开始在看人数都是 1,等一会在看人数才会陆续变成 5。也就是说返回的不是最新值,因为如果返回最新值的话,5 个标签页的在看人数应该分别是 1 2 3 4 5。
猜测应该是同时存在两个 30 秒时间窗口,这里称为当前窗口( currentWindow ,也就是 ts 对应的 30s 窗口) 和上一个窗口(previousWindow 即 ts - 1 对应的 30s 窗口),每次都累加到 currentWindow,但返回 previousWindow。
这样就能解释为什么一开始在看人数都是 1,等一会在看人数才会陆续变成 5 了。打开视频时,previousWindow 不存在,所以返回了 1;同时创建 currentWindow 并从 1 累加到 5。这样等 30s 后下一个定时任务时,currentWindow 就变成了 previousWindow,5 个标签页都会返回 5,在看人数就都陆续变成 5 了。
实现
后端可以使用 Redis 实现,最简单的办法是使用 string 结构,以 aid、bvid、cid、ts 作为 key,给 key 设置大于 60s 的过期时间,每次请求时使用 incr 自增即可。但这样会导致 Redis 找那个有大量的 key,不好维护。
可以使用 hash 结构,以 ts 为 key,以 aid、bvid、cid 为 field,窗口值为 value。这样 Redis 中只会有 ts、ts - 1 两个 key。如果必要的话,也可以根据 field 的值将其 hash 分区到 2 * N 个 key 中。
TotalService
package com.example.demo3;import lombok.SneakyThrows;
import org.redisson.api.*;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;import java.time.Duration;
import java.util.concurrent.ExecutionException;@Service
public class TotalService {private final RedissonClient redisson;public TotalService(RedissonClient redisson) {this.redisson = redisson;}@SneakyThrows({ExecutionException.class, InterruptedException.class})@GetMappingpublic Integer total(String aid, String bvid, String cid, Long ts) {RBatch batch = redisson.createBatch(BatchOptions.defaults());// currentWindow// 以时间戳作为 keyRMapAsync<String, Integer> currentWindow = batch.getMap(ts.toString());// 以 aid, bvid, cid 作为 currentWindow 的 keyString field = field(aid, bvid, cid);// 自增 + 1currentWindow.addAndGetAsync(field, 1);// 过期时间必须大于 60scurrentWindow.expireIfNotSetAsync(Duration.ofSeconds(70));// previousWindowRMapAsync<String, Integer> previousWindow = batch.getMap(String.valueOf(ts - 1));RFuture<Integer> totalFuture = previousWindow.getAsync(field);batch.execute();Integer total = totalFuture.get();// 如果 previousWindow 不存在,则返回 1if (total == null || total == 0) {return 1;}return total;}private String field(String aid, String bvid, String cid) {return aid + ":" + bvid + ":" + cid;}
}
TotalController
@RestController
@RequestMapping("/x/player/online/total")
public class TotalController {private final TotalService totalService;public TotalController(TotalService totalService) {this.totalService = totalService;}@CrossOrigin(originPatterns = "*")@GetMappingpublic Integer total(@RequestParam("aid") String aid, @RequestParam("bvid") String bvid,@RequestParam("cid") String cid, @RequestParam("ts") Long ts) {return totalService.total(aid, bvid, cid, ts);}
}
test.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div><div>aid <input id="aid" type="text" value="113071355923972">bvid <input id="bvid" type="text" value="BV1giHnexEiD">cid <input id="cid" type="text" value="25714427593"></div><div>在看:<span id="total">0</span></div>
</div>
</body>
<script type="text/javascript">const elem_aid = document.getElementById("aid");const elem_bvid_elem = document.getElementById("bvid");const elem_cid_elem = document.getElementById("cid");const elem_total = document.getElementById("total");refreshTotal().then(() => {// 30 秒执行一次setInterval(function () {refreshTotal();}, 30000)});async function refreshTotal() {const aid = elem_aid.value;const bvid = elem_bvid_elem.value;const cid = elem_cid_elem.value;const ts = calcTs(new Date());const url = `http://localhost:8080/x/player/online/total?aid=${aid}&cid=${cid}&bvid=${bvid}&ts=${ts}`;const response = await fetch(url);const total = await response.json();console.log(total);elem_total.innerHTML = total;}function calcTs(date) {// 时间戳(秒)const timestamp_second = date.getTime() / 1000;// 除以 30 向上取整const ts = Math.ceil(timestamp_second / 30);console.log(ts)return ts;}
</script>
</html>
相关文章:
猜测、实现 B 站在看人数
猜测、实现 B 站在看人数 猜测找到接口参数总结 实现 猜测 找到接口 浏览器打开一个 B 站视频,比如 《黑神话:悟空》最终预告 | 8月20日,重走西游_黑神话悟空 (bilibili.com) ,打开 F12 开发者工具,经过观察…...
网络编程(UDP)
UDP编程 UDP:全双工通信、面向无连接、不可靠 UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。 适用场景 发送小尺寸…...
CENet及多模态情感计算实战(论文复现)
CENet及多模态情感计算实战(论文复现) 本文所涉及所有资源均在传知代码平台可获取 文章目录 CENet及多模态情感计算实战(论文复现)概述研究背景主要贡献论文思路主要内容和网络架构数据集介绍性能对比复现过程(重要&am…...
备战秋招60天算法挑战,Day34
题目链接: https://leetcode.cn/problems/coin-change/ 视频题解: https://www.bilibili.com/video/BV1qsvDeHEkg/ LeetCode 322.零钱兑换 题目描述 给你一个整数数组coins,表示不同面额的硬币;以及一个整数amount,表…...
vue实现评论滚动效果
vue插件实现滚动效果 一、安装组件 官网地址:https://chenxuan0000.github.io/vue-seamless-scroll/ 1、vue2安装 npm install vue-seamless-scroll --savevue3安装 npm install vue3-seamless-scroll --save二、组件引入 <template><div v-if"…...
iphone13 不升级IOS使用广电卡
iPhone13的信号📶,15系统刷高版本iPCC,本帖以后不再更新!!! 自从知道可以通过刷iPCC的方式改善信号(不更新iOS大版本的情况下),尝试了各种版本。 我自己用下来总结 - 移动联通48、49、50 &…...
网络地址转换
文章目录 1. NAT使用环境2. NAT的优缺点3. NAT的三种类型4. NAT工作原理5. 配置示例6. 常用排错命令 1. NAT使用环境 需要连接到互联网,但主机没有全局唯一的IP地址;更换的ISP的要求对网络进行重新编址;需要合并两个使用相同编址方案的内联网…...
【python】 @property属性详解 and mysql的sqlalchemy的原生sql
【python】 property属性详解 一文搞懂python中常用的装饰器(classmethod、property、staticmethod、abstractmethod…) sqlalchemy的原生sql from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker# 数据库连接字符串 DATAB…...
西门子WinCC开发笔记(一):winCC西门子组态软件介绍、安装
文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/142060535 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、Op…...
如何在5个步骤中编写更好的ChatGPT提示
ChatGPT是一个风靡全球的生成式人工智能 (AI) 工具。虽然它有可能编造一些东西,但是通过精心设计提示,可以确保获得最佳结果。在这篇文章中,我们将探讨如何做到这一点。 在本文中,我将向你展示如何编写提示,激励驱动C…...
最小堆最大堆
文章目录 最小堆、最大堆最小堆(Min-Heap)最大堆(Max-Heap)堆的主要操作及时间复杂度堆的常见应用堆的数组表示大根堆--堆排序 最小堆、最大堆 最小堆(Min-Heap)和最大堆(Max-Heap)…...
华为 HCIP-Datacom H12-821 题库 (10)
有需要题库的可以看主页置顶 V群进行学习交流 1.缺省情况下,BGP 对等体邻接关系的保持时间是多少秒? A、120 秒 B、60 秒 C、10 秒 D、180 秒 答案:D 解析: BGP 存活消息每隔 60 秒发一次,保持时间“180 秒” 2.缺省…...
如何利用命令模式实现一个手游后端架构?
命令模式的原理解读 命令模式的英文翻译是 Command Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的: The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different reques…...
ThreadLocal 释放的方式有哪些
ThreadLocal基础概念:IT-BLOG-CN ThreadLocal是Java中用于在同一个线程中存储和隔离变量的一种机制。通常情况下,我们使用ThreadLocal来存储线程独有的变量,并在任务完成后通过remove方法清理这些变量,以防止内存泄漏。然而&…...
监控-zabbix
1运维监控 是指对计算机系统、网络、服务器等关键IT基础设施进行实时监控,以确保系统的稳定运行和及时发现潜在问题 2老监控框架(不会用但需要知道) Cacti: Cacti是一款基于PHP、MySQL开发的网络流量监测图形分析工具。主要监…...
设计模式 解释器模式(Interpreter Pattern)
文章目录 解释器模式简绍解释器模式的结构优缺点UML图具体代码实现Context 数据实体类,可以包含一些方法Abstract Expression 创建接口方法Terminal Expression 对数据简单处理Non-Terminal Expression 同样实现抽象接口方法Client(客户端) 调…...
Linux echo命令讲解及与重定向符搭配使用方法,tail命令及日志监听方式详解
echo echo具有回声,回响的意思,在linux系统中echo一般可以输出指定字符或用于命令执行 echo命令的用法为 echo 输出字符串 或 echo 命令 若参数为字符串则进行字符串输出,注意若字符串中含空格最好将其用引号括起,防止echo命…...
Linux网络:总结协议拓展
1. TCP/IP四层模型总结 2. 网络协议拓展 DNS协议(地址解析协议) TCP/IP使用IP地址和端口号来确定网络中一台主机的一个程序。 但是这样标定不方便记忆,于是开始引出主机名(字符串),使用hosts文件来描述…...
去除恢复出厂设置中UI文字显示
文章目录 需求场景 一、代码跟踪与分析在线文字搜索RK平台本地源码搜索实际测试验证代码推理 二、实现方案三、延伸知识四、知识总结 需求 需求:去除恢复出厂设置中UI文字显示 场景 Android 相关产品各种方向旋转、强制横竖屏等需求,导致在恢复出厂设…...
《高校教育管理》
《高校教育管理》为中文社会科学引文索引(CSSCI)来源期刊、北大中文核心期刊、RCCSE中国核心学术期刊、人大“复印报刊资料”重要转载来源期刊,是江苏大学主办,中国高等教育管理研究会协办的全国性高等教育管理专业期刊。 ISSN 1…...
【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...
论文阅读:LLM4Drive: A Survey of Large Language Models for Autonomous Driving
地址:LLM4Drive: A Survey of Large Language Models for Autonomous Driving 摘要翻译 自动驾驶技术作为推动交通和城市出行变革的催化剂,正从基于规则的系统向数据驱动策略转变。传统的模块化系统受限于级联模块间的累积误差和缺乏灵活性的预设规则。…...
【iOS】 Block再学习
iOS Block再学习 文章目录 iOS Block再学习前言Block的三种类型__ NSGlobalBlock____ NSMallocBlock____ NSStackBlock__小结 Block底层分析Block的结构捕获自由变量捕获全局(静态)变量捕获静态变量__block修饰符forwarding指针 Block的copy时机block作为函数返回值将block赋给…...
