猜测、实现 B 站在看人数

news2024/12/28 1:52:46

猜测、实现 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 就是在看人数,如下:

image-20240907171726923

参数

请求有 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;
}

下图是两个请求的参数以及请求的时间:

image-20240907172308166

image-20240907172326531

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

image-20240907172656593

总结

B 站的实现思路应该是:aid、bvid、cid 作为唯一编号,以 30 秒为一个时间窗口进行统计,在这 30s 中的请求都会使窗口值加 1,每次累加完后返回最新值即可。

但同时还发现在多个标签页中打开同一个视频时,比如 5 个标签页,一开始在看人数都是 1,等一会在看人数才会陆续变成 5。也就是说返回的不是最新值,因为如果返回最新值的话,5 个标签页的在看人数应该分别是 1 2 3 4 5

猜测应该是同时存在两个 30 秒时间窗口,这里称为当前窗口( currentWindow ,也就是 ts 对应的 30s 窗口) 和上一个窗口(previousWindowts - 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})
    @GetMapping
    public Integer total(String aid, String bvid, String cid, Long ts) {
        RBatch batch = redisson.createBatch(BatchOptions.defaults());
        // currentWindow
        // 以时间戳作为 key
        RMapAsync<String, Integer> currentWindow = batch.getMap(ts.toString());
        // 以 aid, bvid, cid 作为 currentWindow 的 key
        String field = field(aid, bvid, cid);
        // 自增 + 1
        currentWindow.addAndGetAsync(field, 1);
        // 过期时间必须大于 60s
        currentWindow.expireIfNotSetAsync(Duration.ofSeconds(70));

        // previousWindow
        RMapAsync<String, Integer> previousWindow = batch.getMap(String.valueOf(ts - 1));
        RFuture<Integer> totalFuture = previousWindow.getAsync(field);
        batch.execute();

        Integer total = totalFuture.get();
        // 如果 previousWindow 不存在,则返回 1
        if (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 = "*")
    @GetMapping
    public 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2113150.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构初阶】链表分类与双向带头循环链表接口实现

文章目录 1. 链表的分类2. 双向带头循环链表接口实现2. 1 结点声明2. 2 创建链表节点2. 3 初始化链表2. 4 打印链表2. 5 尾插2. 6 判空2. 7 尾删2. 8 头插2. 9 头删2. 10 查找2. 11 在指定位置删除与插入2. 12 销毁 3. 链表接口测试4. 单链表与双链表5. 顺序表与链表 1. 链表的…

Python安装:Mac 使用brew 安装Python2 和 Python3

安装python ## python2 brew install python ## python3 brew install python3出现错误 Error: An unexpected error occurred during the brew link step The formula built, but is not symlinked into /usr/local Permission denied dir_s_mkdir - /usr/local/Frameworks …

根据NVeloDocx Word模板引擎生成Word(一)

自从我们基于免费开放的E6开发平台&#xff0c;实现了根据Word模版生成Word文档的模版引擎后&#xff0c;也实实在在帮助到了一些有需要的朋友。但是由于制作Word模版的过程对于很多人会是一个很头疼的事情&#xff0c;虽然提供了详细的文档&#xff0c;但是我们也经常接到他们…

EcoPaste:重塑剪切板体验,让信息管理变得前所未有的高效

前言 科技&#xff0c;是连接梦想与现实的桥梁&#xff0c;它让人类的想象力得以飞翔&#xff0c;让未来的愿景变为现实。在它的引领下&#xff0c;我们跨越了时空的界限&#xff0c;打破了传统的束缚&#xff0c;以更加开放和包容的心态去迎接生活的挑战与机遇——在看似不相…

I2VGen-XL模型构建指南

一、介绍 VGen可以根据输入的文本、图像、指定的运动、指定的主体&#xff0c;甚至人类提供的反馈信号生成高质量的视频。它还提供了各类常用的视频生成模型工具&#xff0c;例如可视化、采样、训练、推理、使用图像和视频的联合训练&#xff0c;加速等各类工具和技术。 &quo…

图像处理基础篇-镜像仿射透视

一.图像镜像 图像镜像是图像旋转变换的一种特殊情况&#xff0c;通常包括垂直方向和水平方向的镜像。水平镜像通常是以原图像的垂直中轴为中心&#xff0c;将图像分为左右两部分进行堆成变换。如图7-1所示&#xff1a; 垂直镜像通常是以原图像的水平中轴线为中心&#xff0c;将…

快速搞定“照片调色”!50000+Lr预设滤镜模板,一键让你照片不再丑!

照片调色不仅仅是调整颜色&#xff0c;更是一种艺术表达。通过巧妙地运用 LR 预设&#xff0c;可以突出照片的主题&#xff0c;增强情感共鸣。比如&#xff0c;在风景照片中&#xff0c;使用特定的预设可以让天空更蓝、草地更绿&#xff0c;让大自然的美丽更加生动地展现出来。…

Java——踩坑Arrays.asList()

坑1&#xff1a;不能直接使用 Arrsys.asList() 来转换基本类型数据 public static void test1(){// 1、不能直接使用asList来转换基本类型数组int[] arr {1, 2, 3};List list Arrays.asList(arr);System.out.printf("list:%s size:%s class:%s", list, list.size(…

【卡码网C++基础课 18.开房门】

目录 题目描述与分析一、map的基本介绍二、map的使用三、代码编写四、范围for循环 题目描述与分析 题目描述&#xff1a; 假设你手里有一串钥匙&#xff0c;这串钥匙上每把钥匙都有一个编号&#xff0c;对应着一个房门的编号。现给你一个房门编号&#xff0c;你需要判断是否能…

【虚拟化】AIO主机安装PVE8,配置网络,安装win11(virtio,qcow2,scsi,oobe,adk)

【虚拟化】AIO主机安装PVE8&#xff0c;配置网络&#xff0c;安装win11&#xff08;virtio&#xff0c;qcow2&#xff0c;scsi&#xff0c;oobe&#xff0c;adk&#xff09; 文章目录 1、ESXI vs PVE&#xff0c;AIO主机系统二选一2、PVE网络配置&#xff08;DNS&#xff0c;换…

5G前传-介绍

1. 引用 知识分享系列一&#xff1a;5G基础知识-CSDN博客 5G前传的最新进展-CSDN博客 灰光和彩光_通信行业5G招标系列点评之二&#xff1a;一文读懂5G前传-光纤、灰光、彩光、CWDM、LWDM、MWDM...-CSDN博客 术语&#xff1a; 英文缩写描述‌BBU&#xff1a;Building Baseba…

3.门锁_STM32_矩阵按键设备实现

概述 需求来源&#xff1a; 门锁肯定是要输入密码&#xff0c;这个门锁提供了两个输入密码的方式&#xff1a;一个是蓝牙输入&#xff0c;一个是按键输入。对于按键输入&#xff0c;采用矩阵按键来实现。矩阵按键是为了模拟触摸屏的按键输入&#xff0c;后续如果项目结束前还…

关于 QImage原始数据格式与cv::Mat原始数据进行手码数据转换 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/141996117 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

神经网络骨架nn.Module

文章目录 一、认识nn.Module二、nn.Module的基础加1操作 一、认识nn.Module nn.Module 是 PyTorch 中的一个核心类&#xff0c;它是所有神经网络模块的基类。在 PyTorch 中构建模型时&#xff0c;通常会继承这个类来创建自定义的网络结构。nn.Module 提供了一系列用于构建神经…

MyBatis-Plus拦截器接口InnerInterceptor失效?因MyBatis缓存机制而踩的一个深坑

InnerInterceptor 接口是 MyBatis-Plus 提供的一个拦截器接口&#xff0c;用于实现一些常用的 SQL 处理逻辑。例如某个组件运作在多系统的平台上&#xff0c;不同系统需要隔离&#xff0c;于是可以通过这个拦截器接口&#xff0c;给每一条要执行的sql末尾拼接一个AND systemId …

3个恢复方法详解:iPhone手机快速找回备忘录

当我们在工作或者是学习时&#xff0c;总会有一些灵光乍现的好想法&#xff0c;我们通常会将这些想法记录在iPhone手机备忘录中&#xff0c;以便随时查看。但是&#xff0c;如果出现不慎删除备忘录的情况&#xff0c;iPhone该如何找回备忘录呢&#xff1f;不用担心&#xff0c;…

[JAVA基础知识汇总-1] 创建线程的几种方式(含线程池相关)

文章目录 1. 继承Thread类2. 实现Runnable接口3. 实现Callable接口4. 线程池4.1 利用Executors工具类来创建线程池4.2 为什么不建议使用Executors来创建线程池&#xff1f;4.3 ThreadPoolExecutor是线程池的核心实现类&#xff0c;可以利用它来创建线程池4.4 线程池的状态 可以…

数字化转型的关键指南:《数字化专业知识体系》深度剖析应用策略

数字化浪潮下的企业生存法则 随着全球企业加速数字化转型&#xff0c;如何有效应对技术变革带来的挑战和机遇成为各行业关注的焦点。传统的IT管理模式已经无法满足日益复杂的数字化需求&#xff0c;亟需一种新型、综合的知识体系来引导企业迈向成功。《数字化专业知识体系》&a…

【MySQL】MySQL基础

目录 什么是数据库主流数据库基本使用MySQL的安装连接服务器服务器、数据库、表关系使用案例数据逻辑存储 MySQL的架构SQL分类什么是存储引擎 什么是数据库 mysql它是数据库服务的客户端mysqld它是数据库服务的服务器端mysql本质&#xff1a;基于C&#xff08;mysql&#xff09…

【unix高级编程系列】线程

引言 我们知道unix进程中可以有多个线程&#xff0c;进程中的线程可以访问该进程的所有组成部分。并且CPU的调度单元就是线程。这就面临一个问题&#xff1a;当进程中的临界资源需要在多个线程中共享时&#xff0c;如何解决一致性问题&#xff1f; 本文将从线程的概念、线程的…