惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
博客园 - 【当耐特】
量子位
Engineering at Meta
Engineering at Meta
博客园_首页
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
V
Visual Studio Blog
小众软件
小众软件
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
Jina AI
Jina AI
The Cloudflare Blog
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
博客园 - 聂微东

他说

只因一次笔误:一次笔误让我误以为他是女生,好感竟成了困局 表哥家在装修:Win10 金彩色系统,搭配 Minecraft 光线追踪 服务器的长途跋涉:我的世界服务器探索,飞到世界尽头建领地 一场作弊的高考:高考前重返学校,发现同学都在作弊改答案 后来如何了:辍学以后天天在家打游戏,母亲在超市笑着失落 那个外星女友的故事:我在平流层遇险,外星女友却失联两个月 论网恋的危害性:网恋女友身亡又复活,变声骗走老哥巨额钱财 知乎邀请函后续:面对知乎批量邀请,独立博客作者如何选择 被知乎的运营邀请:收到知乎官方邀请,我却选择坚守独立博客 博客宇宙后续:AI 把博客标题解析成两个域名,凌晨修复友链 貌似网站名字错了:博客宇宙友链出错,生造域名站名不显示 还有翻案环节:Dash to Dock 翻译之争,到看开源社区草台班子 吃药缓解一点了:感冒自己买药扛过来,想起当年学校中考跑步 如果有公共功能就好了:想约人打游戏很尴尬,应该有公告功能 暑假娱乐好无聊:信息茧房塌了,博客圈也全是 SEO 垃圾文 有时候运气真够透的:被抖音信息茧房反噬,过时技术推荐真烦 那篇文章的评论者:投影钟博文未发布,陌生访客已摸进局域网 大家要注意身体啊:楼下雪糕让我身体掏空,夏季食品安全需警惕 服务器开荒第三期:我的世界服务器遭遇强盗,黑暗森林生存法则 我和好朋友讨论龙骑:梦回校园重温特摄友情,却终究只是一场梦 记参加文字园活动:一篇有瑕疵的 Git 文章,入选文字园让我心虚 再审视 Windows 设计:从 Win11 的广告沼泽,逃向 UNIX 的清净世界 张雪和张雪峰的一字之差:张雪峰去世背后,就业选择与教育反思 被 A 社吓哭了:Claude Code 隐私检测,让我果断卸载并回归开源 服务器开荒第二期:给邻居送告示牌,扩建地下室发现蜘蛛刷怪笼 朋友邀请我玩服务器:我的世界服务器开荒,冰原遇险草原中安家 微信账单同步日历:使用 Python 脚本将支付记录导出到苹果日历 楼上装修真是烦死了:被楼上装修噪音追着跑,躲进音乐也逃不掉 为什么我喜欢巴巴爸爸:从漫画到动画,粉色棉花糖的奇遇记 他遇到的伤心事:诚哥的时空穿越奇遇,秘密与校园午后秘密誓言
批量拉黑 B 站评论区账号:评论区太乱?这个脚本帮你批量拉黑
梁栋烨 · 2026-07-01 · via 他说

我真的很喜欢在 B 站上看海绵宝宝,那些无厘头的幽默总能让我忘记一天的疲惫。但不知道从什么时候开始,我的首页推荐流里混进了越来越多奇奇怪怪的东西——二次元游戏圈的党争、键政圈的对线、小鬼圈的烂梗,这些东西像牛皮癣一样甩都甩不掉。每次我只是想安安静静看个视频,评论区却总有人在吵架、钓鱼、刷屏。我尝试过点“不感兴趣”,也试过屏蔽某些关键词,但算法似乎总能找到新的方式来恶心我。说实话,B 站让我挺失望的,那个曾经充满二次元二创、鬼畜调音和经典电视剧的地方,现在变得越来越嘈杂了。

我也不是没想过反抗。B 站其实提供了一个“拉黑”功能,只要把某个用户加入黑名单,你就再也看不到他的评论、私信和动态了——相当于这个人从你的 B 站世界里彻底消失。这本来是个很好的隔离手段,但问题在于:当你看到一条令人不适的评论时,你已经读完了它的内容,已经生气了。在这种情况下,很少有人能心平气和地再点进“更多”菜单、找到“加入黑名单”、再确认一遍弹窗。而且就算你真的拉黑了这个人,你的情绪已经被破坏了,拉黑本身并不能让你高兴起来。更别提那些视频底下动辄几百上千条评论,一个一个手动拉黑根本不现实。

所以我写了一个脚本,专门解决这个问题。它叫做 Bilibili Comment Blacklist Tool,这是项目地址。这个工具的原理很简单:打开一个 B 站视频页面,自动滚动评论区,找到每条评论的“更多”按钮,点击“加入黑名单”,然后确认弹窗——全程自动化,你只需要在命令行里看着它运行就行了。最关键的是,这个过程中你只会看到被拉黑用户的昵称和 UID,完全不会看到评论内容本身。也就是说,你可以批量清理一个视频底下所有评论者,而不会在这个过程中被任何一条评论恶心到。等脚本跑完,这个视频底下的人就跟你的 B 站世界再也没有关系了:

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500

"""
哔哩哔哩评论区批量拉黑工具

DOM 结构(关键):
div#body ← 每条评论的根容器(Light DOM)
a#user-avatar[data-user-profile-id] ← 用户 UID
div#main
div#header
bili-comment-user-info ← Shadow DOM → #user-name > a(用户名)
div#content
bili-rich-text ← Shadow DOM → #contents(评论正文)
div#footer
bili-comment-action-buttons-renderer ← Shadow DOM
div#more
button ← 「更多」按钮
bili-comment-menu ← Shadow DOM → ul#options > li(菜单项)

嵌套 Shadow DOM 穿透策略:
- 用 Playwright 的 >> 组合器找到 Shadow DOM 内的按钮
- 用 JS evaluate + getRootNode().host 从按钮跨越 Shadow DOM 边界到 Light DOM
- 用 JS evaluate + shadowRoot 穿透 bili-comment-menu 的 Shadow DOM 点击菜单项

使用方式:
source venv/bin/activate
python main.py
"""

import asyncio
import random
import re
import sys
import time
from typing import Optional

from playwright.async_api import async_playwright, Page







COOKIE_RAW = """

"""


VIDEO_URL = ""






def parse_cookie_string(raw: str) -> list[dict]:
"""
解析用户粘贴的 Cookie 字符串,转为 Playwright 格式。

支持格式:
1. 浏览器 DevTools 复制: "name1=value1; name2=value2; ..."
2. Netscape 导出格式(多行,含 \t 分隔符)
"""
cookies = []
domain = ".bilibili.com"

if "\n" in raw.strip():
for line in raw.strip().split("\n"):
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) >= 7:
name, value = parts[5], parts[6]
elif "=" in line:
name, value = line.split("=", 1)
else:
continue
cookies.append({
"name": name.strip(),
"value": value.strip(),
"domain": domain,
"path": "/",
"httpOnly": False,
"secure": True,
"sameSite": "Lax",
})
else:
for pair in raw.split(";"):
pair = pair.strip()
if "=" not in pair:
continue
name, value = pair.split("=", 1)
cookies.append({
"name": name.strip(),
"value": value.strip(),
"domain": domain,
"path": "/",
"httpOnly": False,
"secure": True,
"sameSite": "Lax",
})

if not cookies:
print("⚠️ 未解析到任何 Cookie,请检查输入格式。")
sys.exit(1)

print(f"✅ 解析到 {len(cookies)} 个 Cookie")
return cookies






async def get_comment_info(button) -> dict:
"""
从「更多」按钮出发,跨越 Shadow DOM 边界到 Light DOM,
提取评论的用户 UID 和用户名。

返回: {"uid": str, "username": str}
"""
info = await button.evaluate('''
(btn) => {
// Step 1: 从按钮所在的 Shadow DOM 跳到 Light DOM
const actionButtons = btn.getRootNode().host;
if (!actionButtons) return {uid: null, username: '未知'};

const footer = actionButtons.parentElement;
if (!footer) return {uid: null, username: '未知'};

const main = footer.parentElement;
if (!main) return {uid: null, username: '未知'};

const body = main.parentElement;
if (!body) return {uid: null, username: '未知'};

// Step 2: 从 div#body 的子元素中获取用户信息
const avatar = body.querySelector(':scope > a#user-avatar');
const profileId = avatar ? avatar.getAttribute('data-user-profile-id') : null;

// Step 3: 穿透 bili-comment-user-info 的 Shadow DOM 获取用户名
const userInfo = body.querySelector('bili-comment-user-info');
let username = '未知';
if (userInfo && userInfo.shadowRoot) {
const nameLink = userInfo.shadowRoot.querySelector('#user-name a');
if (nameLink) {
username = nameLink.textContent.trim();
}
}

// Step 4: 回退 — 用 bili-rich-text 中的评论内容前20字做标识
let snippet = '';
const richText = body.querySelector('bili-rich-text');
if (richText && richText.shadowRoot) {
const contents = richText.shadowRoot.querySelector('#contents');
if (contents) {
snippet = contents.textContent.trim().substring(0, 30);
}
}

return {
uid: profileId || ('fallback-' + snippet),
username: username,
snippet: snippet,
};
}
''')
return info






async def handle_confirmation_dialog(page: Page, timeout: float = 3.0) -> bool:
"""
等待并自动点击 B 站「确定拉黑」弹窗中的确认按钮。
"""
confirm_selectors = [
"text=确定",
"text=确认",
"button:has-text('确定')",
"button:has-text('确认')",
"[class*='primary']:has-text('确定')",
"[class*='primary']:has-text('确认')",
".bili-modal button:has-text('确定')",
"[class*='dialog'] button:has-text('确定')",
]

for _ in range(int(timeout * 10)):
for sel in confirm_selectors:
try:
btn = page.locator(sel).first
if await btn.is_visible(timeout=100):
await btn.click()
await asyncio.sleep(0.5)
return True
except Exception:
pass
await asyncio.sleep(0.1)

return False






async def process_one_comment(page: Page, button) -> bool:
"""
处理一条评论的拉黑操作(全程 JS evaluate,跳过 Playwright 可见性检查):

1. 强制设置 CSS 让被隐藏的「更多」按钮可见
2. 点击「更多」按钮
3. 穿透 bili-comment-menu 的 Shadow DOM → 点击「加入黑名单」
4. 处理确认弹窗
"""
try:
clicked = await button.evaluate('''
async (btn) => {
// --- 1. 强制让「更多」按钮及其容器可见 ---
// B 站用 CSS 变量 --bili-comment-hover-more-display 控制显隐,
// 默认 display:none,只有鼠标悬停才出现。
// 直接改 inline style 强制露出。
const moreDiv = btn.closest('div#more');
if (moreDiv) {
moreDiv.style.setProperty('display', 'block', 'important');
moreDiv.style.setProperty('visibility', 'visible', 'important');
moreDiv.style.setProperty('opacity', '1', 'important');
}
btn.style.setProperty('display', 'inline-block', 'important');
btn.style.setProperty('visibility', 'visible', 'important');
btn.style.setProperty('opacity', '1', 'important');
// 等一帧确保样式生效
await new Promise(r => requestAnimationFrame(r));

// --- 2. 点击「更多」按钮 ---
btn.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
await new Promise(r => setTimeout(r, 400));

// --- 3. 在 bili-comment-menu 的 Shadow DOM 中点击「加入黑名单」---
if (!moreDiv) return false;
const menu = moreDiv.querySelector('bili-comment-menu');
if (!menu || !menu.shadowRoot) return false;

const items = menu.shadowRoot.querySelectorAll('li');
for (const li of items) {
if (li.textContent && li.textContent.includes('加入黑名单')) {
li.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
return true;
}
}
return false;
}
''')

if not clicked:
try:
await page.get_by_text("加入黑名单", exact=True).first.click(timeout=2000)
clicked = True
except Exception:
pass

if not clicked:
return False


await asyncio.sleep(0.8)
await handle_confirmation_dialog(page, timeout=2.0)

return True

except Exception as e:
print(f" ❌ 处理出错: {e}")
return False






async def run_blacklist_loop(page: Page):
"""
主循环(适配 B 站拉黑后自动刷新评论的行为):

1. 找到一条未处理的评论 → 拉黑
2. 等待页面自动刷新(该评论消失,新评论顶上)
3. 继续找下一条
4. 当前可见的都处理完了 → 缓慢滚动加载更多
5. 到达底部且无新评论 → 结束
"""
processed_uids: set[str] = set()
total_blacklisted = 0
no_new_streak = 0
scroll_accumulator = 0

print("\n🚀 开始处理评论(拉黑一条 → 页面自动刷新 → 下一条)...\n")

while True:

buttons = await page.locator(
"bili-comment-action-buttons-renderer >> div#more > button"
).all()

found_one = False
for i, button in enumerate(buttons):
try:
info = await get_comment_info(button)
uid = info.get("uid", f"unknown-{i}")
username = info.get("username", "未知")

if uid in processed_uids:
continue


found_one = True
processed_uids.add(uid)

print(f"🔨 [{total_blacklisted + 1}] 正在拉黑: {username} (UID: {uid}) ...")

success = await process_one_comment(page, button)

if success:
total_blacklisted += 1
print(f" ✅ 已拉黑: {username}(累计 {total_blacklisted} 人)")
else:
print(f" ⚠️ 跳过: {username}(菜单点击失败)")


await asyncio.sleep(random.uniform(1.5, 2.5))


break

except Exception:

continue


if found_one:
no_new_streak = 0
continue


no_new_streak += 1

if no_new_streak >= 4:

print("\n⏳ 连续多次未发现新评论,尝试最后滚动...")
await page.evaluate("window.scrollBy(0, 1000)")
await asyncio.sleep(5)


any_new = False
final_buttons = await page.locator(
"bili-comment-action-buttons-renderer >> div#more > button"
).all()
for b in final_buttons:
try:
info = await get_comment_info(b)
if info.get("uid") and info.get("uid") not in processed_uids:
any_new = True
break
except Exception:

continue

if not any_new:
print(f"\n🏁 已完成!共拉黑 {total_blacklisted} 人。")
break
else:
no_new_streak = 0
print(" 发现新评论,继续处理...\n")
continue


scroll_delta = random.randint(300, 500)
current_scroll = await page.evaluate("window.scrollY")
page_height = await page.evaluate("document.body.scrollHeight")

await page.evaluate(f"window.scrollBy(0, {scroll_delta})")

wait_time = random.uniform(2.0, 3.5)
await asyncio.sleep(wait_time)

print(f" 📜 滚动 {scroll_delta}px | 位置 {current_scroll}/{page_height} | 已拉黑 {total_blacklisted} 人")


if current_scroll + scroll_delta >= page_height - 100:
await asyncio.sleep(3)
new_height = await page.evaluate("document.body.scrollHeight")
if new_height <= page_height:
no_new_streak += 1

return total_blacklisted






async def main():
print("=" * 60)
print(" 哔哩哔哩评论区 · 批量拉黑工具")
print("=" * 60)
print()


cookie_str = COOKIE_RAW.strip()
if not cookie_str:
print("❌ 请先在 main.py 顶部的 COOKIE_RAW 中粘贴你的 B 站 Cookie。")
print(" 浏览器 DevTools → Application → Cookies → 全选复制 → 粘贴到三引号之间。")
sys.exit(1)


video_url = VIDEO_URL.strip()
if not video_url:
video_url = input("🔗 请输入视频页面 URL: ").strip()
else:
print(f"🔗 使用配置中的视频 URL: {video_url}")

if not video_url:
print("❌ 未输入视频 URL,程序退出。")
sys.exit(1)


cookies = parse_cookie_string(cookie_str)


print("\n🌐 正在启动无头浏览器...")
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
],
)

context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
),
locale="zh-CN",
)

await context.add_cookies(cookies)

page = await context.new_page()


page.on("dialog", lambda dialog: asyncio.ensure_future(_accept_dialog(dialog)))


print(f"📄 正在加载: {video_url}")
try:
await page.goto(video_url, wait_until="domcontentloaded", timeout=30000)
except Exception:
print("⚠️ 页面加载超时,尝试继续...")
await page.wait_for_timeout(3000)

try:
await page.wait_for_load_state("load", timeout=15000)
except Exception:
pass
print("✅ 页面加载完成")


await asyncio.sleep(3)


await page.evaluate("window.scrollBy(0, 800)")
await asyncio.sleep(3)


try:
total = await run_blacklist_loop(page)
print(f"\n🎉 程序运行完毕,共拉黑 {total} 人。")
except KeyboardInterrupt:
print(f"\n⏹️ 用户中断。")
finally:
await browser.close()
print("👋 浏览器已关闭。")


async def _accept_dialog(dialog):
"""自动接受浏览器原生弹窗"""
try:
await dialog.accept()
except Exception:
pass


if __name__ == "__main__":
asyncio.run(main())

说起来简单,但实际写这个脚本的时候遇到了不少麻烦。首先,B 站的评论组件使用了嵌套的 Shadow DOM,里面包着 bili-comment-menu,常规的 DOM 选择器根本拿不到里面的按钮。其次,B 站的“更多”按钮默认是隐藏的,它通过一个 CSS 变量控制显隐,只有鼠标悬停在评论上才会出现。Playwright 的可见性检查在这种元素上会直接超时。我的解决方案是全部在 JavaScript 层面操作:用 element.evaluate() 强制覆盖 CSS 样式让按钮可见,再用 dispatchEvent 模拟点击事件——即使元素是 display:none 也能正常工作。

另外,每次拉黑一个人之后 B 站会自动刷新评论区,脚本需要实时重新扫描,并且按 UID 去重,避免重复拉黑同一个人。如果你想自己试试这个工具,部署流程并不复杂,但需要一点命令行基础。首先把项目克隆到本地,然后创建 Python 虚拟环境并激活——我推荐用虚拟环境,这样所有依赖都装在项目目录里,不会污染系统。接着安装依赖:

1
2
3
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/playwright install chromium

下载 Playwright 所需的 Chromium 浏览器近 200MB,如果网络好的话不需要等待多久。最关键的一步是配置 Cookie:打开 B 站网页,按 F12 打开开发者工具,复制所有 Cookie(这一步在网络上很多人写教程,可自行向 AI 提问该步骤,或者在搜索引擎搜索“如何提取页面 Cookie”),粘贴到 main.py 里 COOKIE_RAW 的三引号中间。这个 Cookie 包含了你的登录状态,非常重要,千万不要提交到 Git 或者分享给任何人,最后在 main.py 里填上你要处理的视频链接。一切配置好之后,在终端里激活虚拟环境,运行这段命令就行了:

1
.venv/bin/python main.py

脚本会启动一个无头浏览器(你看不到窗口),自动打开视频页面,然后开始缓慢滚动评论区,等待 2 到 3.5 秒让评论加载出来。每找到一条未处理的评论,脚本就会执行拉黑操作,然后 B 站刷新评论区,脚本继续处理下一条。命令行里会实时输出进度。当所有可见评论都处理完后,脚本会继续滚动加载更多,直到到达评论区底部,然后自动退出。整个过程你只需要看着屏幕,什么都不用做。

最后必须强调一点:使用这个脚本有封号风险。因为脚本用到了你的 B 站 Cookie,相当于模拟你在浏览器里的操作,B 站的风控系统可能会检测到异常行为。为了降低风险,我在脚本里加入了随机延迟——操作之间间隔 0.5 到 1.5 秒,滚动间隔 2 到 3.5 秒——尽可能模仿人类的操作节奏。这个工具仅供学习研究使用,我会根据实际需求持续维护,未来计划增加按关键词拉黑、按用户名拉黑等更精细的功能,让大家能更精准地过滤掉不想看到的内容,还大家一个清净的 B 站。欢迎大家收藏和提交反馈。