本项目参考了 Wormwaker 的 Jackal Client
众所周知,B站的个性签名是静态的,大多数时候内容只随用户偶尔更新而改变。而Steam好友界面的游戏状态是动态的——你经常能看见右下角时不时弹出来“XXX 正在玩:XXX”。
那有没有办法让B站签名像Steam游戏状态一样动态展现一些东西,比如正在玩的游戏呢?
这就是今天这个玩具做的事情。
这个玩具由三个模块组合而成,实现了从本机读取正在游玩的Steam游戏,到将这个游戏更新到B站的签名中去的过程、

下面会依次讲解每个模块的思路和实现逻辑
从注册表里获取SteamAppID
Steam 在运行游戏时会往 Windows 注册表里写一个值:
HKCU\Software\Valve\Steam\RunningAppID
这个值是当前正在运行的游戏的 AppID。如果你在玩星露谷物语,那AppID就是4313150;如果你在玩火山的女儿,那AppID就是1669980
import winreg
def get_running_steam_appid():
try:
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Valve\Steam"
)
value, _ = winreg.QueryValueEx(key, "RunningAppID")
winreg.CloseKey(key)
if value and value != 0:
return int(value)
return None
except FileNotFoundError:
return None

AppID 变游戏名
拿到App ID还不够,你不能把一串数字贴签名上,你得把它翻译成人类能读的游戏名
Steam 把已安装游戏的名字也写进了注册表:
HKCU\Software\Valve\Steam\Apps\{AppID}\Name
直接读就完事了
import winreg
def lookup_game_name(appid):
if appid is None:
return None
try:
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
rf"Software\Valve\Steam\Apps\{appid}"
)
name, _ = winreg.QueryValueEx(key, "Name")
winreg.CloseKey(key)
return name
except FileNotFoundError:
return None
把游戏名贴到签名上
B站有一个接口可以修改当前登录用户的签名:
POST https://api.bilibili.com/x/member/web/sign/update
需要带上 bili_jct 和 SESSDATA,这两个都可以从浏览器 Cookie 里获取。

签名的格式设计成换行分隔,上面是正在玩的游戏名,下面是原本的签名
正在玩:Wallpaper Engine
音为源于真心
这样关闭游戏时只要删掉第一行就行
import requests
SIGN_MAX_LENGTH = 70 # B站签名字数上限
def update_signature(sessdata, bili_jct, game_name, original_sign):
if game_name:
prefix = f"正在玩:{game_name}"
new_sign = f"{prefix}\n{original_sign}"[:SIGN_MAX_LENGTH]
else:
new_sign = original_sign # 没在玩,恢复原样
resp = requests.post(
"https://api.bilibili.com/x/member/web/sign/update",
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Referer": "https://www.bilibili.com",
},
data={"user_sign": new_sign, "csrf": bili_jct},
cookies={"SESSDATA": sessdata, "bili_jct": bili_jct},
timeout=10,
)
return resp.json().get("code") == 0
主循环:事件驱动
这个玩具现在拥有了”查当前游戏——更新签名“的功能,但是怎样让它能在游戏开启或者关闭的时候自主地完成”查当前游戏“呢?
你当然可以选择每隔一段时间查询一次,但Win32 API 提供了RegNotifyChangeKeyValue。你可以将它理解为,注册表不变时线程直接挂在内核的等待队列里,注册表一旦发生变化立马完成“查当前游戏”。比起轮询,它的好处是CPU 占用显著的低。
import ctypes
from ctypes import wintypes
_advapi32 = ctypes.windll.advapi32
_kernel32 = ctypes.windll.kernel32
def wait_for_registry_change():
# 打开注册表键的监听句柄
raw_hkey = ctypes.c_void_p()
_advapi32.RegOpenKeyExW(
0x80000001, r"Software\Valve\Steam",
0, 0x0010, ctypes.byref(raw_hkey),
)
# 创建 Win32 事件
h_event = _kernel32.CreateEventW(None, True, False, None)
# 注册异步通知:Steam 改值 → 内核唤醒
_advapi32.RegNotifyChangeKeyValue(
raw_hkey, False, 0x00000004, h_event, True,
)
# 挂起等待,每 1 秒超时检查一次 Python 的关闭信号
while True:
ret = _kernel32.WaitForSingleObject(h_event, 1000)
if ret == 0: # 注册表变了
return True
# 超时就返回,让主循环检查关闭标志
return False
主循环变成:
while not shutdown_flag:
if wait_for_registry_change():
appid = get_running_steam_appid()
if appid != last_appid:
last_appid = appid
game_name = lookup_game_name(appid) if appid else None
update_signature(..., game_name, original_sign)
实现游戏启动 → 内核唤醒进程 → 签名改变
游戏关闭 → 内核唤醒进程 → 签名恢复。
开机自启
用注册表 HKCU\Software\Microsoft\Windows\CurrentVersion\Run 加一条启动项,指向一个 .vbs 脚本:
Set fso = CreateObject("Scripting.FileSystemObject")
scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "pythonw.exe """ & scriptDir & "\main.py""", 0, False
最后
B站用的 SESSDATA 是短期令牌,几天到几周后就会过期,但每次过期就手动去浏览器复制新的 SESSDATA 太过繁琐
好在B站登录接口除了返回 SESSDATA ,还会返回一个叫 refresh_token 的长寿令牌。 refresh_token 不会过期,且可以帮我们自动更新 SESSDATA
每次更新流程如下:
检查要不要更新
def _needs_refresh(sessdata, bili_jct):
resp = _session.get(_INFO_URL,
params={"csrf": bili_jct},
cookies={"SESSDATA": sessdata})
data = resp.json()
refresh = data["data"].get("refresh", False)
ts = data["data"].get("timestamp", 0)
return refresh, ts
可以每次启动/每隔一小时检查一次
用B站的公钥给时间戳加密
def _generate_correspond_path(timestamp_ms):
cipher = PKCS1_OAEP.new(_RSA_KEY, SHA256)
encrypted = cipher.encrypt(f"refresh_{timestamp_ms}".encode())
return binascii.b2a_hex(encrypted).decode()
提取新的 SESSDATA 和 bili_jct
for chunk in set_cookie.split(", "):
for field in chunk.split("; "):
if field.startswith("SESSDATA="):
new_sessdata = field[len("SESSDATA="):]
elif field.startswith("bili_jct="):
new_bili_jct = field[len("bili_jct="):]

