轉(zhuǎn)載 感謝 盛百凡 寫的動(dòng)態(tài)評(píng)論區(qū)抽獎(jiǎng)腳本工具源代碼 版本更新到1.7 可顯示Ip地址歸屬
/**
?* 抽獎(jiǎng)號(hào)的日常2: 如何專業(yè)地評(píng)論區(qū)開獎(jiǎng)
?*
?* @author [盛百凡]{@link https://space.bilibili.com/14064125}
?* @version 1.7.0
?* @see [Weighted random sampling with a reservoir]{@link https://doi.org/10.1016/j.ipl.2005.11.003}
?*/
(async () => {
? 'use strict';
? // 暫停
? const wait = async delay => new Promise(resolve => setTimeout(resolve, delay));
? // 清空控制臺(tái)
? await wait(0);
? console.clear();
? // 用戶配置
? const USER_CONFIG = {
? ? // 本地保存所有評(píng)論
? ? SAVE_COMMENTS: true,
? ? // 獲取評(píng)論IP歸屬地
? ? GET_COMMENT_IP: true,
? ? // 單用戶累計(jì)評(píng)論數(shù)上限
? ? MAX_REPEAT: 5,
? ? // 用戶等級(jí)權(quán)重
? ? LEVEL_WEIGHT: {Lv0: 0, Lv1: 0, Lv2: 0, Lv3: 0.5, Lv4: 1, Lv5: 1, Lv6: 1},
? ? // 勛章等級(jí)權(quán)重
? ? MEDAL_WEIGHT: {
? ? ? Lv_0: 1,
? ? ? Lv_1: 1, Lv_2: 1, Lv_3: 1, Lv_4: 1, Lv_5: 1, Lv_6: 1, Lv_7: 1, Lv_8: 1, Lv_9: 1, Lv10: 1,
? ? ? Lv11: 1, Lv12: 1, Lv13: 1, Lv14: 1, Lv15: 1, Lv16: 1, Lv17: 1, Lv18: 1, Lv19: 1, Lv20: 1,
? ? ? Lv21: 1, Lv22: 1, Lv23: 1, Lv24: 1, Lv25: 1, Lv26: 1, Lv27: 1, Lv28: 1, Lv29: 1, Lv30: 1,
? ? ? Lv31: 1, Lv32: 1, Lv33: 1, Lv34: 1, Lv35: 1, Lv36: 1, Lv37: 1, Lv38: 1, Lv39: 1, Lv40: 1
? ? },
? ? // 會(huì)員類型權(quán)重
? ? VIP_WEIGHT: {普通: 1, 月度: 1, 年度: 1}
? };
? // 系統(tǒng)配置
? const SYS_CONFIG = {
? ? // API請(qǐng)求間隔(毫秒)
? ? API_INTERVAL: 250
? };
? // 控制臺(tái)顏色
? const COLOR = {
? ? RED: '#EE230D',
? ? PINK: '#FF8CC6',
? ? ORANGE: '#FF9201',
? ? GREEN: '#1DB100',
? ? BLUE: '#02A2FF',
? ? GRAY: '#D6D5D5'
? };
? // 格式百分比
? const stylePercent = (num, digits) => {
? ? if (num < 0 || num > 1) {
? ? ? throw `百分比[${num}]必須在[0, 1]之間`;
? ? }
? ? const maxLen = digits === 0 ? 3 : digits + 4;
? ? if (num === 0) {
? ? ? return {text: 'N/A'.padStart(maxLen, ' '), css: `color:${COLOR.GRAY}`};
? ? }
? ? return {text: (100 * num).toFixed(digits).padStart(maxLen, ' '), css: ''};
? };
? // 格式整數(shù)
? const styleInt = (num, maxLen, color) => ({text: num.toString().padStart(maxLen, ' '), css: color === undefined ? '' : `color:${color}`});
? // 格式A(用戶等級(jí) 認(rèn)證類型 會(huì)員類型)
? const styleA = (words, color, key) => ({key: key, text: words[0], css: `border-radius:3px;color:#FFFFFF;background:${color};padding:1px`});
? // 格式B(勛章等級(jí))
? const styleB = (words, color, key) => {
? ? const style = {
? ? ? key: key,
? ? ? prefix: {text: words[0], css: `border-radius:3px 0 0 3px;color:#FFFFFF;background:${color};padding:1px 0 1px 1px`},
? ? ? main: {text: words[1], css: `border-radius:0 3px 3px 0;border-style:solid;border-width:1px;border-color:${color};color:${color}`}
? ? };
? ? if (words[1].length === 1) {
? ? ? style.main.css += ';padding:0 0.5ch';
? ? }
? ? return style;
? };
? // 格式捕捉器
? const styleHandler = (style, prefix) => ({
? ? get(target, prop, receiver) {
? ? ? const origin = Reflect.get(target, prop, receiver);
? ? ? if (typeof prop === 'string' && Reflect.has(target, prop) && !Number.isNaN(+prop)) {
? ? ? ? if (prefix === undefined) {
? ? ? ? ? return style([origin[0]], origin[1], origin[0]);
? ? ? ? }
? ? ? ? return style([prefix, prop], origin[1], origin[0]);
? ? ? }
? ? ? return origin;
? ? }
? });
? // 控制臺(tái)格式化
? const consoleFormat = (styles, separator = '') => {
? ? // 轉(zhuǎn)義
? ? const escape = msg => {
? ? ? if (msg === null || msg === undefined) {
? ? ? ? return `%c${msg}`;
? ? ? }
? ? ? return '%c' + msg.toString().replaceAll('%', '%%');
? ? };
? ? const text = [];
? ? const css = [];
? ? for (const s of styles) {
? ? ? if (s === null || s === undefined || s === '') {
? ? ? ? continue;
? ? ? }
? ? ? if (typeof s === 'string') {
? ? ? ? text.push(escape(s));
? ? ? ? css.push('');
? ? ? } else if (s.main === undefined) {
? ? ? ? text.push(escape(s.text));
? ? ? ? css.push(s.css);
? ? ? } else if (s.prefix !== undefined) {
? ? ? ? text.push(escape(s.prefix.text) + escape(s.main.text));
? ? ? ? css.push(s.prefix.css, s.main.css);
? ? ? } else {
? ? ? ? throw `格式錯(cuò)誤 ${styles}`;
? ? ? }
? ? ? text.push(escape(separator));
? ? ? css.push('');
? ? }
? ? return {text: text.join(''), css: css};
? };
? // 圖標(biāo)格式
? const STYLE = {
? ? // 用戶等級(jí)
? ? LEVEL: new Proxy([
? ? ? ['Lv0', '#BFBFBF'],
? ? ? ['Lv1', '#BFBFBF'],
? ? ? ['Lv2', '#95DDB2'],
? ? ? ['Lv3', '#92D1E5'],
? ? ? ['Lv4', '#FFB37C'],
? ? ? ['Lv5', '#FF6C00'],
? ? ? ['Lv6', '#FF0000']
? ? ], styleHandler(styleA)),
? ? // 勛章等級(jí)
? ? MEDAL: new Proxy([
? ? ? ['Lv_0', '#BFBFBF'],
? ? ? ['Lv_1', '#5C968E'], ['Lv_2', '#5C968E'], ['Lv_3', '#5C968E'], ['Lv_4', '#5C968E'],
? ? ? ['Lv_5', '#5D7B9E'], ['Lv_6', '#5D7B9E'], ['Lv_7', '#5D7B9E'], ['Lv_8', '#5D7B9E'],
? ? ? ['Lv_9', '#8D7CA6'], ['Lv10', '#8D7CA6'], ['Lv11', '#8D7CA6'], ['Lv12', '#8D7CA6'],
? ? ? ['Lv13', '#BE6686'], ['Lv14', '#BE6686'], ['Lv15', '#BE6686'], ['Lv16', '#BE6686'],
? ? ? ['Lv17', '#C79D24'], ['Lv18', '#C79D24'], ['Lv19', '#C79D24'], ['Lv20', '#C79D24'],
? ? ? ['Lv21', '#1A544B'], ['Lv22', '#1A544B'], ['Lv23', '#1A544B'], ['Lv24', '#1A544B'],
? ? ? ['Lv25', '#06154C'], ['Lv26', '#06154C'], ['Lv27', '#06154C'], ['Lv28', '#06154C'],
? ? ? ['Lv29', '#2D0855'], ['Lv30', '#2D0855'], ['Lv31', '#2D0855'], ['Lv32', '#2D0855'],
? ? ? ['Lv33', '#7A0423'], ['Lv34', '#7A0423'], ['Lv35', '#7A0423'], ['Lv36', '#7A0423'],
? ? ? ['Lv37', '#FF610B'], ['Lv38', '#FF610B'], ['Lv39', '#FF610B'], ['Lv40', '#FF610B']
? ? ], styleHandler(styleB, '粉絲')),
? ? // 認(rèn)證類型
? ? OFFICIAL: new Proxy([
? ? ? ['普通', '#BFBFBF'],
? ? ? ['個(gè)人', '#F6C851'],
? ? ? ['企業(yè)', '#6FC4FA']
? ? ], styleHandler(styleA)),
? ? // 會(huì)員類型
? ? VIP: new Proxy([
? ? ? ['普通', '#BFBFBF'],
? ? ? ['月度', '#FDB8CC'],
? ? ? ['年度', '#FB7299']
? ? ], styleHandler(styleA))
? };
? // 原生請(qǐng)求
? const web = async (baseUrl, params, useCookie) => {
? ? const url = new URL(baseUrl);
? ? for (const [k, v] of Object.entries(params)) {
? ? ? url.searchParams.set(k, v);
? ? }
? ? const response = await fetch(url, {method: 'GET', credentials: useCookie ? 'include' : 'omit', referrerPolicy: 'no-referrer'});
? ? if (response.status !== 200) {
? ? ? throw `請(qǐng)求異常 ${response.status}`;
? ? }
? ? return await response.json();
? };
? // 日志
? const LOG = [];
? // 日志記錄器
? const LOGGER = {
? ? level: 0,
? ? plain_(msg) {
? ? ? if (msg === null || msg === undefined) {
? ? ? ? return `${msg}`;
? ? ? }
? ? ? return msg.toString().replaceAll('%c', '').replaceAll('%%', '%');
? ? },
? ? log_(msg) {
? ? ? LOG.push(this.plain_(msg));
? ? },
? ? group_(msg) {
? ? ? this.level++;
? ? ? LOG.push(`${'#'.repeat(this.level)} ${this.plain_(msg)}`);
? ? },
? ? log(msg, ...params) {
? ? ? this.log_(msg);
? ? ? console.log(msg, ...params);
? ? },
? ? table(data, properties) {
? ? ? this.log_('(暫不支持表格)');
? ? ? console.table(data, properties);
? ? },
? ? group(msg, ...params) {
? ? ? this.group_(msg);
? ? ? console.group(msg, ...params);
? ? },
? ? groupCollapsed(msg, ...params) {
? ? ? this.group_(msg);
? ? ? console.groupCollapsed(msg, ...params);
? ? },
? ? groupEnd() {
? ? ? if (this.level > 0) {
? ? ? ? this.level--;
? ? ? ? if (this.level === 0) {
? ? ? ? ? LOG.push('');
? ? ? ? }
? ? ? }
? ? ? console.groupEnd();
? ? }
? };
? // 比較數(shù)字
? const compareInt = (v1, v2) => v1 < v2 ? -1 : (v1 === v2 ? 0 : 1);
? // 比較器
? const comparator = (...extracts) => (c1, c2) => {
? ? for (const ext of extracts) {
? ? ? const res = compareInt(ext(c1), ext(c2));
? ? ? if (res !== 0) {
? ? ? ? return res;
? ? ? }
? ? }
? ? return 0;
? };
? // 格式化時(shí)間
? const formatTime = unix => {
? ? const d = new Date(unix * 1000);
? ? const year = d.getFullYear().toString();
? ? const month = (d.getMonth() + 1).toString().padStart(2, '0');
? ? const date = d.getDate().toString().padStart(2, '0');
? ? const hours = d.getHours().toString().padStart(2, '0');
? ? const minutes = d.getMinutes().toString().padStart(2, '0');
? ? const seconds = d.getSeconds().toString().padStart(2, '0');
? ? return `${year}-${month}-${date} ${hours}:${minutes}:${seconds}`;
? };
? // 格式化日期
? const formatDate = unix => {
? ? const d = new Date(unix * 1000);
? ? const year = d.getFullYear().toString();
? ? const month = (d.getMonth() + 1).toString().padStart(2, '0');
? ? const date = d.getDate().toString().padStart(2, '0');
? ? return `${year}-${month}-${date}`;
? };
? // 檢查整數(shù)
? const ckInt = value => {
? ? if (!Number.isInteger(value)) {
? ? ? throw `[${value}]非整數(shù)`;
? ? }
? ? return value;
? };
? // 檢查字符串
? const ckStr = value => {
? ? if (typeof value !== 'string') {
? ? ? throw `[${value}]非字符串`;
? ? }
? ? return value;
? };
? // 檢查IP
? const ckIp = location => {
? ? ckStr(location);
? ? return location.startsWith('IP屬地:') ? location.substring(5) : location;
? };
? // 檢查時(shí)間
? const ckUnix = unix => {
? ? ckInt(unix);
? ? if (unix.toString().length !== 10) {
? ? ? throw `時(shí)間[${unix}]必須為10位數(shù)`;
? ? }
? ? return unix;
? };
? // 檢查用戶等級(jí)
? const ckLevel = level => {
? ? ckInt(level);
? ? if (level < 0 || level > 6) {
? ? ? throw `用戶等級(jí)[${level}]必須在[0, 6]之間`;
? ? }
? ? return level;
? };
? // 檢查勛章等級(jí)
? const ckMedal = medal => {
? ? ckInt(medal);
? ? if (medal < 0 || medal > 40) {
? ? ? throw `勛章等級(jí)[${medal}]必須在[0, 40]之間`;
? ? }
? ? return medal;
? };
? // 檢查認(rèn)證
? const ckOfficial = official => {
? ? ckInt(official);
? ? if (official < -1 || official > 1) {
? ? ? throw `認(rèn)證類型[${official}]必須在[-1, 1]之間`;
? ? }
? ? return official + 1;
? };
? // 檢查會(huì)員
? const ckVip = vip => {
? ? ckInt(vip.vipType);
? ? if (vip.vipType < 0 || vip.vipType > 2) {
? ? ? throw `會(huì)員類型[${vip.vipType}]必須在[0, 2]之間`;
? ? }
? ? return vip.vipStatus === 0 ? 0 : vip.vipType;
? };
? // 加入全局配置
? const globalConfig = () => {
? ? // 關(guān)閉監(jiān)控
? ? if (window['Sentry']) {
? ? ? window['Sentry'].getCurrentHub().getClient().getOptions().enabled = false;
? ? ? // 恢復(fù)控制臺(tái)函數(shù)
? ? ? for (const name of ['debug', 'info', 'warn', 'error', 'log', 'assert']) {
? ? ? ? if (console[name].__sentry__) {
? ? ? ? ? console[name] = console[name].__sentry_original__;
? ? ? ? }
? ? ? }
? ? ? // 恢復(fù)全局函數(shù)
? ? ? for (const name of ['fetch', 'addEventListener', 'setTimeout']) {
? ? ? ? if (window[name].__sentry__) {
? ? ? ? ? window[name] = window[name].__sentry_original__;
? ? ? ? }
? ? ? }
? ? }
? ? // 提示頁(yè)面關(guān)閉
? ? addEventListener('beforeunload', event => {
? ? ? event.preventDefault();
? ? ? return event.returnValue = false;
? ? });
? };
? // 加入全局函數(shù)
? const globalFunctions = (...functions) => {
? ? for (const func of functions) {
? ? ? if (window[func.name] !== undefined) {
? ? ? ? throw '重復(fù)運(yùn)行開獎(jiǎng)腳本,請(qǐng)刷新當(dāng)前網(wǎng)頁(yè)頁(yè)面后再次嘗試。';
? ? ? }
? ? ? Object.defineProperty(window, func.name, {value: func});
? ? }
? };
? // 計(jì)算頁(yè)面評(píng)論區(qū)參數(shù)
? const computeCommentInfo = () => {
? ? const url = new URL(location);
? ? const host = url.hostname;
? ? const path = url.pathname;
? ? // 檢查路徑
? ? const ckPath = prefix => {
? ? ? return path.length > prefix.length && path.substring(0, prefix.length) === prefix;
? ? };
? ? // 提取ID
? ? const extId = prefix => {
? ? ? return ckInt(+path.substring(prefix.length));
? ? };
? ? // 檢查全局變量
? ? const ckW = prop => {
? ? ? if (prop === undefined || prop === null) {
? ? ? ? throw '無(wú)法獲取全局變量';
? ? ? }
? ? ? return prop;
? ? };
? ? let info;
? ? if (host === 't.bilibili.com';) {
? ? ? if (ckPath('/')) {
? ? ? ? const vue1 = document.querySelector('div.detail-card > div')?.__vue__?.cardData;
? ? ? ? if (vue1 !== undefined) {
? ? ? ? ? info = {web: '動(dòng)態(tài)', type: vue1.comment_id, oid: vue1.comment_oid};
? ? ? ? }
? ? ? ? const vue2 = document.querySelector('div.bili-dyn-item')?.__vue__?.data.basic;
? ? ? ? if (vue2 !== undefined) {
? ? ? ? ? info = {web: '動(dòng)態(tài)', type: vue2.comment_type, oid: vue2.comment_id_str};
? ? ? ? }
? ? ? }
? ? } else if (host === 'www.bilibili.com';) {
? ? ? if (ckPath('/video/')) {
? ? ? ? info = {web: '投稿視頻', type: 1, oid: ckW(window.aid ?? window.playerInfo?.aid)};
? ? ? } else if (ckPath('/bangumi/play/')) {
? ? ? ? info = {web: '版權(quán)視頻', type: 1, oid: ckW(window.aid ?? window.playerInfo?.aid)};
? ? ? } else if (ckPath('/blackboard/')) {
? ? ? ? info = {web: '活動(dòng)', type: 4, oid: ckW(window.activityId)};
? ? ? } else if (ckPath('/blackroom/ban/')) {
? ? ? ? info = {web: '小黑屋', type: 6, oid: extId('/blackroom/ban/')};
? ? ? } else if (ckPath('/read/cv')) {
? ? ? ? info = {web: '專欄', type: 12, oid: extId('/read/cv')};
? ? ? } else if (ckPath('/audio/au')) {
? ? ? ? info = {web: '音頻', type: 14, oid: extId('/audio/au')};
? ? ? } else if (ckPath('/audio/am')) {
? ? ? ? info = {web: '音頻列表', type: 19, oid: extId('/audio/am')};
? ? ? } else if (ckPath('/cheese/play/ep')) {
? ? ? ? info = {web: '課程', type: 33, oid: extId('/cheese/play/ep')};
? ? ? }
? ? } else if (host === 'h.bilibili.com';) {
? ? ? if (ckPath('/')) {
? ? ? ? info = {web: '相簿', type: 11, oid: extId('/')};
? ? ? }
? ? } else if (host === 'manga.bilibili.com';) {
? ? ? if (ckPath('/detail/mc')) {
? ? ? ? info = {web: '漫畫', type: 22, oid: extId('/detail/mc')};
? ? ? }
? ? }
? ? if (info === undefined) {
? ? ? throw '不支持當(dāng)前頁(yè)面';
? ? }
? ? url.hash = '';
? ? for (const [key, value] of new URL(url).searchParams) {
? ? ? if (key !== 'type') {
? ? ? ? url.searchParams.delete(key);
? ? ? }
? ? }
? ? info.url = url;
? ? LOGGER.log(`[類型] %c${info.web}`, `color:${COLOR.BLUE}`);
? ? LOGGER.log(`%c[url] ${info.url}`, `color:${COLOR.GRAY}`);
? ? LOGGER.log(`%c[type] ${info.type}`, `color:${COLOR.GRAY}`);
? ? LOGGER.log(`%c[oid] ${info.oid}`, `color:${COLOR.GRAY}`);
? ? return info;
? };
? // 獲取當(dāng)前登錄用戶
? const getUser = async () => {
? ? const userRes = await web('https://api.bilibili.com/x/web-interface/nav', {}, true);
? ? if (userRes.code !== 0) {
? ? ? return undefined;
? ? }
? ? return userRes.data.uname;
? };
? // 獲取用戶與當(dāng)前登錄用戶關(guān)系
? const getRelation = async uid => {
? ? await wait(SYS_CONFIG.API_INTERVAL);
? ? const relationRes = await web('https://api.bilibili.com/x/space/acc/relation', {mid: uid}, true);
? ? if (relationRes.code !== 0) {
? ? ? throw '無(wú)法獲取用戶關(guān)系';
? ? }
? ? const relation = relationRes.data.be_relation;
? ? return {followed: relation.attribute !== undefined && relation.attribute !== 0 && relation.attribute !== 128, ts: relation.mtime};
? };
? // 計(jì)算單項(xiàng)權(quán)重
? const computeWeights = (title, styles, weight, column = 1) => {
? ? LOGGER.group(`${title}權(quán)重 (相對(duì)百分比)`);
? ? let max = 0;
? ? let weights = [];
? ? for (const tag of styles) {
? ? ? const w = weight[tag.key];
? ? ? if (typeof w !== 'number' || w < 0) {
? ? ? ? throw `${title}權(quán)重[${tag.key}]必須為非負(fù)數(shù)`;
? ? ? }
? ? ? weights.push(w);
? ? ? max = Math.max(max, w);
? ? }
? ? if (max === 0) {
? ? ? throw `${title}權(quán)重全為零`;
? ? }
? ? weights = weights.map(w => w / max);
? ? let text = [];
? ? let css = [];
? ? for (let i = 0; i < weights.length; i++) {
? ? ? const format = consoleFormat([styles[i], stylePercent(weights[i], 1)], ' ');
? ? ? text.push(format.text);
? ? ? css.push(...format.css);
? ? ? if (i % column === 0) {
? ? ? ? LOGGER.log(text.join('? ?'), ...css);
? ? ? ? text = [];
? ? ? ? css = [];
? ? ? }
? ? }
? ? LOGGER.groupEnd();
? ? return weights;
? };
? // 獲取單頁(yè)評(píng)論
? const getPageComments = async (type, oid, next) => {
? ? await wait(SYS_CONFIG.API_INTERVAL);
? ? G.next = next;
? ? console.log(`%c[${G.call++}] ${next}`, `color:${COLOR.GRAY}`);
? ? const pageRes = await web('https://api.bilibili.com/x/v2/reply/main', {type: type, oid: oid, next: next, mode: 2}, USER_CONFIG.GET_COMMENT_IP);
? ? if (pageRes.code !== 0) {
? ? ? throw '無(wú)法獲取評(píng)論';
? ? }
? ? return [pageRes.data.cursor.is_end, pageRes.data.cursor.prev, pageRes.data.cursor.next, pageRes.data.replies ?? []];
? };
? // 保存評(píng)論
? const saveCommentsToMap = comments => {
? ? for (const [i, c] of comments.entries()) {
? ? ? try {
? ? ? ? const ts = ckUnix(c.ctime);
? ? ? ? if (ts > DRAW_TIME) {
? ? ? ? ? continue;
? ? ? ? }
? ? ? ? const rpid = ckInt(c.rpid);
? ? ? ? const ip = ckIp(c.reply_control?.location ?? '');
? ? ? ? const msg = ckStr(c.content.message);
? ? ? ? const like = ckInt(c.like);
? ? ? ? const reply = ckInt(c.count);
? ? ? ? const detail = {rpid, ts, ip, msg, like, reply};
? ? ? ? const uid = ckInt(c.mid);
? ? ? ? const user = G.uMap.get(uid);
? ? ? ? if (user === undefined) {
? ? ? ? ? const name = ckStr(c.member.uname);
? ? ? ? ? const level = ckLevel(c.member.level_info.current_level);
? ? ? ? ? const medal = ckMedal(c.member.fans_detail?.level ?? 0);
? ? ? ? ? const official = ckOfficial(c.member.official_verify.type);
? ? ? ? ? const vip = ckVip(c.member.vip);
? ? ? ? ? const weight = LEVEL_WEIGHTS[level] * MEDAL_WEIGHTS[medal] * VIP_WEIGHTS[vip];
? ? ? ? ? const details = new Map([[rpid, detail]]);
? ? ? ? ? G.uMap.set(uid, {uid, name, level, medal, official, vip, weight, details});
? ? ? ? } else {
? ? ? ? ? user.details.set(rpid, detail);
? ? ? ? }
? ? ? } catch (e) {
? ? ? ? console.warn(`[${G.call}] ${i} ${e} 跳過`);
? ? ? }
? ? }
? };
? // 加載評(píng)論
? const loadCommentMap = async () => {
? ? let [isEnd, prev, next, cs] = await getPageComments(INFO.type, INFO.oid, G.next);
? ? let mid = 0;
? ? while (!isEnd && prev !== 0 && next !== 0 && cs.length !== 0) {
? ? ? saveCommentsToMap(cs);
? ? ? mid = next + ((prev - next) >> 1);
? ? ? [isEnd, prev, next, cs] = await getPageComments(INFO.type, INFO.oid, next);
? ? }
? ? // 再次獲取最早評(píng)論 (B站BUG 最早評(píng)論有可能被遺漏)
? ? [isEnd, prev, next, cs] = await getPageComments(INFO.type, INFO.oid, mid);
? ? saveCommentsToMap(cs);
? };
? // 用戶概況
? const showSummary = total => {
? ? const pass = G.uList.length;
? ? const fail = G.uMap.size - pass;
? ? if (pass === 0) {
? ? ? throw '不存在有抽獎(jiǎng)資格用戶';
? ? }
? ? LOGGER.log(`[總評(píng)論數(shù)] ${total}`);
? ? LOGGER.log(`[有抽獎(jiǎng)資格用戶總數(shù)] %c${pass}`, `color:${COLOR.BLUE}`);
? ? LOGGER.log(`%c[無(wú)抽獎(jiǎng)資格用戶總數(shù)] ${fail}`, `color:${COLOR.GRAY}`);
? };
? // 用戶分布
? const showUserDistribution = (title, styles, summary, column = 1) => {
? ? LOGGER.group(`用戶分布 [${title}] (有資格數(shù), %c無(wú)資格數(shù)%c)`, `color:${COLOR.GRAY}`, '');
? ? const passLen = Math.max(...summary.passes).toString().length;
? ? const failLen = Math.max(...summary.fails).toString().length;
? ? let text = [];
? ? let css = [];
? ? for (let i = 0; i < summary.passes.length; i++) {
? ? ? const format = consoleFormat([styles[i], styleInt(summary.passes[i], passLen), styleInt(summary.fails[i], failLen, COLOR.GRAY)], ' ');
? ? ? text.push(format.text);
? ? ? css.push(...format.css);
? ? ? if (i % column === 0) {
? ? ? ? LOGGER.log(text.join('? ?'), ...css);
? ? ? ? text = [];
? ? ? ? css = [];
? ? ? }
? ? }
? ? LOGGER.groupEnd();
? };
? // 評(píng)論分布
? const showCommentDistribution = (title, keyName, map, compareFn) => {
? ? LOGGER.groupCollapsed(`評(píng)論分布 [${title}] (評(píng)論總數(shù), Lv0, Lv1, Lv2, Lv3, Lv4, Lv5, Lv6)`);
? ? const list = Array.from(map).map(([key, value]) => ({key, sum: value.reduce((a, b) => a + b, 0), value})).sort(compareFn);
? ? LOGGER.table(list.map(e => ({
? ? ? [keyName]: e.key,
? ? ? '評(píng)論總數(shù)': e.sum,
? ? ? 'Lv0': e.value[0],
? ? ? 'Lv1': e.value[1],
? ? ? 'Lv2': e.value[2],
? ? ? 'Lv3': e.value[3],
? ? ? 'Lv4': e.value[4],
? ? ? 'Lv5': e.value[5],
? ? ? 'Lv6': e.value[6]
? ? })));
? ? LOGGER.groupEnd();
? };
? // 用戶映射
? const mapToUser = user => ({
? ? UID: user.uid,
? ? 用戶名: user.name,
? ? 用戶等級(jí): user.level,
? ? 勛章等級(jí): user.medal,
? ? 認(rèn)證類型: user.official,
? ? 會(huì)員類型: user.vip,
? ? 累計(jì)評(píng)論數(shù): user.details.size
? });
? // 評(píng)論映射
? const mapToDetail = detail => ({
? ? 評(píng)論ID: detail.rpid,
? ? 評(píng)論時(shí)間: formatTime(detail.ts),
? ? 評(píng)論IP: detail.ip,
? ? 評(píng)論內(nèi)容: detail.msg,
? ? 被點(diǎn)贊數(shù): detail.like,
? ? 被評(píng)論數(shù): detail.reply
? });
? // 用戶列表
? const showUsers = (title, list, compareFn) => {
? ? LOGGER.groupCollapsed(`用戶列表 [${title}] (${list.length})`);
? ? if (list.length === 0) {
? ? ? LOGGER.log('%cN/A', `color:${COLOR.GRAY}`);
? ? } else {
? ? ? LOGGER.table(list.sort(compareFn).map(mapToUser));
? ? }
? ? LOGGER.groupEnd();
? };
? // 展示用戶
? const displayUser = async user => {
? ? const details = Array.from(user.details.values()).sort(comparator(detail => detail.ts, detail => detail.rpid));
? ? const replyUrl = new URL(INFO.url);
? ? replyUrl.hash = `reply${details[0].rpid}`;
? ? const level = STYLE.LEVEL[user.level];
? ? const medal = user.medal === 0 ? undefined : STYLE.MEDAL[user.medal];
? ? const official = user.official === 0 ? undefined : STYLE.OFFICIAL[user.official];
? ? const vip = user.vip === 0 ? undefined : STYLE.VIP[user.vip];
? ? const uid = styleB(['UID', user.uid], '#8A8A8A');
? ? const name = {text: user.name, css: 'font-weight:bold'};
? ? const weight = styleB(['權(quán)重', (100 * user.weight).toFixed(1)], '#FEFEFE');
? ? const prob = styleB(['概率', (100 * user.weight / G.uWeight).toFixed(6)], '#FEFEFE');
? ? const format = consoleFormat([level, medal, official, vip, uid, name, weight, prob], ' ');
? ? LOGGER.log(format.text, ...format.css);
? ? if (USER_NAME !== undefined) {
? ? ? const relation = await getRelation(user.uid);
? ? ? if (relation.followed) {
? ? ? ? LOGGER.log(`[是否關(guān)注%c${USER_NAME}%c] %c已關(guān)注 %c${formatTime(relation.ts)}`, `color:${COLOR.PINK};font-weight:bold`, '', `color:${COLOR.GREEN}`, '');
? ? ? } else {
? ? ? ? LOGGER.log(`[是否關(guān)注%c${USER_NAME}%c] %c未關(guān)注`, `color:${COLOR.PINK};font-weight:bold`, '', `color:${COLOR.RED}`);
? ? ? }
? ? }
? ? LOGGER.log(`[首條評(píng)論位置鏈接] ${replyUrl}`);
? ? LOGGER.table(details.map(mapToDetail));
? };
? // 保存文件
? const save = (data, fileName, fileType) => {
? ? const link = document.createElement('a');
? ? link.download = fileName;
? ? const blob = new Blob([data], {type: fileType});
? ? link.href = URL.createObjectURL(blob);
? ? link.click();
? ? URL.revokeObjectURL(link.href);
? };
? // CSV映射
? const mapToCSV = prop => {
? ? switch (typeof prop) {
? ? ? case 'undefined':
? ? ? ? return '';
? ? ? case 'number':
? ? ? ? return `"${prop}"`;
? ? ? case 'string':
? ? ? ? return `"${prop.replaceAll('"', '""').replaceAll('\r', '\u23CE').replaceAll('\n', '\u23CE')}"`;
? ? ? default:
? ? ? ? throw `不支持格式[${typeof prop}]`;
? ? }
? };
? // 保存所有評(píng)論
? const saveComments = rows => {
? ? const data = rows.map(row => row.map(mapToCSV).join(',')).join('\r\n') + '\r\n';
? ? save(data, `評(píng)論_${INFO.type}_${INFO.oid}_${DRAW_TIME}.csv`, 'text/csv');
? };
? // 加權(quán)隨機(jī)排序
? const weightedRandomShuffle = () => {
? ? const MAX = 0x4000;
? ? const ua = new Uint32Array(MAX);
? ? for (const [i, user] of G.uList.entries()) {
? ? ? const index = i % MAX;
? ? ? if (index === 0) {
? ? ? ? window.crypto.getRandomValues(ua);
? ? ? }
? ? ? user.key = -Math.log((ua[index] + 1) / 0x100000001) / user.weight;
? ? }
? ? G.uList.sort(comparator(user => user.key));
? };
? // 統(tǒng)計(jì)評(píng)論
? const countCommentMap = () => {
? ? // 創(chuàng)建統(tǒng)計(jì)數(shù)組
? ? const newSummary = length => ({passes: new Array(length).fill(0), fails: new Array(length).fill(0)});
? ? let total = 0;
? ? const level = newSummary(7);
? ? const medal = newSummary(41);
? ? const official = newSummary(3);
? ? const vip = newSummary(3);
? ? const dateMap = new Map();
? ? const ipMap = new Map();
? ? const overLimits = [];
? ? const zeroWeights = [];
? ? const fans = [];
? ? const companies = [];
? ? const rows = [];
? ? for (const [uid, user] of G.uMap) {
? ? ? if (user.medal > 0) {
? ? ? ? fans.push(user);
? ? ? }
? ? ? if (user.official === 2) {
? ? ? ? companies.push(user);
? ? ? }
? ? ? if (user.details.size > USER_CONFIG.MAX_REPEAT || user.weight === 0) {
? ? ? ? level.fails[user.level]++;
? ? ? ? medal.fails[user.medal]++;
? ? ? ? official.fails[user.official]++;
? ? ? ? vip.fails[user.vip]++;
? ? ? ? if (user.details.size > USER_CONFIG.MAX_REPEAT) {
? ? ? ? ? overLimits.push(user);
? ? ? ? }
? ? ? ? if (user.weight === 0) {
? ? ? ? ? zeroWeights.push(user);
? ? ? ? }
? ? ? } else {
? ? ? ? level.passes[user.level]++;
? ? ? ? medal.passes[user.medal]++;
? ? ? ? official.passes[user.official]++;
? ? ? ? vip.passes[user.vip]++;
? ? ? ? G.uWeight += user.weight;
? ? ? ? G.uList.push(user);
? ? ? }
? ? ? for (const [rpid, detail] of user.details) {
? ? ? ? total++;
? ? ? ? const date = formatDate(detail.ts);
? ? ? ? const dates = dateMap.get(date);
? ? ? ? if (dates === undefined) {
? ? ? ? ? dateMap.set(date, Array.from({length: 7}, (e, i) => user.level === i ? 1 : 0));
? ? ? ? } else {
? ? ? ? ? dates[user.level]++;
? ? ? ? }
? ? ? ? const ips = ipMap.get(detail.ip);
? ? ? ? if (ips === undefined) {
? ? ? ? ? ipMap.set(detail.ip, Array.from({length: 7}, (e, i) => user.level === i ? 1 : 0));
? ? ? ? } else {
? ? ? ? ? ips[user.level]++;
? ? ? ? }
? ? ? ? if (USER_CONFIG.SAVE_COMMENTS) {
? ? ? ? ? rows.push([detail.rpid, formatTime(detail.ts), detail.ip, detail.msg, detail.like, detail.reply, user.uid, user.name, user.level, user.medal, user.official, user.vip, user.details.size, user.weight]);
? ? ? ? }
? ? ? }
? ? }
? ? // 打亂用戶順序
? ? weightedRandomShuffle();
? ? // 參與人數(shù)
? ? showSummary(total);
? ? // 用戶分布
? ? showUserDistribution('用戶等級(jí)', STYLE.LEVEL, level);
? ? showUserDistribution('勛章等級(jí)', STYLE.MEDAL, medal, 4);
? ? showUserDistribution('認(rèn)證類型', STYLE.OFFICIAL, official);
? ? showUserDistribution('會(huì)員類型', STYLE.VIP, vip);
? ? // 評(píng)論分布
? ? showCommentDistribution('日期', '評(píng)論日期', dateMap, comparator(e => e.key));
? ? showCommentDistribution('IP歸屬地', '評(píng)論IP', ipMap, comparator(e => -e.sum, e => e.key));
? ? // 用戶列表
? ? showUsers('超出累計(jì)評(píng)論數(shù)上限', overLimits, comparator(user => -user.details.size, user => user.uid));
? ? showUsers('無(wú)權(quán)重', zeroWeights, comparator(user => user.uid));
? ? showUsers('勛章', fans, comparator(user => -user.medal, user => user.uid));
? ? showUsers('企業(yè)認(rèn)證', companies, comparator(user => user.uid));
? ? // 詳細(xì)評(píng)論
? ? if (USER_CONFIG.SAVE_COMMENTS) {
? ? ? rows.sort(comparator(row => row[1], row => row[0]));
? ? ? rows.unshift(['評(píng)論ID', '評(píng)論時(shí)間', '評(píng)論IP', '評(píng)論內(nèi)容', '被點(diǎn)贊數(shù)', '被評(píng)論數(shù)', 'UID', '用戶名', '用戶等級(jí)', '勛章等級(jí)', '認(rèn)證類型', '會(huì)員類型', '累計(jì)評(píng)論數(shù)', '相對(duì)權(quán)重']);
? ? ? saveComments(rows);
? ? }
? };
? // 評(píng)論加載完成提示
? const commentPrompt = () => {
? ? if (G.done) {
? ? ? console.warn('加載與統(tǒng)計(jì)評(píng)論已完成,控制臺(tái)輸入%c draw(n) %c(n為正整數(shù)) 并回車以隨機(jī)抽取n位用戶。', `color:${COLOR.ORANGE}`, '');
? ? ? console.warn('控制臺(tái)輸入%c shuffle() %c并回車以重新打亂用戶順序。', `color:${COLOR.ORANGE}`, '');
? ? ? console.warn('控制臺(tái)輸入%c saveLog() %c并回車以保存當(dāng)前控制臺(tái)所有顯示內(nèi)容。', `color:${COLOR.ORANGE}`, '');
? ? } else {
? ? ? console.warn('加載與統(tǒng)計(jì)評(píng)論未完成,請(qǐng)嘗試控制臺(tái)輸入%c resume() %c并回車以繼續(xù)加載評(píng)論。', `color:${COLOR.ORANGE}`, '');
? ? }
? };
? // [加載與統(tǒng)計(jì)評(píng)論]
? const resume = async () => {
? ? await wait(0);
? ? if (!G.done) {
? ? ? // ---- 加載評(píng)論 ----
? ? ? console.group('加載評(píng)論');
? ? ? try {
? ? ? ? await loadCommentMap();
? ? ? } catch (e) {
? ? ? ? if (e.status === 412) {
? ? ? ? ? console.error('觸發(fā)B站安全風(fēng)控策略,當(dāng)前IP被暫時(shí)屏蔽。%c請(qǐng)勿關(guān)閉或刷新本頁(yè)面%c,以防丟失加載進(jìn)度。', `color:${COLOR.ORANGE}`, '');
? ? ? ? ? console.error('請(qǐng)更換IP或等待1小時(shí)后,控制臺(tái)輸入%c resume() %c并回車以繼續(xù)加載評(píng)論。', `color:${COLOR.ORANGE}`, '');
? ? ? ? } else {
? ? ? ? ? console.error('發(fā)生未知錯(cuò)誤。%c請(qǐng)勿關(guān)閉或刷新本頁(yè)面%c,以防丟失加載進(jìn)度。', `color:${COLOR.ORANGE}`, '');
? ? ? ? ? console.error('控制臺(tái)輸入%c resume() %c并回車以繼續(xù)加載評(píng)論。', `color:${COLOR.ORANGE}`, '');
? ? ? ? }
? ? ? ? throw e;
? ? ? } finally {
? ? ? ? console.groupEnd();
? ? ? }
? ? ? // ---- 統(tǒng)計(jì)評(píng)論 ----
? ? ? LOGGER.group('統(tǒng)計(jì)評(píng)論');
? ? ? countCommentMap();
? ? ? LOGGER.groupEnd();
? ? ? G.done = true;
? ? }
? ? commentPrompt();
? };
? // [保存日志]
? const saveLog = () => {
? ? save(LOG.join('\r\n') + '\r\n', `日志_${INFO.type}_${INFO.oid}_${DRAW_TIME}.txt`, 'text/plain');
? };
? // [重新打亂用戶順序]
? const shuffle = () => {
? ? if (G.done) {
? ? ? G.dIndex = 0;
? ? ? weightedRandomShuffle();
? ? ? console.warn('成功重新打亂用戶順序,控制臺(tái)輸入%c draw(n) %c(n為正整數(shù)) 并回車以重新隨機(jī)抽取n位用戶。', `color:${COLOR.ORANGE}`, '');
? ? } else {
? ? ? commentPrompt();
? ? }
? };
? // [隨機(jī)抽取用戶]
? const draw = async (num = 1) => {
? ? await wait(0);
? ? if (G.done) {
? ? ? if (!Number.isInteger(num) || num <= 0) {
? ? ? ? throw `用戶數(shù)量[${num}]必須為正整數(shù)`;
? ? ? }
? ? ? // ---- 隨機(jī)抽取用戶 ----
? ? ? LOGGER.group(`隨機(jī)抽取用戶(${num})`);
? ? ? let count = num;
? ? ? try {
? ? ? ? while (count > 0) {
? ? ? ? ? if (G.dIndex === G.uList.length) {
? ? ? ? ? ? throw '無(wú)剩余有資格用戶';
? ? ? ? ? }
? ? ? ? ? LOGGER.group(`%c第${G.dIndex + 1}名`, `color:${COLOR.BLUE}`);
? ? ? ? ? const user = G.uList[G.dIndex];
? ? ? ? ? await displayUser(user);
? ? ? ? ? LOGGER.groupEnd();
? ? ? ? ? count--;
? ? ? ? ? G.dIndex++;
? ? ? ? }
? ? ? } finally {
? ? ? ? LOGGER.groupEnd();
? ? ? }
? ? ? console.warn(`成功隨機(jī)抽取${num}位用戶,控制臺(tái)輸入%c draw(n) %c(n為正整數(shù)) 并回車以繼續(xù)隨機(jī)抽取n位用戶。`, `color:${COLOR.ORANGE}`, '');
? ? } else {
? ? ? commentPrompt();
? ? }
? };
? // [查找用戶]
? const search = key => {
? ? if (G.done) {
? ? ? console.group(`查找用戶[${key}]`);
? ? ? let res = G.uMap.get(key);
? ? ? if (res === undefined) {
? ? ? ? for (const [uid, user] of G.uMap) {
? ? ? ? ? if (user.name === key) {
? ? ? ? ? ? res = user;
? ? ? ? ? ? break;
? ? ? ? ? }
? ? ? ? }
? ? ? }
? ? ? if (res === undefined) {
? ? ? ? console.warn(`評(píng)論區(qū)不存在用戶[${key}]`);
? ? ? } else {
? ? ? ? const details = Array.from(res.details.values()).sort(comparator(detail => detail.ts, detail => detail.rpid));
? ? ? ? console.table([res].map(mapToUser));
? ? ? ? console.table(details.map(mapToDetail));
? ? ? }
? ? ? console.groupEnd();
? ? } else {
? ? ? commentPrompt();
? ? }
? };
? // ---- 窗口管理 ----
? globalConfig();
? globalFunctions(resume, saveLog, shuffle, draw, search);
? const G = {call: 1, next: 0, done: false, uMap: new Map(), uWeight: 0, uList: [], dIndex: 0};
? // ---- 當(dāng)前頁(yè)面 ----
? LOGGER.group('當(dāng)前頁(yè)面');
? const INFO = computeCommentInfo();
? const USER_NAME = await getUser();
? LOGGER.groupEnd();
? // ---- 運(yùn)行配置 ----
? LOGGER.group('運(yùn)行配置');
? const DRAW_TIME = Math.trunc(Date.now() / 1000);
? LOGGER.log(`[開獎(jiǎng)時(shí)間] %c${formatTime(DRAW_TIME)}`, `color:${COLOR.BLUE}`);
? if (USER_NAME === undefined) {
? ? LOGGER.log('[當(dāng)前登錄用戶] %c未登錄', `color:${COLOR.GRAY};font-weight:bold`);
? ? console.warn('登錄后可自動(dòng)驗(yàn)證中獎(jiǎng)用戶是否關(guān)注當(dāng)前登錄用戶(需刷新頁(yè)面并重新運(yùn)行開獎(jiǎng)腳本)。');
? } else {
? ? LOGGER.log(`[當(dāng)前登錄用戶] %c${USER_NAME}`, `color:${COLOR.PINK};font-weight:bold`);
? }
? if (USER_CONFIG.SAVE_COMMENTS) {
? ? LOGGER.log('[本地保存所有評(píng)論] %c保存', `color:${COLOR.GREEN}`);
? } else {
? ? LOGGER.log('[本地保存所有評(píng)論] %c不保存', `color:${COLOR.RED}`);
? }
? if (USER_CONFIG.GET_COMMENT_IP) {
? ? LOGGER.log('[獲取評(píng)論IP歸屬地] %c獲取', `color:${COLOR.GREEN}`);
? ? if (USER_NAME === undefined) {
? ? ? console.warn('登錄后才能獲取評(píng)論IP歸屬地(需刷新頁(yè)面并重新運(yùn)行開獎(jiǎng)腳本)。');
? ? }
? } else {
? ? LOGGER.log('[獲取評(píng)論IP歸屬地] %c不獲取', `color:${COLOR.RED}`);
? }
? LOGGER.log(`[單用戶累計(jì)評(píng)論數(shù)上限] ${USER_CONFIG.MAX_REPEAT}`);
? LOGGER.log(`%c[API請(qǐng)求間隔] ${SYS_CONFIG.API_INTERVAL}毫秒`, `color:${COLOR.GRAY}`);
? const LEVEL_WEIGHTS = computeWeights('用戶等級(jí)', STYLE.LEVEL, USER_CONFIG.LEVEL_WEIGHT);
? const MEDAL_WEIGHTS = computeWeights('勛章等級(jí)', STYLE.MEDAL, USER_CONFIG.MEDAL_WEIGHT, 4);
? const VIP_WEIGHTS = computeWeights('會(huì)員類型', STYLE.VIP, USER_CONFIG.VIP_WEIGHT);
? LOGGER.groupEnd();
? // ---- 運(yùn)行確認(rèn) ----
? console.group('運(yùn)行確認(rèn)');
? if (!confirm(`確認(rèn)在 本頁(yè)面評(píng)論區(qū) (按照控制臺(tái)所示配置) 開獎(jiǎng)?`)) {
? ? throw '已取消';
? }
? console.log('%c已確認(rèn)', `color:${COLOR.GREEN}`);
? console.groupEnd();
? // ---- 加載與統(tǒng)計(jì)評(píng)論 ----
? await resume();
})();