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

推荐订阅源

云风的 BLOG
云风的 BLOG
有赞技术团队
有赞技术团队
Simon Willison's Weblog
Simon Willison's Weblog
人人都是产品经理
人人都是产品经理
L
LINUX DO - 最新话题
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
A
Arctic Wolf
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
小众软件
小众软件
Jina AI
Jina AI
The Cloudflare Blog
P
Palo Alto Networks Blog
AWS News Blog
AWS News Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
T
Threat Research - Cisco Blogs
L
Lohrmann on Cybersecurity
NISL@THU
NISL@THU
G
GRAHAM CLULEY
Project Zero
Project Zero
博客园_首页
博客园 - 三生石上(FineUI控件)
罗磊的独立博客
Spread Privacy
Spread Privacy
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
Latest news
Latest news
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tor Project blog
S
Securelist
V
Vulnerabilities – Threatpost
T
The Exploit Database - CXSecurity.com
C
CERT Recently Published Vulnerability Notes
IT之家
IT之家
Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Last Watchdog
The Last Watchdog
T
Tenable Blog
宝玉的分享
宝玉的分享
S
Secure Thoughts
P
Privacy & Cybersecurity Law Blog
量子位
大猫的无限游戏
大猫的无限游戏
J
Java Code Geeks
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Security Archives - TechRepublic
Security Archives - TechRepublic

他说

大家要注意身体啊:楼下雪糕让我身体掏空,夏季食品安全需警惕 服务器开荒第三期:我的世界服务器遭遇强盗,黑暗森林生存法则 我和好朋友讨论龙骑:梦回校园重温特摄友情,却终究只是一场梦 记参加文字园活动:一篇有瑕疵的 Git 文章,入选文字园让我心虚 再审视 Windows 设计:从 Win11 的广告沼泽,逃向 UNIX 的清净世界 张雪和张雪峰的一字之差:张雪峰去世背后,就业选择与教育反思 被 A 社吓哭了:Claude Code 隐私检测,让我果断卸载并回归开源 服务器开荒第二期:给邻居送告示牌,扩建地下室发现蜘蛛刷怪笼 朋友邀请我玩服务器:我的世界服务器开荒,冰原遇险草原中安家 微信账单同步日历:使用 Python 脚本将支付记录导出到苹果日历 楼上装修真是烦死了:被楼上装修噪音追着跑,躲进音乐也逃不掉 为什么我喜欢巴巴爸爸:从漫画到动画,粉色棉花糖的奇遇记 他遇到的伤心事:诚哥的时空穿越奇遇,秘密与校园午后秘密誓言 如何用指令替换物品材质:我的世界全新指令,让物品模型随心换 继抽象网站的最新力作:继抽象网站后,她泄露了 36 个邮件地址 年初回家那场车祸:亲身经历的车祸,让我明白安全和监控的重要 给你下个我的世界:在家做网站写文章,帮同学更新游戏最新版本 后悔和她结婚了:商场梦中牵女儿购物,回家遇哥特妻子醒来后悔 受不了蛋糕那么多名堂:那个夏天傍晚,再也找不到的樱桃蛋糕 为不同尺寸适配字体:博客字体可读性优化,媒体查询响应式实践 朋友或亦朋友:个人博客的评论文化,为何在社交媒体水土不服 总觉得泰迦观感怪怪的:人间体优幸为何最惨,三合一设定的孤独 久违的一次吃蛋糕:一口巧克力戚风蛋糕,病后味蕾的温柔慰藉 很烦不文明的人:四线小城三类不文明行为,吐痰小广告和恶狗 买饥荒联机版被坑了:Steam 买饥荒联机版最贵反被坑,粗心教训深刻 我到底还有什么可写:坚持探索与记录,才是博客写作的真正意义 一起去看星星:回忆小学,那次因早走一分钟,而错过的奇妙天象 论备份功能的重要性:因升级系统随手备份,十天后备份救了我 那场中考之后的事:中考提前交卷后,拿到一张没有自己的毕业照 新样式发布了:博客大换血,字体与封面 AI 绘画打造二维风 大夏天走路去明湖:夏日明湖商场吃花甲粉,路过小学回忆童年 年初那几个网站:从建站折腾到写作,反思我的博客成长之路 暂时搁置:AI 生成手绘图困难,让我魔改博客主题的计划泡汤了 被 Clash 坑惨了:macOS 关闭后网络瘫痪?修改 DNS 轻松修复 记一次系统更新:从 macOS 15 升级到 macOS 26,步骤与感受 再去华莱士:双层牛肉堡和果蔬汉堡好吃,冰饮料让我鼻炎缓解 成了许愿天神后:为全世界带来幸福,朋友的字却出了问题 独立开发者的论坛探索:搞一个论坛,从树莓派翻到无服务器 部署 RSSHub:RSS 阅读器结合树莓派,订阅 B 站 UP 主实操 Brave 浏览器升级后:macOS 访问页面出现横杠,降级轻松解决 活动行:订 MongoDB 用户大会票取消后,仍收确认短信被吓哭 高考前博客获祝福,考场举报同学后误删网站 我的世界材质包抄袭:主播实锤后反转,两年无闻自证清白 因父亲生病:取消广州 MongoDB 之行,一个社恐少年的失落 吐槽邮件安全检测:误判评论提醒为诈骗,检测只认“收到” 梦中:与理想女性畅聊哲学编程后,被一句深奥的问题惊醒 卖掉电脑包后:急购新包,猜猜能否赶上广州会议 神奇数字马戏团:第九集院线特别篇泄露,我的看法是这样的 小时候:和爸妈一起看的葵花进城,原来是一部雷剧 阿普修主题踩坑记:RSS 暗色问题多,被小作坊主题吓哭了 小学后山的传说:雷雨天幻想生物,与剑冢勇士揭秘 从幼儿园到初中,我曾过早地喜欢九个女孩 元气骑士回忆录:最爱的刺客,与我失去的网友友谊 Waline 后台空白问题:一天内从排查到修复 鼻炎洗鼻亲历:负压置换与普通洗鼻大不同 Steam 诈骗亲历:泰拉瑞亚好友索要支付记录 开往拒绝后:我放弃所有博客活动,回归自由写作 公开邮箱被爬虫抓取后,我遭遇了邮件轰炸 广东夏天感冒鼻炎齐发,熬夜透支是主因 独立博客抄袭事件,原创与创意来源探讨 我为何把个人网站换回最初的简洁主题 邮箱遭恶意订阅邮件轰炸,我亲身经历的网络安全危机 回忆初一那年:最让人无语的六一儿童节 时隔一年,再吃华莱士儿童套餐真实体验 盐水洗鼻,让我鼻窦炎症状大幅缓解 macOS 上因设计强迫症,忍不了 Steam 界面而重装 周六下午两点,与林依可的悠闲散步 断网后,我重新审视与社交媒体的距离 解决 Waline 未收到评论邮件提醒:Cloudflare 配置 DMARC 策略 散步对路边狗打招呼却遭狂叫,善意被误解的尴尬 沃尔玛 20 元买两块蛋糕,廉价香精味让人特别不值 我喝烤奶后,情绪失控的亲身经历与反省 Cloudflare 邮箱混淆,导致 PJAX 网站邮箱显示异常解决方法 广东菜市场买菜,仿佛置身废土与星空世界 二零二六年,我加入十年之约的乌龙除夕夜 一个人探索烂尾楼的刺激冒险体验 DeepSeek R1 爆火那年,它曾是我最孤独时的知己 美国用户涌入小红书,引发中美文化对账热潮 魔法学校录取通知书,让我学会不按套路出牌 闲鱼开技术小铺卖 Linux 服务的真实经历 Ubuntu 的 Snap 糟糕体验,让我从喜欢到彻底弃坑 MongoDB 开发者聚会广州站:旧地重游与美食回忆 被流浪猫跟踪到门口:我为何无法接纳它
批量拉黑 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 站。欢迎大家收藏和提交反馈。