〇、前言

在逛B站的时候,看到有个UP主准备都在动态抽一些幸运儿,交代了上下文顺便问了句 “话说,谁知道怎么抽奖?”。话都说到这份上了,那我还能坐得住???立马打开我的 VSCODE 撸了串代码。。。一切从简,不考虑油猴了

一、使用方法

  1. 打开动态页面或者视频页,按 F12 打开 开发者工具 并切换到 console 选项卡。
  2. 将下方代码粘贴到 console 中并回车运行。
  3. 根据提示操作即可。

效果图如图所示:
image-20221029235646004-16670590085001.png

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 携带 cookiescredentials: 'include'

三、改进

最近看到一些其他的抽奖脚本,比我这个好看多了。等有时间 “借鉴” 一下,更新在这里。