〇、前言
在逛B站的时候,看到有个UP主准备都在动态抽一些幸运儿,交代了上下文顺便问了句 “话说,谁知道怎么抽奖?”。话都说到这份上了,那我还能坐得住???立马打开我的 VSCODE 撸了串代码。。。一切从简,不考虑油猴了
一、使用方法
- 打开动态页面或者视频页,按
F12打开开发者工具并切换到console选项卡。 - 将下方代码粘贴到
console中并回车运行。 - 根据提示操作即可。
效果图如图所示:

let auto = false;
let mode = '';
let userList = new Map();
let luckyNumber = new Set();
let luckyDogList = [];
let levelUserList = null;
let interval = 1000;
let commentNum = getCommentNum();
let commentCurPage = 1;
const wait = async delay => new Promise(resolve => setTimeout(resolve, delay));
function getCommentNum() {
let domain = window.location.href;
mode = domain.includes('t.bilibili.com') ? 'dynamic' : domain.includes('bilibili.com/video/') ? 'video' : 'other';
let className = domain.includes('t.bilibili.com') ? 'comment' : 'total-reply';
let commentRaw = document.getElementsByClassName(className)[0].textContent;
if (commentRaw.includes('万')) {
return 10000 * parseInt(commentRaw.replace(/[^0-9]/, ''));
}
return parseInt(commentRaw);
}
async function getMoreComment() {
let curNum = document.querySelectorAll('.con').length || document.querySelectorAll('.reply-item').length || 0;
let nextNum = 0;
if (!curNum) throw new Error('获取评论失败');
while (curNum !== nextNum) {
curNum = nextNum;
await wait(interval);
window.scroll(0, 1920 * commentNum); // ?
print('已加载 ' + (commentCurPage++) + ' 页数据', 'font-size: 20px;color: #f5b689;font-weight: bold');
nextNum = document.querySelectorAll('.con').length || document.querySelectorAll('.reply-item').length;
}
}
async function getValidUser() {
if (mode === 'dynamic') {
let commentList = document.querySelectorAll('.list-item.reply-wrap');
for (let comment of commentList) {
let node = comment.querySelector('.con');
let name = node.querySelector('.user > a').textContent;
let id = node.querySelector('.user > a').getAttribute('data-usercard-mid');
let level = node.querySelector('img.level').src.match(/level_\d/gm)[0].replace(/[^0-9]/g, '');
let content = node.querySelector('.text').textContent;
userList.set(id, { id, name, level, content, comment });
}
} else if (mode === 'video') {
let commentList = document.querySelectorAll('.reply-item');
for (let comment of commentList) {
let node = comment.querySelector('.content-warp');
let name = node.querySelector('.user-name').textContent;
let id = node.querySelector('.user-name').getAttribute('data-user-id');
let level = node.querySelector('i.user-level').className.match(/level-\d/gm)?.[0].replace(/[^0-9]/g, '') || '6';
let content = node.querySelector('.reply-content.root-reply').textContent;
userList.set(id, { id, name, level, content, comment });
}
} else {
throw new Error('这是啥子页面哦,没见过~');
}
print('数据全部加载完毕', 'font-size: 20px;color: white;background-color: #00a0d8;border-radius: 3px;');
}
async function roll(num, level) {
// roll(1) 表示抽取 1 位
await wait(interval);
if (level < 0 || level > 6) {
print('今夕是何年?B站都有这等级的人了?');
level = 0;
}
let newUserList = null;
if (level) {
let iterator = userList.entries();
newUserList = new Map();
while (true) {
let item = iterator.next().value;
if (!item) break;
if (parseInt(item[1].level) >= num) {
newUserList.set(item[0], item[1]);
}
}
}else {
newUserList = userList;
}
if (num < 0 || num > newUserList.size) {
print('抽奖人数不对劲,给你改成抽一个人了');
}
if (newUserList.size === 0) {
print('搁这抽空气呢?没人了', 'font-size: 20px;color: green');
return;
}
while (luckyNumber.size < num) {
luckyNumber.add(Math.floor(Math.random() * newUserList.size));
}
print('随机抽取完成,已标记...', 'font-size: 20px;color: white;background-color: #00a0d8;border-radius: 3px;');
await showLuckyDog(newUserList);
}
async function showLuckyDog(list) {
let ids = Array.from(list.keys());
for (let number of luckyNumber) {
let luckyDog = list.get(ids[number]);
let follow = await getFollowed(luckyDog.id);
luckyDogList.push({
'用户ID': luckyDog.id,
'用户名': luckyDog.name,
'用户等级': luckyDog.level,
'评论内容': luckyDog.content,
'是否关注我': follow || '获取失败'
});
luckyDog.comment.setAttribute('style', 'background-color: #c1dff8');
}
console.table(luckyDogList, ['用户ID', '用户名', '用户等级', '是否关注我']);
luckyNumber.clear();
luckyDogList = [];
}
async function getFollowed(uid) {
await wait(250);
const relationRes = await fetch('https://api.bilibili.com/x/space/acc/relation?mid=' + uid, {
credentials: 'include'
}).then(res => res.json());
if (relationRes.code !== 0) return '获取失败';
let relation = relationRes.data.be_relation;
let followed = relation.attribute !== undefined && relation.attribute !== 0 && relation.attribute !== 128;
return followed ? '已关注' : '未关注';
}
function print(msg, style) {
if (style) {
console.log('%c' + msg, 'padding: 5px 5px;' + style);
} else {
console.log(msg);
}
}
async function main() {
print('代码运行中...');
await wait(interval);
await getMoreComment();
await getValidUser();
print('键入 roll(2) 抽取 2 位用户\t\t\t\t\t\t\t\n键入 roll(1, 4) 抽取 1 位 4 级及以上的用户,以此类推\t',
'width: 100%;font-size: 20px; color: white; background: linear-gradient(270deg,#fad7a1,#e96d71);border-radius: 2px');
if (auto) {
print('为你自动抽取一人', 'font-size: 20px;color: #ccc;');
roll(1)
}
}
main();
二、实现原理
1、动态页或视频页
直接根据 window.location.href 判断是何种页面,然后走不同的页面解析流程即可。
2、自动翻页获取所有评论
最开始的想法是获取到评论总数 commentNum,然后再获取当前页面的顶级评论数 curCommentNum,如果 commentNum !== curCommentNum 说明评论数未获取完毕,则将页面继续向下滚动(加载评论数据),直到满足 commentNum === curCommentNum 。
但在后面的实施过程中,屡次出现下滑页面的死循环,仔细思考过后才明白:由于存在子评论,所以评论总数和顶级评论数的关系只能是 commentNum >= curCommentNum ,而绝大多数都会存在子评论,之前的实现方式是完全错误的。
所以后面修改翻页逻辑:先记录当前页的评论数量 curNum , 翻页后再统计当前的评论数量 nextNum , 如果 curNum !== nextNum 说明新增了一些评论数据,则继续向下翻页;如果 curName === nextNum 说明没有新增评论数据了,即评论获取完毕。
3、获取评论用户信息
这个直接在 html 中提取就行了。
建立一个 Map 变量 userList ,以用户的 uid 作为键,用户的各项信息作为值。遍历每一条评论时,获取用户信息存入 userList ,这样就保证了每个用户的只会存在一次。评论多次的用户,最终都会被最后一条评论覆盖。
4、抽取的随机数
首先获取到抽取的人数 num ,再新建一个 Set 变量 luckyNumber 用来存放生成的随机数,Set 集合能保证一个数只会出现一次。通过 while (luckyNumber.size < num) 来不断加入不同的随机数,数量满足时就会跳出循环。
附录:
获取 是否关注我 的 API : 查询用户与自己关系_互相
console.log 花样:略
fetch 携带 cookies :credentials: 'include'
三、改进
最近看到一些其他的抽奖脚本,比我这个好看多了。等有时间 “借鉴” 一下,更新在这里。
b站评论区抽奖脚本
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法