B 站评论提取脚本及分析提示词

一直没有将音视频当作严肃的信息获取媒介,忽略听视觉从音视频提取信息无疑买椟还珠。局限于「文字」带来的「信息」,显然以文字本身作为载体的内容要高效得多。

新年的内容消费行动建议提到目前 AI 总结类工具已经完全可用,可以反过来,作为了解是否值得阅读的工具。一些知识类、观点类的视频可以提取文字稿利用 AI 总结分析。

一个好的学习方式是找到我们欣赏的人并 hacking 他们公开发表的一切。于是我打算趁着假期对该建议的作者Lunamos执行此建议。

其它方面技术性细节不提。由于 Lunamos 本人,以及观众也在评论区留下不少有趣的内容,我尝试写了个脚本一键提取并交由 AI 筛选。

油猴脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// ==UserScript==
// @name B站视频评论一键复制
// @namespace http://tampermonkey.net/
// @version 0.3
// @description 一键复制 Bilibili 视频评论(用户名 + 内容)
// @match https://www.bilibili.com/video/*
// @grant GM_setClipboard
// @run-at document-idle
// ==/UserScript==

(function () {
"use strict";

/* ---------- 工具函数 ---------- */
function getTextDeep(el) {
if (!el) return "";
if (el.nodeType === Node.TEXT_NODE) {
return el.textContent.trim();
}
return Array.from(el.childNodes)
.map(getTextDeep)
.join(" ")
.replace(/\s+/g, " ")
.trim();
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

/* ---------- 核心提取逻辑 ---------- */
async function extractAllComments() {
const result = [];

// 获取标题和链接
const pageTitle = document.querySelector("title")?.textContent || document.title || "";
const pageUrl = document.querySelector("meta[property='og:url']")?.getAttribute("content") || window.location.href;

if (pageTitle || pageUrl) {
result.push(`标题:${pageTitle.trim()}\n链接:${pageUrl}\n---`); // 放在同一个元素里,避免被双换行隔开太多
}

const biliComments = document.querySelector("bili-comments");
if (!biliComments?.shadowRoot) {
alert("未找到评论容器,请确认在视频播放页");
return result;
}

// 等待评论加载
await sleep(1500);

const threads = biliComments.shadowRoot.querySelectorAll(
"bili-comment-thread-renderer",
);

threads.forEach((thread) => {
if (!thread.shadowRoot) return;

const renderer = thread.shadowRoot.querySelector(
"bili-comment-renderer#comment",
);
if (!renderer?.shadowRoot) return;

// 用户名
let username = "";
const userInfo = renderer.shadowRoot.querySelector(
"bili-comment-user-info",
);
if (userInfo?.shadowRoot) {
const nameBox = userInfo.shadowRoot.querySelector("#user-name");
username = getTextDeep(nameBox);
}

// 评论内容
let content = "";
const contentDiv = renderer.shadowRoot.querySelector("#content");
if (contentDiv) {
const richText = contentDiv.querySelector("bili-rich-text");
if (richText?.shadowRoot) {
const contents = richText.shadowRoot.querySelector("#contents");
content = getTextDeep(contents);
}
}

if (username || content) {
result.push(`${username}${content}`);
}

// 获取当前主评论的回复(replies)
const repliesContainer = thread.shadowRoot.querySelector("#replies bili-comment-replies-renderer");
if (repliesContainer?.shadowRoot) {
const replyRenderers = repliesContainer.shadowRoot.querySelectorAll("bili-comment-reply-renderer");
replyRenderers.forEach((replyRenderer) => {
if (!replyRenderer.shadowRoot) return;

// 回复用户名
let replyUsername = "";
const replyUserInfo = replyRenderer.shadowRoot.querySelector("bili-comment-user-info");
if (replyUserInfo?.shadowRoot) {
const nameBox = replyUserInfo.shadowRoot.querySelector("#user-name");
replyUsername = getTextDeep(nameBox);
}

// 回复内容
let replyContent = "";
const replyRichText = replyRenderer.shadowRoot.querySelector("bili-rich-text");
if (replyRichText?.shadowRoot) {
const contents = replyRichText.shadowRoot.querySelector("#contents");
replyContent = getTextDeep(contents);
}

if (replyUsername || replyContent) {
// 使用缩进表示这是回复
result.push(` - ${replyUsername}${replyContent}`);
}
});
}
});

return result;
}

/* ---------- UI 按钮 ---------- */
function createButton() {
const btn = document.createElement("button");
btn.innerText = "📋 一键复制评论";
Object.assign(btn.style, {
position: "fixed",
top: "80px",
right: "20px",
zIndex: "99999",
padding: "10px 14px",
background: "#fb7299",
color: "#fff",
border: "none",
borderRadius: "8px",
cursor: "pointer",
fontSize: "14px",
boxShadow: "0 2px 8px rgba(0,0,0,.2)",
});

btn.onclick = async () => {
btn.innerText = "⏳ 正在提取...";
const comments = await extractAllComments();

if (!comments.length) {
alert("未提取到评论,请确认评论已加载");
btn.innerText = "📋 一键复制评论";
return;
}

const text = comments.join("\n\n");
GM_setClipboard(text);

btn.innerText = `✅ 已复制 ${comments.length} 条评论`;
setTimeout(() => {
btn.innerText = "📋 一键复制评论";
}, 2000);
};

document.body.appendChild(btn);
}

/* ---------- 初始化 ---------- */
window.addEventListener("load", () => {
setTimeout(createButton, 1000);
});
})();

评论筛选提示词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
你是一个 B 站内容分析师。

下面是一段 Bilibili 视频的评论数据,每条格式固定为:

用户:评论内容

请完成以下任务:

整体分析
用户的主要情绪(正面 / 中性 / 负面)
讨论的核心话题
是否有明显争议或风险
提取有价值的评论

定义「有价值」:
包含实质性观点、有效反馈、建设性建议、补充信息、二次创作线索、典型情绪代表

排除:单纯刷表情、无意义刷屏、「前排」「沙发」「打卡」等低信息量内容

输出格式(必须严格遵守)

标题:XXXX

链接:XXXX

## 一、整体分析

(简要总结)

## 二、有价值的评论

用户:评论内容
用户:评论内容
用户:评论内容

请确保:

「有价值的评论」部分只输出用户和评论原文

不修改、不总结、不缩写评论内容

每行一条,格式统一为:用户:评论