之前那段时间,我在浏览 B 站的时候偶然发现了一个叫“我的世界小站”的新功能。说是小站,其实它并不是一个独立的网站,而是嵌套在 B 站整体框架下的一个社区模块。当时舆论场上不少人都把它看作是 MCBBS 的替代品,因为那个老牌的我的世界中文论坛在前几年已经黯然倒闭了,很多 MC 玩家都流离失所,四处寻找新的聚集地。可是当我真正点进去体验了一番之后,心里却冒出了不一样的想法——我觉得 MC 玩家群体真正缺失的根本不是另一个论坛,而是那些由国人制作的各种优质资源,比如皮肤、材质包、模组、地图存档,还有能够一起联机、一起探险、一起建房子的玩伴。论坛这种形式在如今这个快节奏的互联网时代,其实已经显得有些古老和笨重了,光靠一个论坛很难把散落在各处的玩家重新凝聚起来。

尤其让我在意的是地址栏的样式,怎么看都像是类贴吧的那种结构,一层套一层的。当我偶然翻到其他小站的地址,发现里面确实有真人在活跃的时候,这种感觉就更强烈了。原本我觉得小站应该是一个独立自主的网站,有自己的域名、自己的页面风格、自己的运营规则,可现在它给我的整体感觉更像是闹得热热嚷嚷的,表面上看起来声势浩大,实际上所有人都挤在一个大屋子里,连个像样的隔间都没有。和贴吧那种个人论坛比起来,贴吧好歹每个吧都有自己的吧规、吧务团队和独特的文化氛围,可B站搞的这个小站反而显得更加寒酸了,连基本的个性化定制都做得很有限,像是流水线上批量生产出来的产品,少了些人情味和归属感。

我的朋友小芬之前特意在 B 站私信里艾特我,让我去看看这个小站长什么样。我当时看完之后给她的评价很简单,就是“不懂但是尊重”——这四个字概括了我所有的感受。我看不懂为什么 B 站要花力气做这样一个功能,也看不懂这个小站到底解决了什么痛点,但我尊重那些在里面玩得开心的玩家,毕竟每个人都有自己选择聚集地的自由。可是经过上面那一番关于论坛和资源的思考之后,我忽然对一件事产生了强烈的好奇心:B 站到底做了多少个小站?如果一个个手动点进去看,且不说网络请求可能会出现问题,光是那无穷无尽的数字编码,看到天荒地老都不一定能看完。更麻烦的是,我还需要知道每个数字编码到底对应着什么样的小站,这显然不能靠手工一个个去试。

作为一个技术爱好者,我骨子里就习惯用代码的方式去解决问题,这种批量遍历的事情根本难不倒我。稍微观察了一下小站的地址格式,我就发现它的规律非常简单,就是主站地址加 /bubble/home/1 这样的模式,末尾的数字从 1 开始依次递增。我只需要写一段程序,把末尾的数字不断改大,然后依次访问每一个地址,就能知道总共有多少个小站。只需要提取 div.name 元素,就可以知道每个小站的名字是什么了。这个思路简单直接,没有任何花里胡哨的地方,完全可以用自动化脚本来实现。想到这一点,我立刻打开了电脑,准备用我最熟悉的方式来搞定这件事,毕竟在这种重复性的工作面前,人的耐心永远比不上机器的效率。

知道了地址规律之后,我立刻动手,使用 DeepSeek TUI 编写了一个简单的爬虫脚本。为了不被 B 站的反爬机制拦截,我特意给脚本加上了 Firefox 浏览器的 UA,让它假装自己是 Windows 用户。可是脚本跑起来之后,结果却让我大失所望——控制台打印出来的内容完全不是我预想中的那种整齐的HTML结构。这说明这个小站网站并不是传统的动态网站,那种网站是后端直接把完整的HTML吐给浏览器的,不需要额外的渲染步骤。相反,它很可能使用了现代化的前端框架,比如 Vue 或者 Next.js,这些框架采用的是客户端渲染的方式,页面骨架先加载出来,具体的内容要靠 JavaScript 再去请求接口然后动态填充,普通的HTTP请求根本拿不到渲染后的完整内容。

既然知道了网站可能是用现代化 APP 引擎做的,需要 JS 执行完毕后才能看到完整的内容结构,但同时我也注意到一个关键的信息——我们在浏览器的开发者工具控制台里是能够看到完整的 HTML 结构的。这说明页面确实是有内容的,只是那些内容不是直接返回给原始 HTTP 请求的,而是需要浏览器内核真正去执行 JS 之后才会出现在 DOM 树里。AI 助手很快帮我分析了这个情况,提醒我可以直接使用一个完整的浏览器内核去完成这个任务,让程序像真人一样打开浏览器、加载页面、等待 JS 执行完毕,然后再从渲染好的页面里提取需要的信息。它向我推荐了 Playwright 这个工具,说这个库最近非常热门,功能强大而且文档齐全,于是我很快就写好了下面这段代码:

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
#!/usr/bin/env python3
# Bilibili 小站爬虫,使用无头 Chromium 渲染 JS 动态页面,提取 div.name 元素。每隔 0.5 秒请求一次,超时自动停止。

import os
import sys
import time
from pathlib import Path

from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

# 浏览器内核安装在本项目目录下,与系统隔离
PROJECT_ROOT = Path(__file__).resolve().parent
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(PROJECT_ROOT / ".playwright-browsers")

# Win10 Firefox User-Agent(即使内核是 Chromium,UA 按要求伪装)
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) "
"Gecko/20100101 Firefox/136.0"
)

REQUEST_INTERVAL = 0.5 # 两次请求之间的间隔(秒)
GOTO_TIMEOUT = 15_000 # 页面导航超时(毫秒)
SELECTOR_TIMEOUT = 10_000 # 等待 div.name 出现的超时(毫秒)


def main():
count = 0

print("开始爬取 Bilibili bubble home 页面(Playwright 引擎)...")
print(f"UA: {USER_AGENT}")
print(f"请求间隔: {REQUEST_INTERVAL}s,遇到 HTTP 404 自动停止\n")

with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(user_agent=USER_AGENT)

try:
while True:
target = count + 1
url = f"https://www.bilibili.com/bubble/home/{target}"
page = context.new_page()

try:
resp = page.goto(
url,
wait_until="domcontentloaded",
timeout=GOTO_TIMEOUT,
)

# 等待 Vue 渲染完成,div.name 出现在 DOM 中
try:
page.wait_for_selector(
"div.name",
timeout=SELECTOR_TIMEOUT,
)
name_elem = page.query_selector("div.name")
name_text = name_elem.inner_text().strip() if name_elem else ""
if name_text:
print(f"号码 {target}{name_text}")
else:
print(f"号码 {target} div.name 为空")
except PlaywrightTimeout:
print(f"号码 {target} 等待超时,未找到 div.name 元素")
break

except Exception as exc:
print(f"[错误] 号码 {target}: {exc}", file=sys.stderr)

finally:
page.close()

count += 1
time.sleep(REQUEST_INTERVAL)

except KeyboardInterrupt:
print("\n用户中断。")
finally:
browser.close()
print(f"\n爬取结束,共访问了 {count} 个页面。")


if __name__ == "__main__":
main()

关于代码开源的事情,有人可能会问我为什么不把这个脚本上传到 GitHub 上,让更多人受益。答案其实很简单,就是我懒得写双语版的 README 文档。上次我做的那个邮件提醒文章的项目,双语版的 README 从头到尾都是 AI 帮我翻译和润色的,我本人的英语水平可以说是相当炸裂,写个中文说明还行,一碰到英文就头皮发麻。除了脚本本身之外,还需要一个依赖文件,就是那个 requirements.txt,里面列了一长串第三方库,这些库各自负责不同的功能,有的是做 HTML 解析的,有的是处理网络请求的,其中最核心的就是 Playwright,它是整个爬虫的发动机:

1
2
3
4
5
6
7
8
9
10
11
beautifulsoup4==4.14.3
certifi==2026.5.20
charset-normalizer==3.4.7
greenlet==3.5.1
idna==3.17
playwright==1.60.0
pyee==13.0.1
requests==2.34.2
soupsieve==2.8.4
typing_extensions==4.15.0
urllib3==2.7.0

这个脚本的运作模式其实非常简单直接,说白了就是用一个下载好的浏览器内核去批量地、自动化地访问一个个小站的地址。脚本会从数字 1 开始,依次访问每个数字对应的小站页面,等待页面加载完毕,然后从渲染好的 HTML 中提取出 div.name 元素里的文字内容。如果某个页面加载超时了,脚本就会自动停止,不再继续往后访问,因为小站的编号是连续的,一旦遇到不存在的编号就不会再有后面的了,这个超时机制实质上就替代了标准的 HTTP 404 状态码的判断逻辑。我这里使用的是 Chromium 浏览器内核,和我日常使用的 Brave 浏览器内核差不多。如果需要安装这个内核的话,需要配置好环境变量,把浏览器的安装路径指向当前项目目录下:

1
2
export PLAYWRIGHT_BROWSERS_PATH = $PWD/.playwright-browsers
.venv/bin/playwright install chromium

根据当前脚本的运行结果,B 站目前一共有 72 个小站。号码第一的是艾尔登法环小站,从内容能看出来法环的玩家群体确实很活跃;排在第二位的是 Switch 小站,任天堂的粉丝们也有了自己的聚集地;而排在最后一个也就是第 72 位的,是一个叫佛学学习分享的小站,这个倒是挺出乎意料的,谁能想到 B 站的小站里还有一个专门讨论佛学的地方呢。这些小站的名称在输出结果中都没有带空格,比如“艾尔登法环小站”就是连续的七个字,“Switch小站”也是连续的,中间没有任何多余的空格或标点符号。既然输出的格式已经够整齐了,我也就不打算再花时间去修改日志的打印格式了,保持现状就挺好的,反正核心的信息都已经完整地呈现在了控制台上:

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
开始爬取 Bilibili bubble home 页面(Playwright 引擎)...
UA: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
请求间隔: 0.5s,遇到 HTTP 404 自动停止

号码 1 是艾尔登法环小站
号码 2 是Switch小站
号码 3 是明末:渊虚之羽小站
号码 4 是解限机小站
号码 5 是Steam小站
号码 6 是凡人修仙传动画小站
号码 7 是轨迹小站
号码 8 是星露谷物语小站
号码 9 是漫展小站
号码 10 是逃离鸭科夫小站
号码 11 是明日方舟:终末地小站
号码 12 是葬送的芙莉莲小站
号码 13 是新三国宇宙·天翊元年小站
号码 14 是鸣潮小站
号码 15 是Fate小站
号码 16 是咒术回战小站
号码 17 是OpenClaw小站
号码 18 是洛克王国:世界小站
号码 19 是宝可梦Pokopia小站
号码 20 是特摄小站
号码 21 是影游小站
号码 22 是月亮计划小站
号码 23 是JOJO的奇妙冒险小站
号码 24 是大巴扎小站
号码 25 是魔法少女的魔女审判小站
号码 26 是模型小站
号码 27 是金牌得主小站
号码 28 是光之美少女小站
号码 29 是东方Project小站
号码 30 是火凤燎原小站
号码 31 是海贼王小站
号码 32 是Undertale小站
号码 33 是燕云十六声小站
号码 34 是F1小站
号码 35 是凹凸世界小站
号码 36 是Re:从零开始的异世界生活小站
号码 37 是王者荣耀世界小站
号码 38 是BanGDream小站
号码 39 是柚子社小站
号码 40 是AI创作互助小站
号码 41 是虹猫蓝兔小站
号码 42 是EVA小站
号码 43 是人格测试小站
号码 44 是GIRLS BAND CRY小站
号码 45 是A宅小站
号码 46 是音游小站
号码 47 是H萌小站
号码 48 是战争雷霆小站
号码 49 是黑袍纠察队小站
号码 50 是我的世界小站
号码 51 是DELTARUNE小站
号码 52 是塞尔达传说小站
号码 53 是实习交流小站
号码 54 是泰拉瑞亚小站
号码 55 是游戏王小站
号码 56 是杀戮尖塔小站
号码 57 是AI开发者小站
号码 58 是后室小站
号码 59 是NBA小站
号码 60 是MyGO_Mujica小站
号码 61 是KPOP小站
号码 62 是世界杯小站
号码 63 是守望先锋小站
号码 64 是Dota小站
号码 65 是反恐精英小站
号码 66 是光遇小站
号码 67 是克苏鲁小站
号码 68 是高考互助小站
号码 69 是恋与深空小站
号码 70 是音MAD小站
号码 71 是生化危机小站
号码 72 是佛学学习分享小站
号码 73 等待超时,未找到 div.name 元素

爬取结束,共访问了 72 个页面。

看了上面这份小站名单,不知道有没有你喜欢的那个圈子呢?如果你喜欢这个脚本,觉得它对你探索 B 站小站有帮助的话,不妨把这个博客地址收藏起来,等以后什么时候想再看看小站的变化,或者想自己跑一遍脚本看看有没有新增的小站,都可以随时翻出来用。如果你对这个脚本有什么改进的建议,或者在使用过程中遇到了什么问题想要评论的话,请记得在留言的时候填入你的名字和邮箱。这算是一个小小的忠告,因为如果没有留下邮箱地址的话,我即使想回复你的评论也没有办法通知到你,到时候你可能就收不到回复提醒了,那就太可惜了。希望这个小工具能帮到你,也欢迎你和我交流更多关于爬虫技术的心得。