// ==UserScript==
// @name B站弹幕查询器(查发布者)
// @namespace PyHaoCoder
// @version 1.0
// @description 通过B站视频查询弹幕并查找指定用户
// @author PyHaoCoder
// @icon https://www.bilibili.com/favicon.ico
// @match https://www.bilibili.com/video/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect api.bilibili.com
// ==/UserScript==
(function () {
'use strict';
// 定时器间隔时间(单位:毫秒)
const intervalTime = 1000; // 5秒
// ==================== 获取视频CID ====================
function fetchCID() {
const bvid = location.href.split('/')[4].split('?')[0];
const url = `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}&jsonp=jsonp`
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function (response) {
// 获取页面内容
const pageContent = response.responseText;
const data = JSON.parse(pageContent);
if (data.data) {
// 获取视频CID
window._cid = data.data[0].cid; // 更新全局变量
console.log('获取视频CID:', window._cid);
// 更新 _url
if (!window._url || location.href !== _url) {
window._url = location.href;
}
} else {
console.error('获取视频CID失败');
}
},
onerror: function (err) {
console.log('获取视频CID失败:' + err.statusText);
}
});
}
// 隐藏表格
function hideTable() {
const container = document.getElementById('resultContainer')
container.style.display = "none"
}
// 初始化定时器
function startTimer() {
setInterval(() => {
// 如果 _url 未定义或当前 URL 不等于 _url,则更新 CID
if (window._url && location.href !== window._url) {
fetchCID();
hideTable();
}
}, intervalTime);
}
// 首次初始化
fetchCID();
// 启动定时器
startTimer();
})();
(function () {
'use strict';
// ==================== 哈希转换模块 ====================
window.BiliBili_midcrc = function () {
'use strict';
const CRCPOLYNOMIAL = 0xEDB88320;
const startTime = new Date().getTime(),
crctable = new Array(256),
create_table = function () {
let crcreg,
i, j;
for (i = 0; i < 256; ++i) {
crcreg = i;
for (j = 0; j < 8; ++j) {
if ((crcreg & 1) != 0) {
crcreg = CRCPOLYNOMIAL ^ (crcreg >>> 1);
} else {
crcreg >>>= 1;
}
}
crctable[i] = crcreg;
}
},
crc32 = function (input) {
if (typeof (input) != 'string')
input = input.toString();
let crcstart = 0xFFFFFFFF, len = input.length, index;
for (let i = 0; i < len; ++i) {
index = (crcstart ^ input.charCodeAt(i)) & 0xff;
crcstart = (crcstart >>> 8) ^ crctable[index];
}
return crcstart;
},
crc32lastindex = function (input) {
if (typeof (input) != 'string')
input = input.toString();
let crcstart = 0xFFFFFFFF, len = input.length, index;
for (let i = 0; i < len; ++i) {
index = (crcstart ^ input.charCodeAt(i)) & 0xff;
crcstart = (crcstart >>> 8) ^ crctable[index];
}
return index;
},
getcrcindex = function (t) {
for (let i = 0; i < 256; i++) {
if (crctable[i] >>> 24 == t)
return i;
}
return -1;
},
deepCheck = function (i, index) {
let tc = 0x00, str = '',
hash = crc32(i);
tc = hash & 0xff ^ index[2];
if (!(tc <= 57 && tc >= 48))
return [0];
str += tc - 48;
hash = crctable[index[2]] ^ (hash >>> 8);
tc = hash & 0xff ^ index[1];
if (!(tc <= 57 && tc >= 48))
return [0];
str += tc - 48;
hash = crctable[index[1]] ^ (hash >>> 8);
tc = hash & 0xff ^ index[0];
if (!(tc <= 57 && tc >= 48))
return [0];
str += tc - 48;
hash = crctable[index[0]] ^ (hash >>> 8);
return [1, str];
};
create_table();
const index = new Array(4);
// 单次转换函数
const singleConvert = function (input) {
let ht = parseInt('0x' + input) ^ 0xffffffff,
snum, i, lastindex, deepCheckData;
for (i = 3; i >= 0; i--) {
index[3 - i] = getcrcindex(ht >>> (i * 8));
snum = crctable[index[3 - i]];
ht ^= snum >>> ((3 - i) * 8);
}
for (i = 0; i < 100000000; i++) {
lastindex = crc32lastindex(i);
if (lastindex == index[3]) {
deepCheckData = deepCheck(i, index)
if (deepCheckData[0])
break;
}
}
if (i == 100000000)
return -1;
return i + '' + deepCheckData[1];
};
// 批量转换函数
const batchConvert = function (hashArray) {
return hashArray.map(function (hash) {
return singleConvert(hash);
});
};
return {
singleConvert: singleConvert, // 单次转换
batchConvert: batchConvert // 批量转换
};
};
})();
(function () {
'use strict';
// ==================== 油猴脚本主逻辑 ====================
// 创建UI界面
function createUI() {
const style = `
<style>
.bili-parser-container {
position: fixed;
top: 70px;
right: 20px;
z-index: 9999;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
width: 300px;
cursor: default;
transition: transform 0.1s ease-out; /* 平滑复位效果 */
will-change: transform; /* 提前声明变化属性 */
}
.bili-parser-header {
cursor: move;
padding: 10px 0;
margin: -10px 0 10px;
border-bottom: 1px solid #eee;
}
#resultContainer {
max-height: 400px; /* 设置最大高度 */
overflow-y: auto; /* 添加垂直滚动条 */
margin-top: 0;
display: none;
}
.bili-input {
width: 100%;
padding: 8px 0;
margin: 0 0 8px;
border: 1px solid #ddd;
box-sizing: border-box; /* 确保宽度包括内边距和边框 */
}
#keywordInput.bili-input {
padding-left: 8px;
padding-right: 8px;
}
.bili-btn {
background: #00a1d6;
color: white;
border: none;
padding: 8px 15px;
cursor: pointer;
width: 100%;
box-sizing: border-box; /* 确保宽度包括内边距和边框 */
}
.result-table {
width: 100%;
border-collapse: collapse;
display: block;
}
.result-table td, .result-table th {
border: 1px solid #ddd;
padding: 8px;
font-size: 12px;
}
</style>
`;
const html = `
<div class="bili-parser-container">
<div class="bili-parser-header"><h3>B站弹幕查询器 <span style="float: right;">By: @PyHaoCoder</span></h3></div>
<input type="text" class="bili-input" id="keywordInput" placeholder="输入要查找的关键字">
<button class="bili-btn" id="startSearch">开始搜索</button>
<div id="resultContainer"></div>
</div>
`;
document.body.insertAdjacentHTML('afterbegin', style + html);
// 添加悬浮窗移动功能
addDragFunctionality();
}
// ==================== 添加悬浮窗移动功能 ====================
function addDragFunctionality() {
const container = document.querySelector('.bili-parser-container');
const header = document.querySelector('.bili-parser-header');
let isDragging = false;
let startX, startY, initialX, initialY;
header.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialX = container.offsetLeft;
initialY = container.offsetTop;
// 防止文本选中
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
// 计算新位置(限制在窗口范围内)
const newX = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, initialX + deltaX));
const newY = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, initialY + deltaY));
container.style.left = `${newX}px`;
container.style.right = 'auto';
container.style.top = `${newY}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
});
}
// 获取视频CID
function getVideoCID() {
if (window._cid) {
return window._cid
}
}
// 显示结果
function showResults(comments) {
const container = document.getElementById('resultContainer');
let html = `
<table class="result-table">
<tr>
<th>用户MID</th>
<th>时间</th>
<th>内容</th>
</tr>
`;
// 显示所有数据
comments.forEach(comment => {
// 如果弹幕长度超过 40,则截断并添加“...”
const text = comment.text.length > 40 ? comment.text.substring(0, 40) + '...' : comment.text;
html += `
<tr>
<td><a href="https://space.bilibili.com/${comment.mid}" target="_blank">${comment.mid}</a></td>
<td>${comment.date}</td>
<td>${text}</td>
</tr>
`;
});
html += '</table>';
// 添加“共 n 条数据”提示
html += `
<div style="margin-top: 10px; color: #666; text-align: center;">
共 ${comments.length} 条数据
</div>
`;
container.innerHTML = html;
container.style.marginTop = "10px"
container.style.display = "block"
// 动态调整表格高度和滚动条
const table = container.querySelector('.result-table');
if (table) {
if (table.scrollHeight > 280) {
table.style.maxHeight = '280px';
table.style.overflowY = 'auto'; // 添加垂直滚动条
} else {
table.style.maxHeight = 'none';
table.style.overflowY = 'visible';
}
}
}
// 主逻辑
async function main(keyword) {
const cid = getVideoCID();
if (!cid) {
alert('获取视频信息失败,请刷新页面重试');
return;
} else {
console.log(`开始解析:https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`)
}
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`,
onload: function (response) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(response.responseText, "text/xml");
const ds = xmlDoc.getElementsByTagName('d');
const comments = [];
for (let d of ds) {
const text = d.textContent;
if (keyword && !text.includes(keyword)) continue;
const p = d.getAttribute('p').split(',');
comments.push({
hash: p[6],
ts: parseInt(p[4]) * 1000,
text: text
});
}
// 使用优化后的哈希转换模块
const midcrc = new BiliBili_midcrc();
const midBatch = midcrc.batchConvert(comments.map(comment => comment.hash))
const results = comments.map((comment, idx) => ({
mid: midBatch[idx],
date: new Date(comment.ts).toLocaleString(),
text: comment.text
})).filter(comment => comment.mid); // 过滤无效结果
console.log('解析结果:', results)
showResults(results);
},
onerror: function (err) {
alert('获取弹幕失败:' + err.statusText);
}
});
}
// 初始化
function init() {
createUI();
document.getElementById('startSearch').addEventListener('click', () => {
const keyword = document.getElementById('keywordInput').value.trim();
main(keyword || undefined);
});
}
// 启动脚本
init();
})();