v18.0.0.a1 2026/01/01
Pre-release应该有人注意到 dev-dyn-fp 分支了。这里正进行着许多对模块功能大胆的尝试。目前阶段成品便是这个 pre-release。
总之变化很多,一时半会儿写不完(懒),有时间会在这里补充。主要会以代码示例形式体现。
总之先发出来。Docs
安装:pip3 install bilibili-api-dev。或者通过远程储存库 dev-dyn-fp 分支安装。
现在是写好一份介绍了,但有亿点长。不过首先是发布的版本有点小问题:
一些注意事项
模块 metadata 中遗漏了一项依赖:chompjs,若从 pypi 安装模块需要单独补上。
Major changes
fpgen 支持 in #973
运行前需要安装 fpgen 和 curl_cffi。亦可 pip3 install bilibili-api-dev[fingerprint]。
fpgen 可以通过对应参数生成浏览器指纹。详细信息可见 https://github.com/scrapfly/fingerprint-generator。启用 fpgen 后激活 buvid 提供的指纹信息会采用 fpgen 生成的结果,请求时也会加上 fpgen 生成的请求头。
import json
from pprint import pprint
from bilibili_api import get_buvid, request_log, request_settings, select_client, sync
async def main() -> None:
@request_log.on("REQUEST")
def handle(desc: str, data: dict) -> None:
if (
data["url"]
== "https://api.bilibili.com/x/internal/gaia-gateway/ExClimbWuzhi"
):
payload = data["data"]
pprint(json.loads(json.loads(payload)["payload"]))
select_client("curl_cffi")
request_settings.set_enable_fpgen(True)
request_settings.set_fpgen_args(
{
"strict": True,
"browser": "Firefox",
"os": "Linux",
}
)
await get_buvid()
sync(main())可以发现激活 buvid 使用的 payload 会随着参数产生变化。
展开输出示例
{'03bf': 'https%3A%2F%2Fwww.bilibili.com%2F',
'07a4': 'pl-PL',
'3064': 1,
'34f1': '',
'39c8': '333.1007.fp.risk',
'3c43': {'07a4': 'pl-PL',
'097b': 0,
'0bd0': 16,
'13ab': 'VM0qCTq7dkX8Q5b+',
'1c57': 'undefined',
'2673': 0,
'3b21': 1,
'52cd': [0, 0, 0],
'5766': 24,
'641c': 0,
'6527': 0,
'6aa9': 'Asia/Shanghai',
'6bc5': 'AMD~Radeon HD 3200 Graphics, or similar',
'7003': 1,
'72bd': 0,
'748e': [1920, 1200],
'75b8': 1,
'807e': 1,
'80c9': [['PDF Viewer',
'Portable Document Format',
[['application/pdf', 'pdf'], ['text/pdf', 'pdf']]],
['Chrome PDF Viewer',
'Portable Document Format',
[['application/pdf', 'pdf'], ['text/pdf', 'pdf']]],
['Chromium PDF Viewer',
'Portable Document Format',
[['application/pdf', 'pdf'], ['text/pdf', 'pdf']]],
['Microsoft Edge PDF Viewer',
'Portable Document Format',
[['application/pdf', 'pdf'], ['text/pdf', 'pdf']]],
['WebKit built-in PDF',
'Portable Document Format',
[['application/pdf', 'pdf'], ['text/pdf', 'pdf']]]],
'8a1c': 0,
'a3c1': ['extensions:ANGLE_instanced_arrays;EXT_blend_minmax;EXT_color_buffer_half_float;EXT_float_blend;EXT_frag_depth;EXT_shader_texture_lod;EXT_sRGB;EXT_texture_compression_bptc;EXT_texture_compression_rgtc;EXT_texture_filter_anisotropic;OES_element_index_uint;OES_fbo_render_mipmap;OES_standard_derivatives;OES_texture_float;OES_texture_float_linear;OES_texture_half_float;OES_texture_half_float_linear;OES_vertex_array_object;WEBGL_color_buffer_float;WEBGL_compressed_texture_astc;WEBGL_compressed_texture_etc;WEBGL_compressed_texture_s3tc;WEBGL_compressed_texture_s3tc_srgb;WEBGL_debug_renderer_info;WEBGL_debug_shaders;WEBGL_depth_texture;WEBGL_draw_buffers;WEBGL_lose_context',
'webgl aliased line width range:[1, 2048]',
'webgl aliased point size range:[0.125, 2048]',
'webgl alpha bits:8',
'webgl antialiasing:yes',
'webgl blue bits:8',
'webgl depth bits:24',
'webgl green bits:8',
'webgl max anisotropy:16',
'webgl max combined texture image units:192',
'webgl max cube map texture size:16384',
'webgl max fragment uniform vectors:4096',
'webgl max render buffer size:16384',
'webgl max texture image units:32',
'webgl max texture size:16384',
'webgl max varying vectors:32',
'webgl max vertex attribs:16',
'webgl max vertex texture image units:32',
'webgl max vertex uniform vectors:4096',
'webgl max viewport dims:[16384, 16384]',
'webgl red bits:8',
'webgl renderer:Radeon HD 3200 Graphics, or similar',
'webgl shading language version:WebGL GLSL ES 1.0',
'webgl stencil bits:0',
'webgl vendor:Mozilla',
'webgl version:WebGL 1.0',
'webgl unmasked vendor:AMD',
'webgl unmasked renderer:Radeon HD 3200 Graphics, or '
'similar',
'webgl vertex shader high float precision:23',
'webgl vertex shader high float precision rangeMin:127',
'webgl vertex shader high float precision rangeMax:127',
'webgl vertex shader medium float precision:23',
'webgl vertex shader medium float precision rangeMin:127',
'webgl vertex shader medium float precision rangeMax:127',
'webgl vertex shader low float precision:23',
'webgl vertex shader low float precision rangeMin:127',
'webgl vertex shader low float precision rangeMax:127',
'webgl fragment shader high float precision:23',
'webgl fragment shader high float precision rangeMin:127',
'webgl fragment shader high float precision rangeMax:127',
'webgl fragment shader medium float precision:23',
'webgl fragment shader medium float precision rangeMin:127',
'webgl fragment shader medium float precision rangeMax:127',
'webgl fragment shader low float precision:23',
'webgl fragment shader low float precision rangeMin:127',
'webgl fragment shader low float precision rangeMax:127',
'webgl vertex shader high int precision:0',
'webgl vertex shader high int precision rangeMin:24',
'webgl vertex shader high int precision rangeMax:24',
'webgl vertex shader medium int precision:0',
'webgl vertex shader medium int precision rangeMin:24',
'webgl vertex shader medium int precision rangeMax:24',
'webgl vertex shader low int precision:0',
'webgl vertex shader low int precision rangeMin:24',
'webgl vertex shader low int precision rangeMax:24',
'webgl fragment shader high int precision:0',
'webgl fragment shader high int precision rangeMin:24',
'webgl fragment shader high int precision rangeMax:24',
'webgl fragment shader medium int precision:0',
'webgl fragment shader medium int precision rangeMin:24',
'webgl fragment shader medium int precision rangeMax:24',
'webgl fragment shader low int precision:0',
'webgl fragment shader low int precision rangeMin:24',
'webgl fragment shader low int precision rangeMax:24'],
'a658': ['Abyssinica SIL',
'Caladea',
'Cantarell',
'Cantarell Extra Bold',
'Cantarell Light',
'Cantarell Thin',
'Carlito',
'D050000L',
'DejaVu Sans',
'DejaVu Sans Condensed',
'DejaVu Sans Mono',
'DejaVu Serif',
'DejaVu Serif Condensed',
'Droid Sans',
'Jomolhari',
'Khmer OS Content',
'Khmer OS System',
'Liberation Mono',
'Liberation Sans',
'Liberation Serif',
'Lohit Assamese',
'Lohit Bengali',
'Lohit Devanagari',
'Lohit Gujarati',
'Lohit Gurmukhi',
'Lohit Kannada',
'Lohit Odia',
'Lohit Tamil',
'Lohit Telugu',
'Meera',
'Montserrat Black',
'Montserrat ExtraBold',
'Montserrat ExtraLight',
'Montserrat Light',
'Montserrat Medium',
'Montserrat SemiBold',
'Montserrat Thin',
'Noto Color Emoji',
'Noto Emoji',
'Nuosu SIL',
'OpenSymbol',
'PT Sans',
'PT Sans Bold',
'PT Sans Bold Italic',
'PT Sans Caption',
'PT Sans Caption Bold',
'PT Sans Italic',
'Padauk',
'PakType Naskh Basic',
'Rachana',
'STIX',
'Source Code Pro',
'Source Code Pro Black',
'Source Code Pro ExtraLight',
'Source Code Pro Light',
'Source Code Pro Medium',
'Source Code Pro Semibold',
'Twemoji Mozilla',
'Waree'],
'adca': 'Linux x86_64',
'b8ce': 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 '
'Firefox/128.0',
'bfe9': 'B6LoxMEel7lyBxr2Jb1oNG',
'd02f': '124.04347579762239',
'd52f': 'not available',
'd61f': [1920, 1200],
'ed31': 0,
'fc9d': -480},
'5062': '1767324757487',
'54ef': '{"b_ut":"","home_version":"V8","in_new_ab":true,"ab_version":{"for_ai_home_version":"V8","in_theme_version":"OPEN","enable_web_push":"DISABLE","enable_ai_floor_api":"ENABLE","enable_shortcut_key":"DISABLE","rcmd_timeout_config":"550","home_performance_opt":"default"},"ab_split_num":{"for_ai_home_version":140,"in_theme_version":136,"enable_web_push":15,"enable_ai_floor_api":25,"enable_shortcut_key":140,"rcmd_timeout_config":80,"home_performance_opt":80},"uniq_page_id":"120815316454","is_modern":true}',
'5f45': None,
'654a': '',
'6e7c': '1920x1048',
'8b94': 'https%3A%2F%2Fwww.bilibili.com%2F',
'd402': '',
'db46': 0,
'df35': '5271B777-5863-106E1-923B-8571010D43F2CF55464infoc'}Credential
1. sid
新增对 sid cookie 项的支持。sid 也需要通过登录获取。
2. buvid / bili_ticket
不同于上面两项目前阶段可有可无,此项更改显得更为重要一些。
之前模块自动生成 buvid 会应用在 Api 层和 get_cookies,和 Credential 的信息维护分离。
现在的逻辑是这样:对每个 Credential,若未提供 buvid 则生成 buvid 作为 buvid 值,具体生成过程在 get_cookies 中。
看似没有任何改变,可实际上现在生成的 buvid 会直接写进 Credential 的字段,作为其信息的一部分被维护。而之前版本只是复制一份全局的 buvid 使用,其维护靠全局的 get_buvid 函数。
或者说,现在不同 Credential 会有不同的 buvid。同时,随 buvid 生成过程中生成的 b_nut, b_lsid 和 _uuid 也各有不同。
import asyncio
from bilibili_api import Credential, sync
async def main() -> None:
cred1 = Credential(sessdata="114514", bili_jct="1919810")
cred2 = Credential(sessdata="1919810", bili_jct="114514")
print(await cred1.get_cookies())
await asyncio.sleep(60)
print(await cred2.get_cookies())
sync(main())展开输出示例
{'buvid3': 'B4E3ABF7-3F1D-40D4-7B73-4B66B5DEFEEA73027infoc', 'b_nut': '1767325772', 'b_lsid': '3676222B_19B7CD32B38', '_uuid': '101ACD965-8E2D-EEC2-9614-194B3FFC92F572600infoc', 'buvid4': '8204249F-054F-34C1-0CB2-F20588108CCG73027-026010211-I0F/4cRGxQ6HmQe2mEjAU8Q+SHfD/hipc4afUIkwH5dvVfKFp9DoCpu+DQmbGMg2', 'buvid_fp': '4541f21369b121c3309db1f0557d90d8', 'SESSDATA': '114514', 'bili_jct': '1919810', 'browser_resolution': '1280-665', 'opus-goback': '1'}
{'buvid3': 'B4E3ABF7-3F1D-40D4-7B73-4B66B5DEFEEA73027infoc', 'b_nut': '1767325833', 'b_lsid': 'C7C0D2C7_19B7CD4189D', '_uuid': 'EB863272-C8101-4628-FBB3-371E38D2639133373infoc', 'buvid4': '1BC6C68F-AF7A-48C3-BFBE-E2028F0B350G33593-026010211-I0F/4cRGxQ6HmQe2mEjAU8Q+SHfD/hipc4afUIkwH5dvVfKFp9DoCpu+DQmbGMg2', 'buvid_fp': '85aa4bb50dd5f6198edb4709cf3e2c74', 'SESSDATA': '1919810', 'bili_jct': '114514', 'browser_resolution': '1280-665', 'opus-goback': '1'}过程中一共生成了两次 buvid,分别将结果给到了两个 Credential。如果需要回到以前的模式,可以通过加入设置。运行后会发现两个 Credential 的 buvid 项完全一致,都复制了一个全局的 buvid 值。
from bilibili_api import request_settings
request_settings.set_enable_buvid_global_persistence(True)新版本中 buvid 将成为 Credential 的一部分维护,而非在全局下维护,不同的 Credential 也可以维护不同的 buvid。
同时,部分 API 设计选择性提供 Credential,如果不提供默认使用空白的 Credential(),为减少无意义的网络请求次数,也为了达到在过程中持久化 buvid,将对所有空白 Credential 使用全局维护的一份 buvid (也可以理解将所有的 Credential() 视为相同的一块白板,给予相同的 buvid)。若将上述示例程序改成 cred1, cred2 = Credential(), Credential(),则输出的两份 buvid 仍会保持一致,就是因为此处的机制。
再附一个极其潦草的流程图。
global = Credential(sessdata="global", bili_jct="global")
blank = Credential()
normal = Credential(**)
generate buvid (if not global persistence) normal
| /|\
| (if global persistence)
| |
-----------------------------> global -> blank
过滤器
It does what you think it does.
值得注意的是,此过滤器工作层级在 BiliAPIClient 而非 Api,故可对模块发出的所有请求生效。
例如 request_log 的请求日志目前就是通过此 api 实现的。这里借此演示 api 用法。
import asyncio
from json import JSONDecodeError
from typing import Literal
from bilibili_api import (
BiliAPIClient,
BiliAPIResponse,
BiliFilterFlags,
bangumi,
register_post_filter,
register_pre_filter,
)
def pre_filter(
cnt: int, ins: BiliAPIClient, client: str, on: str, params: dict
) -> tuple[Literal[BiliFilterFlags.CONTINUE], None]:
print(params.get("url", ""))
return BiliFilterFlags.CONTINUE, None
def post_filter(
cnt: int,
ins: BiliAPIClient,
client: str,
on: str,
ret: BiliAPIResponse,
params: dict,
) -> tuple[Literal[BiliFilterFlags.CONTINUE], None]:
print("http", ret.code, end=" bili ")
try:
print(ret.json().get("code", ""))
except JSONDecodeError:
print("null")
return BiliFilterFlags.CONTINUE, None
register_pre_filter(
name="simple_log", func=pre_filter, trigger=lambda client, key: key == "request"
)
register_post_filter(
name="simple_log", func=post_filter, trigger=lambda client, key: key == "request"
)
async def main() -> None:
bgm = bangumi.Bangumi(ssid=36204)
await bgm.get_meta()
asyncio.run(main())展开输出示例
url https://api.bilibili.com/x/frontend/finger/spi_v2
http 200 bili 0
url https://www.bilibili.com
http 200 bili null
url https://api.bilibili.com/x/internal/gaia-gateway/ExClimbWuzhi
http 200 bili 0
url https://api.bilibili.com/pgc/view/web/simple/season
http 200 bili 0
url https://api.bilibili.com/pgc/review/user
http 200 bili 0
每个过滤器都有一个名字,前置过滤器和后置过滤器可以重名,但两个前置/两个后置过滤器不行。实际上可以重名,但 unregister_(pre|post)_filter 是通过名字查找过滤器的,到时候代码作出什么行为模块不做任何担保。
每个过滤器实质上为函数,提供 cnt ins。ins 就是 BiliAPIClient 对象,其中的函数都可以调用,包括但不限于 request set_xxx 之类,再过分点可以把 。ins 改为 self,就成为成员函数了cnt 是一个编号,每一次 BiliAPIClient 函数调用都会有一个编号,可用于区分不同时间的不同函数的调用。剩下的参数 client 是当前选择的请求客户端 (如 httpx aiohttp) on 是当前触发了过滤器的函数名称 (如 request ws_create) 某些地方亦会使用 key 称之。params 和 ret 是当前准备传入调用函数的参数和调用函数当前的返回值。为什么是当前?看到后面就知道了。
过滤器触发有多种方式。一是 client + on,提供两个列表,若请求客户端在第一个列表中,触发函数名在第二个列表中,则触发;二是 trigger,将 client 和 on 作为参数传入一个函数中,若函数返回 True 则触发,支持异步函数。
过滤器函数返回两个值,一个是 BiliFilterFlags 阐述它接下来想做的行为(当然是函数内实现不了的行为,所以才要传递给模块实现),另一个值理论上什么都行,需要配合 BiliFilterFlags。接下来看一下各个 flags:
-
CONTINUE: 继续下一个过滤器,最正常的一集。
-
SET_PARAMS: 设置函数的参数 (仅前置过滤器) 如在 cookies 中添加一些私货,就能使用这个 flag。具体的参数就是第二个过滤器返回值,请返回
**kwargs字典。 -
SET_RETURN: 设置返回值 (仅后置过滤器) 具体的返回值就是第二个过滤器返回值。一个接口你看着不顺眼,马上给它篡改结果,就用这个 flag。
-
EXECUTE_NOW: 直接运行函数 (仅前置过滤器)
-
RETURN_NOW: 直接作为函数返回值返回
这两个 flag 和上面两个类似,但是更加强势,一个改完参数后直接跳转到调用函数运行阶段,不给后面的过滤器修改了,还有一个直接将过滤器返回值返回掉,返回结果也不给后面的过滤器修改了。值得注意第二个 flag 在前置过滤器中也能使用,例如设置一个全局 api 缓存,只要前置过滤器中匹配到了缓存过的响应,就能直接返回而无需进行调用函数的运行。 -
BACK: 回到上一个过滤器
-
SKIP: 跳过下一个过滤器
-
GOTO: 跳到任意一个过滤器 需通过 (async_)get_registered_(pre|post)_filters 查询对应过滤器的下标
这三个过滤器和上面几个不同,是用于不同过滤器间的打架交互。你看一个过滤器不顺眼,就能直接把它跳掉;上一个过滤器很喜欢,也能返回上一个过滤器再执行一遍。值得注意的是,模块并不万能,如果这些 flags 跳来跳去出现safe_sleep情节模块无法识别,务必把控好分寸。上面的几个 flags 也同理,能不花哨尽量不要花哨,一般来讲 CONTINUE 就是最佳选择。
说了这么多 flags 其实能用到的也不会太多(应该吧),总之结合实际情况使用。上面还提到过滤器执行顺序,一是所有过滤器 串联执行,若要并联可使用 asyncio,二是具体执行顺序取决于一个优先级 (priority),在 register_(pre|post)_filter 中设置,模块默认有几个注册器,也有 priority,后文会列出,三是关于获取当前调用函数的所有过滤器及其运行顺序,可通过 (async_)get_registered_(pre|post)_filters 函数获取,返回的是一个有序列表,分别是各个过滤器及其信息。一个问题是不同时间运行 (async_)get_registered_(pre|post)_filters 结果会有差异,或者说 trigger 不同时间运行结果会有差异,有的 trigger 是异步函数,运行可能还需要进行网络请求、文件读写等过程,重复运行可能带来副作用,这个问题的解决且听下回分解请继续往下读。
为让各个过滤器间充分打架交流,模块默认在 ins 下注册了一个 data 字典,使用 cnt 作为键会访问到一个字典,这个字典便能作为一次函数调用的过滤器间独立的数据交换处。每一次函数调用前,模块会创建 ins.data[cnt] = {},函数返回后模块便会销毁 ins.data[cnt],在此期间过滤器就能访问这一个公共字典。拿上述代码举个例子:
import asyncio
from json import JSONDecodeError
from typing import Literal
from bilibili_api import (
BiliAPIClient,
BiliAPIResponse,
BiliFilterFlags,
bangumi,
register_post_filter,
register_pre_filter,
)
def pre_filter(
cnt: int, ins: BiliAPIClient, client: str, on: str, params: dict
) -> tuple[Literal[BiliFilterFlags.CONTINUE], None]:
ins.data[cnt]["log_url"] = params.get("url", "") # type: ignore
return BiliFilterFlags.CONTINUE, None
def post_filter(
cnt: int,
ins: BiliAPIClient,
client: str,
on: str,
ret: BiliAPIResponse,
params: dict,
) -> tuple[Literal[BiliFilterFlags.CONTINUE], None]:
print("url", ins.data[cnt]["log_url"]) # type: ignore
print("http", ret.code, end=" bili ")
try:
print(ret.json().get("code", ""))
except JSONDecodeError:
print("null")
return BiliFilterFlags.CONTINUE, None
register_pre_filter(
name="simple_log", func=pre_filter, trigger=lambda client, key: key == "request"
)
register_post_filter(
name="simple_log", func=post_filter, trigger=lambda client, key: key == "request"
)
async def main() -> None:
bgm = bangumi.Bangumi(ssid=36204)
await bgm.get_meta()
asyncio.run(main())通过 ins.data[cnt],过滤器期间便能交流信息了,甚至是前置与后置过滤器之间也可以。
最后推销介绍一下新增的 global_credential 功能,可以设置一个全局凭据类(和前文中提到的全局凭据类不是一个对象),例如你可以直接把你的账号设置为全局凭据类。然后每次进行请求,只要可以设置 cookies,全局凭据类中的 cookies 都会覆写掉原来参数中提供的 cookies,就相当于是在全局范围内应用了凭据类。这个功能就是通过前置过滤器 + SET_PARAMS 实现的。
附:内置过滤器表(任意请求客户端只要函数名满足都能触发,此处省略)
| name | position | on | priority |
|---|---|---|---|
| __builtin_log_request | pre | request | 998244353 |
| __builtin_log_response | post | request | -998244353 |
| __builtin_log_dwn_create | post | download_create | -998244353 |
| __builtin_log_dwn_chunk | post | download_chunk | -998244353 |
| __builtin_log_dwn_close | post | download_close | -998244353 |
| __builtin_log_ws_create | post | ws_create | -998244353 |
| __builtin_log_ws_recv | post | ws_recv | -998244353 |
| __builtin_log_ws_send | pre | ws_send | 998244353 |
| __builtin_log_ws_close | pre | ws_close | 998244353 |
| __builtin_global_credential | pre | all | 0 |
- 关于 ws_close 和 download_close 一个前置一个后置的问题,我也不记得当时怎么想的了。总之
v18.0.0.a0中是这么写的,至少这是事实。
线程安全优化
同一个 BiliAPIClient 下不同的 ws 连接使用一个自增变量作为编号区分,于是在多线程下就出事了。
最早疑似出现在 #956,这里也使用同时连接多个直播间演示。
注:下列输出示例为脚本于11:运行时的输出。
import asyncio
import logging
from bilibili_api import live, request_log
request_log.set_on(True)
request_log.set_on_events(["WS_CREATE", "WS_RECV", "WS_SEND", "WS_CLOSE"])
request_log.logger.addHandler(logging.FileHandler("test.out"))
async def main() -> None:
live_ids = [33989, 2751313, 21402309, 23611306]
live_rooms = [live.LiveDanmaku(room_display_id=live_id) for live_id in live_ids]
connects = [live_room.connect() for live_room in live_rooms]
disconnects = [live_room.disconnect() for live_room in live_rooms]
try:
await asyncio.gather(*connects)
except (asyncio.CancelledError, KeyboardInterrupt):
await asyncio.gather(*disconnects)
asyncio.run(main())展开输出示例
WS #1 开始 WebSocket 连接: {'url': 'wss://zj-cn-live-comet.chat.bilibili.com:2245/sub', 'headers': {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Referer': 'https://www.bilibili.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept': '*/*', 'Priority': 'u=1, i', 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-site'}, 'params': {}}
WS #1 发送 WebSocket 请求: {'data': b'\x00\x00\x01\xa8\x00\x10\x00\x01\x00\x00\x00\x07\x00\x00\x00\x01{"uid":0,"roomid":23611306,"protover":3,"platform":"web","type":2,"buvid":"03A8EE71-EA2B-49A6-9966-8A64004439B306580infoc","key":"tjTu8PEOrDKHlt6K2Ofz4cIljY3txKhF55kts-S0ubzencFM5MoCxGnBk-F76HXGaOip6Z0fNARCuMCSYsJzBtRzOAD-k5F3eKoJvk5qyaCvCDZrp9VZWwWRS--ysIGLxgc3rTpdyTH0oI2qrUbalS5sDBqCGAZOomXhaksdFyfu5k3sOxN4jO0H1CsXD_VcdZZh4wrgRuSXvijYtVjT-3QfgOH0px3bE7qlz94uXOOuGshkyIwRqAaR6UTgG_FKajHRSYiUSmI2tc6isA=="}'}
WS #2 开始 WebSocket 连接: {'url': 'wss://zj-cn-live-comet.chat.bilibili.com:2245/sub', 'headers': {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Referer': 'https://www.bilibili.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept': '*/*', 'Priority': 'u=1, i', 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-site'}, 'params': {}}
WS #2 发送 WebSocket 请求: {'data': b'\x00\x00\x01\xa8\x00\x10\x00\x01\x00\x00\x00\x07\x00\x00\x00\x01{"uid":0,"roomid":21402309,"protover":3,"platform":"web","type":2,"buvid":"03A8EE71-EA2B-49A6-9966-8A64004439B306580infoc","key":"skI7_Suu_KXA8Ekxydx1R5qfdjiNCMKMlL462M9q6j7dhDY8E_jKyzvLl84piYjr2M8LvL07uPgbIi4LUvjTBX6vM6tYyTI3uIig3edQmqP6O5QoVJHIX6RIbxYEkzqDiHGZWi6-sLBCCmP5yp0V4JeqIt4dJj4r0Coqz-Z9IbLKYA-IgMqXVJrpAluRkXp9ZrQGTa60BHW5uGaMyanbwrZMutQGz2-L_zTFyIRC8rp2zPolj_mX7oB09FePK9JY0aoyAGNBu0lWlOAhd-A="}'}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x08\x00\x00\x00\x01{"code":0}', 'flags': 2}
WS #1 发送 WebSocket 请求: {'data': b'\x00\x00\x00\x1f\x00\x10\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01[object Object]'}
WS #2 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x08\x00\x00\x00\x01{"code":0}', 'flags': 2}
WS #2 发送 WebSocket 请求: {'data': b'\x00\x00\x00\x1f\x00\x10\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01[object Object]'}
WS #3 开始 WebSocket 连接: {'url': 'wss://zj-cn-live-comet.chat.bilibili.com:2245/sub', 'headers': {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Referer': 'https://www.bilibili.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept': '*/*', 'Priority': 'u=1, i', 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-site'}, 'params': {}}
WS #3 发送 WebSocket 请求: {'data': b'\x00\x00\x01\xa1\x00\x10\x00\x01\x00\x00\x00\x07\x00\x00\x00\x01{"uid":0,"roomid":33989,"protover":3,"platform":"web","type":2,"buvid":"03A8EE71-EA2B-49A6-9966-8A64004439B306580infoc","key":"cSU-foStXAdRe3LwGHLU1cdDzi766va_SI0ApuG0GWcbxHNRMP8BRCqXYF9Tzw23Zfn9OP8HtydtF0ezQrsi3GWgyJK5eyAsNI2iTQMA2oy7u83C9dKW2o2cwj0mQFEIfOhIQfh41rg4CWFjzcSuXKJu0ARmBZ2coi42wVKZTACMsHTnujQKZqFZbasWXwHG_4oRcdRr2w00ScY_GzOyuLdGzG2Lwuwq0jeOq-360cvKrpBYB_80jGhl1mFMBOaMcw50gEwhyAwsYQTS"}'}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x14\x00\x10\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01[object Object]', 'flags': 2}
WS #2 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x14\x00\x10\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01[object Object]', 'flags': 2}
WS #3 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x08\x00\x00\x00\x01{"code":0}', 'flags': 2}
WS #3 发送 WebSocket 请求: {'data': b'\x00\x00\x00\x1f\x00\x10\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01[object Object]'}
WS #3 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x14\x00\x10\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01[object Object]', 'flags': 2}
WS #4 开始 WebSocket 连接: {'url': 'wss://zj-cn-live-comet.chat.bilibili.com:2245/sub', 'headers': {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Referer': 'https://www.bilibili.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept': '*/*', 'Priority': 'u=1, i', 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-site'}, 'params': {}}
WS #4 发送 WebSocket 请求: {'data': b'\x00\x00\x01\xa3\x00\x10\x00\x01\x00\x00\x00\x07\x00\x00\x00\x01{"uid":0,"roomid":2751313,"protover":3,"platform":"web","type":2,"buvid":"03A8EE71-EA2B-49A6-9966-8A64004439B306580infoc","key":"BhEuRGLxvcrj9r9O3jp-nmCiooj8y2OJmRYejL22Vu3401liCKduFTNv7Z4ThPI5hF4H4rANM-EdvHgjY5QPz4AOz6OSXhX5XCmMAP7ervw3QjKNGBzJRL34a1C0vca5YQp0-r6_NX_Z2ZACPEKGkKrubosbwHj2NuoGwNwWYKwN-P1n1-xYy_lNix4hDQzuYLuPcb8tyhO2h3kznGx9Knx3WwjmtXiPH7PaJ5456NdsYTKmGd3k0hqM-6RmOJTRMnMWx6kfWBJT7ZIS"}'}
WS #4 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x08\x00\x00\x00\x01{"code":0}', 'flags': 2}
WS #4 发送 WebSocket 请求: {'data': b'\x00\x00\x00\x1f\x00\x10\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01[object Object]'}
WS #4 收到 WebSocket 数据: {'data': b'\x00\x00\x00\x14\x00\x10\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01[object Object]', 'flags': 2}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x01%\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"LOG_IN_NOTICE","data":{"notice_msg":"\xe4\xb8\xba\xe4\xbf\x9d\xe6\x8a\xa4\xe7\x94\xa8\xe6\x88\xb7\xe9\x9a\x90\xe7\xa7\x81\xef\xbc\x8c\xe6\x9c\xaa\xe7\x99\xbb\xe5\xbd\x95\xe6\x97\xa0\xe6\xb3\x95\xe6\x9f\xa5\xe7\x9c\x8b\xe4\xbb\x96\xe4\xba\xba\xe6\x98\xb5\xe7\xa7\xb0","image_web":"http://i0.hdslb.com/bfs/dm/75e7c16b99208df259fe0a93354fd3440cbab412.png","image_app":"http://i0.hdslb.com/bfs/dm/b632f7dcd3acf47deffb5f9ccc9546ae97a3415b.png"}}', 'flags': 2}
WS #3 收到 WebSocket 数据: {'data': b'\x00\x00\x01%\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"LOG_IN_NOTICE","data":{"notice_msg":"\xe4\xb8\xba\xe4\xbf\x9d\xe6\x8a\xa4\xe7\x94\xa8\xe6\x88\xb7\xe9\x9a\x90\xe7\xa7\x81\xef\xbc\x8c\xe6\x9c\xaa\xe7\x99\xbb\xe5\xbd\x95\xe6\x97\xa0\xe6\xb3\x95\xe6\x9f\xa5\xe7\x9c\x8b\xe4\xbb\x96\xe4\xba\xba\xe6\x98\xb5\xe7\xa7\xb0","image_web":"http://i0.hdslb.com/bfs/dm/75e7c16b99208df259fe0a93354fd3440cbab412.png","image_app":"http://i0.hdslb.com/bfs/dm/b632f7dcd3acf47deffb5f9ccc9546ae97a3415b.png"}}', 'flags': 2}
WS #4 收到 WebSocket 数据: {'data': b'\x00\x00\x01%\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"LOG_IN_NOTICE","data":{"notice_msg":"\xe4\xb8\xba\xe4\xbf\x9d\xe6\x8a\xa4\xe7\x94\xa8\xe6\x88\xb7\xe9\x9a\x90\xe7\xa7\x81\xef\xbc\x8c\xe6\x9c\xaa\xe7\x99\xbb\xe5\xbd\x95\xe6\x97\xa0\xe6\xb3\x95\xe6\x9f\xa5\xe7\x9c\x8b\xe4\xbb\x96\xe4\xba\xba\xe6\x98\xb5\xe7\xa7\xb0","image_web":"http://i0.hdslb.com/bfs/dm/75e7c16b99208df259fe0a93354fd3440cbab412.png","image_app":"http://i0.hdslb.com/bfs/dm/b632f7dcd3acf47deffb5f9ccc9546ae97a3415b.png"}}', 'flags': 2}
WS #2 收到 WebSocket 数据: {'data': b'\x00\x00\x01%\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"LOG_IN_NOTICE","data":{"notice_msg":"\xe4\xb8\xba\xe4\xbf\x9d\xe6\x8a\xa4\xe7\x94\xa8\xe6\x88\xb7\xe9\x9a\x90\xe7\xa7\x81\xef\xbc\x8c\xe6\x9c\xaa\xe7\x99\xbb\xe5\xbd\x95\xe6\x97\xa0\xe6\xb3\x95\xe6\x9f\xa5\xe7\x9c\x8b\xe4\xbb\x96\xe4\xba\xba\xe6\x98\xb5\xe7\xa7\xb0","image_web":"http://i0.hdslb.com/bfs/dm/75e7c16b99208df259fe0a93354fd3440cbab412.png","image_app":"http://i0.hdslb.com/bfs/dm/b632f7dcd3acf47deffb5f9ccc9546ae97a3415b.png"}}', 'flags': 2}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x00\xa8\x00\x10\x00\x03\x00\x00\x00\x05\x00\x00\x00\x00\x1b\xe8\x00\x00\x9c\tv\xac\t\xd0\x99\xc3Es\x82R\xb77S\xe6tMG\xb2\x11\x0fFW\x80-\x0e5*\x9c\xa7\x16\x7f\x13\xe2\x903t,w\xf4\xf3\x07\x0b\x08kw[\xdb\xff\x92\xf0\x17`\x94&\x91f\x9ajo\x13A\x10\xf4\x1e\x8b:\x1e\x93VI)"m\xda\x16W\xde0^\xf08\x8f\xf84\xb8\xdd\x88y\xb1x\xded\xf6m\x1a\t=\xd2\x05\xa8\x1b\x88\x1e\x84\xcf\x9e\xfb\xba\x7fTI\xfa\nr\xd7\xc1\x16\xb3\x18s\x05\xc5\'\xe3V\x12> ZT)0}`\x1c\x92\x87\xf1\xc6wmu\xcf\xb6\xa0\n', 'flags': 2}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x01\xf8\x00\x10\x00\x03\x00\x00\x00\x05\x00\x00\x00\x00\x1bz\x02 ,\x8ew\x10\x11\xb3\xbc\x8a\x17\xe5\x98\xbb\xa8\x01\xf6\n\x04\x16\x84\xce\xb06\xffO\xf7w\xfd\xcd\xfa\x08\x8e9T0j\xf0{\x85\x85\xe8\xc2\xa0X\xfaZ\x04\x80f\xef\xd3\x9b\x97d!\xa7\x14\x86m\xd7\xdb\xc1\xa2\xbb\xa6\t\x1e\xf4\x92qz\xc3l%\xf7D\x0e9\x1e\x91\x15AW\xe7\xa2\xae\x1f\x11\x96\xe0\x08Z\xc9\xdf\x17\x82\xc3\xdf\x0c|0\xa4\xcdsWu5\xa5\xad\xec\xdd\xd9\xff\x86\xe5Y=Va\xef\x96\xba\x84\x92"\x17\xe2\xb1\xd7\x9f\n\xcbT?\xea\x9b\xe7\xa2\xda\xf2\x14\xb5\x92\xf7\x9e\xb9\x14\x96\xb0\x16\x1b\x8c"x\xb0e\xdb>u\xab5\xdf7jMd;\xdc\x7f\xc0\xb2\xbc\xed\xbe\xa5\x16u6\x8b]H\x96\xd6\xa4\x86\x88\xbb}\xa0\xc3\x97>*\x02o\xfe\xc6\xaf\xc5\xdc\x0f\xcf\xf3\xdev4\x82\x1eA\x93\xc4q\x06\xb5\xbd\xea?\xa5n\xa9{\\w\xfaz\x8c\xc6\x8b\xd1\x83\x07\xea\xd1SE}\x01S\xe9)6\x9c\xe3])Z:\x08u\xa8D\xbcS\xb4\n\'n\xa8\xb0\xc2\xe34\xe1Lmn\x84\x8d\x01K\xe0\x92\x99kCs\xbc\x14*5\xd4\xc8\xef\x1cd\x15O"\xe5\xa9n\x14S\xa7\xad5v\x88\xa2\x15\x90\x80^\x95\xc5\x11|\xf7\xbd&\xa9D)\xb1\xa2\xf6\x19\x1e8Z\xad\xad\x98\xfe`\x86\xcfarY\xf9\x9c\xa0\x92\xa0\x13A\x10\x9a\x99\x1d\xd5B\x9a\xfc\xff\x0f\xc3\xbab\xc9\x05\xd0\x1a/\xa2\x84\x97P\xca#\x95\xd6\x1a\x9e\xb8\x1b\xab\xe2\xfb\xf5\x88\x8c_n\x87\xff\t|\xff\xb7#s\xb6\xc9\x9b\x83\xdd\xcb\x03\x8e\xba\xcc\xbb\xe7\xff\x93h<\x80Gf\xe3\x91\xabL\t\xdb)i\x9eU,)*^\x1d\x14\xdd\xa3\x12\t1t\x0f\x013dl?-\x18 \x80&\xe9\xfbb\xafZ\\\x9e\xf5\xb0:\xaa\xd6\xf9\xa6\xca\xee\xd44\xda\xd3\xc3\xf2>\x0c\xc44\x9d\xf3\xc0]\xbf\xc9*\ro\xf1:\xafs\xb7\xf7\xf3\xb0\x9b\xa45J\x0c?\x06', 'flags': 2}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x00q\x00\x10\x00\x03\x00\x00\x00\x05\x00\x00\x00\x00\x1b{\x00\xa0,\x8bw\x82\xc3\x92I\xba\xe4Hf\xb5\xaf\xfcp\rP\xb0\xc7\xb7 \xdaZ$flo\xb1\xf4\x85N\x8b|\xb1\x90\xad\x9a\x1b\x9d\x1c\xf0\x01-hk{\x16\xa6i\x14\xdf$\xd0\x8d\xc1\xd3C\x1d*bH"^\x1cFd\xf6\\\x1e\x98mF\xec\x0f\xa1[d\x8c\x9b\xd1\xca\xc4\x8a\nA\xcb\x19\xff\n"\xd2p\x1b', 'flags': 2}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x06\x80\x00\x10\x00\x03\x00\x00\x00\x05\x00\x00\x00\x00\x1b(\x0f0,\n\xecX\x03n\x8aL\x9d/\xeb\x8d\xd7\x91#$\x99E&\xdc$.\xd1\x03\xf0\x17uz\xfd{; \x0fN\xa7\xe8\xc4\x92[\x9e\xf49\x89\x17 \xa7@\xc7\x9a\xf6\x7f\xba\xffkU\x94-G#\x84\xa7Y\xdd\xa7\xce\x86\x98_\xb3\r\x19\x98\x94\xa6\x92:,}!2\xfc\xfdL\xf7m\x80\x03\xdct\xc0\xcfx \x11i\xc3Y\x85\x95\xad-\x9a*\xc0\x06\x14\x114\x9e\x8f\xab\x0e\xe4\x0e\xf8\x80\xa7\x87\x07v\x80zv\xe4\x07r\xb29\xa1=H\x08l\xb6=o\xb0\x12\xdc\\\'\x18/\x18\x08\x84\x0b\x84\x0b\x04\x83\xca\xf4\x8cSr\xa9\xff\x93&}`\xea\x97\xa2\x96\xff\xb5Y\xbd\xfao\x9d<\x14\xb2\x1e\xacl\x1f\xd7\xbf\xf4\xd0\x1c\xcfqs\xf5q\xb3\xfdN\x9a\xc9\xcd^\xf7&3\xc5\xe2\xa8`\xb1\x98\xd4\xdd\xce\xda(\x99U\x0fz\xfe\xf5\x90\xf3\x8e\x7f\xd4\x17\xb2\xa2\x8c\xdf_\xcfD\x1c4\xe3\x81\xd8\xe6\x1e\xef\xd6W\xa2\xed\xac\x9983M\x90;\xde\'\xd3\x0f\x1b\x08\x19\x0cTv\xf4\xabM\x1bW\xa4\x9d\xe9\xd2\xd2Y\xa6\xd3\xeeV\xacLPeu\xfek#\x0b\x17ur\\|\xac\xe2\xcbx\x15\xb9v\x892\xec\x17\xa6\x9b?\x84\x8c]y&\xb4E\xee\x1b+\xecW(\xedo\x97\xce\xda1\xb9wg\xcd\xaa\x8b\xa6\xbcJ\xde\xaf\x85&\xbc\xc7\xab\xe7\x82\xf7\xf0P\xef\xf9\xc8:.x\xc3\x06\xc5\xcdoC\xba\xf6\x9a\xfc\x85C\x98\xe7\xe6\xe5\xd0\xf2\xbek\xed\xc8\x9dT\xd24|\xae\xf7]\xb5za\xf9k:[\x04c\x7f\xcev\x9d\xdd\xec\xb4\r\xf1\xb4\xb8\x11\xfb=j\x16Rr3=\xf6V\xc7\xbb\xd2\'\xc1;\xc5/my\xb3m\xb07\xe6\x90\xbdx-\x12\x8aM\x9eU\x9d\x0cj\x04\xa4+\x88Bti\x00\xe1\x01\xc1\x9d\x12[_\xa0\x18O\xce\x95\xb2\xe9:\x82\xacg9\xb0\xe8\x03\xd0\x82#\xca\x94\xb0\xfa\x00\x84\x99E\x93\x18U\x8d\x04[\x0bJ&\xc0\x14\x07\x911\xa4-\x128l\x0bS\xa3&\xc8\x8b\xba\xff\xb2\xff\xd5i\x95f\x83\xdb\xb8\xb7l\xd8\x9c^\x87y\xf5\xb2o\xbb\xcbX\x9eT\x9e\x9b\xb7\\\xf8A\xc7\xb5\x1d\xad\xb0H\xe7K\xd4U\x91n\x18\x84\xd0\x19\xe8G\xa3\n\x8eA\xe1\x07\x8b0\xe4%(\xc9\x08\x83\x01\x03Z\x03\xd9\x88\xc8\x9e\xd8\x98\xd7\xfa\x9e\xb3\x8f\x11_5\x84+\xa1\xd3\xc5\xcf?}W\x97\xf1,\x17\xa7I6\x16\x01y\x89\x18\xc0\xab\xc1\x93 \x18*BYA&\xc4\x90\xd9&2\xb0&\x91\x8a\x0c\x8dq\x16}\xc4\xb4%\xc6\xcdD\xa4F\xd0\xa5]]O\xdd\xb0\x82\xcc\x0e\xbfv\xaew\xf6\xed8\xd0\xae\xbce\xa3\xe6$\xf3\xbcP\xbbjL\xee|\xa6V\xa7\xef5\\\xb5\x0f\xd8)\x1a-SV\x85s\xa8F\x08\xe8P|\xd6(\xdc\x1b\x93\x9aQ\x0c\xb9\xd1\rC\xb5\xf5L\x14\xe3\xd4@,-Y\x1d\xcan\xc7.\xe4vu\x14\xfb]\xd2^\xfcg\x1a,\r\xd8F\xbf\xad}\xb4o\x8e\xf1?\xde\xd4S\xb3\x80R1\xd4\xb9Q\xadU\x8e \xaa\xd0*\nC\xc3Q\xd7\x89\x8b\x94\xc0W\x88Kb\xbd%\x84\xa5%\x94\x08\xabTy\x93d\xea\xb8\x10yy[\x1a\xd4\xe4\xc3\xc3T\xbac\n\xd6\x1b\x00\xb1\xf9\xcc\x08k\xc7\xb8\x89\x08\'\x04\x98|RH.\x0c\xee\x8e\xb8\xf4\x06\xaa\xd9\xa8\x1a\x15\xf94\xc2\xaa\x88\xf4>\xadi/w\x12\\1\xbc\xd9\xfb\xd9\xad\xd2\xb6\xa4\t\xa09\x04ru\x92\xa6\x88+\x12\xebc\xbf\xda\x05C\xf4!M\x17\xf1\xbdy\x16\xda:\xc4\xf1\x84\x8b\xf2\x85H\xbd\x08Y\x84\xb4\x89\x0071\xab>FUL\x8cJ6\xe3\x04\xd4\x1b\x87@\x11\xf8\xe6\x00\xf3\x92B0 [Z\xcc\xf9\x07\xebr\x8c|\x15A\xc3\xd2u\xacV\x9d\x9b\x11\xc5\xd6\xa3(b\xd4-!\xb4\x16t?w+\xc1\x10\xe4\xe12\x84\xc8TJL\xd9_\x86\xcf\x8apqK\xdf\xb76\x19\x95\xdb\xdc\xec\x9b\xc3\xf7Gu\n\xd3w\x00\x19\xd5Ix\x18\xce\xa3?\xbd\x81\x9f\xa02rw\x96Uq\xec^\xf2|\x08\xcb\x02\x8e\x8d0\xf3\xfc$\n\x11\x85\xcfQ5\x13\x05\x9aAm=\x06\n\x84i\x03\xb4u,>\xeeL\x13\x8f\xd80V/z*\xc6J\x83\xda\xaa\x9e\xdb\xdd\xb3\x10\xfd\x98\xc7\xc3I\xf8\xe2\xfc\xa5\x7f\xddrm{\xe1\x8b7\xf1\xe5.Q\xb6\xce\xe7L\x01\xb3\xd3Yf\xbaX:vuN\x1b+\xce3/\x88 2ZF,\x96\x88\xa8F\xa6M\x82\x88\x16\x19\xcd\xac[\x87\x01\x89\xf4\x1b\x03\t\xb0c\xa4\x9a\xd6\xfe\xa8G\xb4Y\xa9W\xed\xcf\xce\xd9\xdc\xe8\xf1\xe0,\x9d\x07\xf6\x85\xdc*\xedO\x96\xae\xce\xb5\x87 \xe9\xfcB\xe8\x87CY\xfb\xf8\xa3m\xba\xb5?\xe8\x1f\xd4D\x7f\x1e\xce\xa2\xadN\xd1\xb9i\xf4f\xef\xbfe\xd7i\xda\x0ef\xd2\xceD\x95\x96\xc2\x82\x13\xb4\x0et\xc5\x00\x03b\xf8x\x07\xa6\x12\xc6\xd0\xb3j\x12\xf8\x10\x0c\xfdd\xd2"au1\xb9Y\xc7\xab$\x8f\xfbr\xbc\xd0\x1e\xd4\xf6+>s\xd0k\xcbb9\xa0\xde\x12\x06\x15\x03\xa9\x99B^\x13Cb\xad` \x9bY\x9d2U\x0e&\xc0a\xfa\xb2\x12\x1a;\x89_\xdcI\tj\xfdG[|\x97]\xb1\x9f\xf9\xb2;Kl\xd2t_\xc1w\x94my\xe1\x80X\xab\xf7o\xf3k\x97ff\xe7Z\xe7\xf4\xcd\xd8\xe8\xe2v\xafv0\xaa\xb8\xbb\xb2\x9b\xe8\rZ\x0c\x9a}\x10,=\xd1b"X\x13crC(-\x10\xa4\xack\xe4\x89W\xae.@\xcb\x81\xa0\xb5\x04\xe6J\xd0\x1c6\xbdan^v\xdf\xf6\x94\x90\x8d\'\xd6\xa5\x03-c&J\x0073a\x88F\xad\x91\x18\x8d\x04\x1f\x02\xc4\t#R\x04|\x9c\xba\xc9h7\x98\xfe\x0bm%\xf7_\xa9\xf8<}~\xa9\xd3U\xd0\x83\x9e\xe8O\xee\x8d\xf9\x9b\xcaZ\xbb\xa1\x16\xa3\x0e\xbe\xaf\x87\x83\xba.\xc9\xa8%R_Y0\x80pW#>K\xc2:g\xd1\xc7\x04\xee\x1du3\x1a\xd1L\xac\xde"\x06\xf3\x01\xb3a\xff\x9bwZ\x1fr\xb7{\x1dN\xbb\xe9\xe0v\xce\x0f\xb4\xbe\xae\xcc\xac9\xab<_\xadv~qX\x9dd\xb1\n^\x7f\t~{\xf5\x7f\xcda\xcem1\xe1\xf40\xe5\xf1t\x12v\xb5\x17\xbc{\xe3c"\x88(%]\xc9\xb42Pr4 Sdh\x8d^:\xc4\xed`\xa0\x8fYd)\x8a\xa5ch\xbd\x11\x1fw{s}\x0e\xcd\x8b\xef\xd5\xc6\x97]\\\x1f\xe2\x99\xf7\xd1w]\xef7\x1d\xa9\xd7\x05c\x9e\x93\xd0\x1f\xb6\x91 \xe3\x00\xe6\x83\x98\xe2\xa4\x1b\x86Zr\xc4\xad\x03a^A\xb7\x1e\xd5[\x8a\x01>\x1f\xc9/\x0f', 'flags': 2}
WS #3 收到 WebSocket 数据: {'data': b'\x00\x00\x02\x19\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"STOP_LIVE_ROOM_LIST","data":{"room_id_list":[1713823108,1875098988,1443595,1949980368,1735944442,21742936,1923358379,2121756,22849209,1791355950,1896768542,24531040,1713426509,26195941,881852,1702711742,23882072,1729696231,27686357,5397465,26889482,4472957,1911278258,1917434416,1796109543,1758140346,1908902023,23581219,12844674,26010387,1913302299,1919957869,1713428129,1886447053,22999353,13596213,1803299897,1746705940,2547862,30754825,23068929,1947275177,21887740,31138460,1817613586,1923353447,1791989792]}}', 'flags': 2}
WS #2 收到 WebSocket 数据: {'data': b'\x00\x00\x02\x19\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"STOP_LIVE_ROOM_LIST","data":{"room_id_list":[1713823108,1875098988,1443595,1949980368,1735944442,21742936,1923358379,2121756,22849209,1791355950,1896768542,24531040,1713426509,26195941,881852,1702711742,23882072,1729696231,27686357,5397465,26889482,4472957,1911278258,1917434416,1796109543,1758140346,1908902023,23581219,12844674,26010387,1913302299,1919957869,1713428129,1886447053,22999353,13596213,1803299897,1746705940,2547862,30754825,23068929,1947275177,21887740,31138460,1817613586,1923353447,1791989792]}}', 'flags': 2}
WS #1 收到 WebSocket 数据: {'data': b'\x00\x00\x02\x19\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"STOP_LIVE_ROOM_LIST","data":{"room_id_list":[1713823108,1875098988,1443595,1949980368,1735944442,21742936,1923358379,2121756,22849209,1791355950,1896768542,24531040,1713426509,26195941,881852,1702711742,23882072,1729696231,27686357,5397465,26889482,4472957,1911278258,1917434416,1796109543,1758140346,1908902023,23581219,12844674,26010387,1913302299,1919957869,1713428129,1886447053,22999353,13596213,1803299897,1746705940,2547862,30754825,23068929,1947275177,21887740,31138460,1817613586,1923353447,1791989792]}}', 'flags': 2}
WS #4 收到 WebSocket 数据: {'data': b'\x00\x00\x02\x19\x00\x10\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00{"cmd":"STOP_LIVE_ROOM_LIST","data":{"room_id_list":[1713823108,1875098988,1443595,1949980368,1735944442,21742936,1923358379,2121756,22849209,1791355950,1896768542,24531040,1713426509,26195941,881852,1702711742,23882072,1729696231,27686357,5397465,26889482,4472957,1911278258,1917434416,1796109543,1758140346,1908902023,23581219,12844674,26010387,1913302299,1919957869,1713428129,1886447053,22999353,13596213,1803299897,1746705940,2547862,30754825,23068929,1947275177,21887740,31138460,1817613586,1923353447,1791989792]}}', 'flags': 2}
WS #2 收到 WebSocket 数据: {'data': b'\x00\x00\x00}\x00\x10\x00\x03\x00\x00\x00\x05\x00\x00\x00\x00\x1bi\x00P<\x14\xef\xd2>T\x9d\xf5,\x19bT\xceF\xe1\x91\x7fE\xd5\x00\x05j{[$f\x0f}-\xfe6D\xdf!\xa7\x9e!\x1as\x93,"0\xa1\xd3"_Ed\xb3M\xa4N\'\'\x079 ?kI\xd6\x96PX\x00\x96S\x8e\xda\x12\x89[\x8b\xcez\xb9~b@\xe6\\\xcebp\xcf\xde\xc1M\xdc\xd3<1\xa5\x17\x16\x12A\x13\xb0\xeb\xb9\xb5\x8f\x02', 'flags': 2}
WS #3 关闭 WebSocket 请求: {}
WS #4 关闭 WebSocket 请求: {}
WS #2 关闭 WebSocket 请求: {}
WS #1 关闭 WebSocket 请求: {}
async file io
模块中存在文件 io,本次引入了异步文件 io 实现,但仍然保留了部分同步文件 io (例如无处不在的 get_api)。
或者说,这次更改的原则是:对内同步,对外异步。因为内部文件大小有保证,不至于堵塞时间循环,外部文件谁又知道呢。
简单演示一下。
import asyncio
import json
from bilibili_api import Credential, Picture
credential = Credential.from_cookies(json.load(open("test-cookies.json")))
async def main() -> None:
pic = await Picture.load_file("test.jpg")
await pic.upload(credential)
print(pic.url)
asyncio.run(main())此部分后续有待进一步实施与调整。(例如当前 Picture 内部涉及 PIL 相关逻辑仍为同步代码)
Other changes
docstring
本次对 docstring 进行了统一的规范工作。现统一仓库所有 docstring 使用 google docstring style。可参考 https://github.com/NilsJPWerner/autoDocstring/blob/HEAD/docs/google.md
python ver 升至 3.10,部分复合类型已用新语法进行重写,如 X | Y。
在开发中若要编写 docstring,此处推荐一个 vsc 插件 autoDocstring - Python Docstring Generator。
formatting / linting
项目已接入 ruff 和 pyright 进行 linting 和 formatting。后者主要进行类型检查。
关于类型检查,因项目性质所以许多地方的类型注解无法维护,如 api 返回结果的字段。所以类型检查中心还是关注与模块逻辑相关的函数,避免低级代码错误。
ruff 主要目的还是 formatting 和 import organizing,以及应用各种 autofix。上面利用新语法重写复合类型的工作就是交给 ruff 来做的。
ruff 和 pyright 的选择与相关配置借鉴了 nonebot2 仓库。
再提到类型检查与类型注解,模块至今的定位更偏向于明确地引导使用者调用正确的 api,而对结果处理的引导较少,例如拿到一个返回值需要自己寻找字段。确实,前者相比后者更重要且困难得多,我会生成 user_info 的 protobuf 却不会处理 wbi 风控,又有什么用呢?但这不代表对结果处理的引导就能完全忽略,尤其是在后 typeshed 时代,类型检查和类型注解既能加快编码速度,也能减少代码中的出错。当把 data["ts"] 和 data["uname"] 加在一起时,没有类型注解看似没有问题,而有了类型注解便能察觉到这里面有离大谱的问题存在。甚至类型注解厉害到一定程度,它能告诉你 data.ts: int 和 data.uname: str,当输入 data[""] 时对需要填入的字段一脸茫然时,这样的类型注释帮助就不小了。目前我所了解的在结果处理方面的引导有这些:对各种直播事件的注释,Danmaku 类,#991 中提到的抽象基类当然也算,还有更多,可光是这些工作够不够相信所有人都心里有数。于是乎,需要进一步完善这方面的类型注解,可是接口复杂性、体量和更改频率摆在这儿,这决不会是什么轻松的工作,是什么高效的工作,但母庸置疑的是,这方面的工作模块不会放弃。
docs
许多依赖重复工作的文档现在已经有脚本可以生成 (scripts/doc_gen.py),接下来便可以将文档中心放在其他部分,例如 api 示例与模块开发者文档等等。
Other changes in branch dev
- fix: 专栏页面消失导致 get_all 失效 (#994)
- feat(VDURLDDetecter): add missing fields in dash (#995)
- feat(ass): 字幕 ass 生成支持更多参数
未来小型 patch 仍会在 dev 发布,甚至 dev 分支上还会继续发布新版本,同时 patches 也会合并到 dev-dyn-fp,故不用担心使用 dev-dyn-fp 分枝上的代码漏掉 dev 上的提交。