猜测、实现 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…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...

select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...