ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Streamlink 的 Webbrowser API 深入解析:基于 CDP 的浏览器自动化与网络请求拦截

Streamlink 的 Webbrowser API 深入解析:基于 CDP 的浏览器自动化与网络请求拦截 Streamlink 的 Webbrowser API 深入解析基于 CDP 的浏览器自动化与网络请求拦截【免费下载链接】streamlinkStreamlink is a CLI utility which pipes video streams from various services into a video player项目地址: https://gitcode.com/gh_mirrors/st/streamlink导读Streamlink 的streamlink.webbrowser包为插件提供了一套基于 Chrome DevTools ProtocolCDP的浏览器自动化能力它可以在幕后启动一个 Chromium 内核的浏览器通过 WebSocket 建立远程调试连接然后以异步 API 的方式完成页面导航、网络请求/响应拦截、JavaScript 表达式求值以及 Cookie 同步等操作。本文以 docs/api/webbrowser.rst 为骨架结合 src/streamlink/webbrowser 下的源码实现系统讲解这套 API 的类层次、会话选项、核心方法以及它在 Twitch 插件中的真实落地方式。读完本文你将能够理解并编写出完整的 CDP 自动化流程启动浏览器 → 建立连接 → 拦截请求 → 执行 JS → 获取结果。概览webbrowser 包是什么streamlink.webbrowser是一套开箱即用的 CDP 客户端封装。它的设计目标是让 Streamlink 插件能够在流媒体网站上自动完成需要真实浏览器环境才能通过的挑战例如Twitch 插件src/streamlink/plugins/twitch.py在获取client-integrity token时使用该 APIKick 插件src/streamlink/plugins/kick.py用它求解 JS challenge 并回填 Cookie。⚠️稳定性警告官方文档明确声明streamlink.webbrowser包的 API 被视为unstable不稳定使用时需自行承担风险Use at your own risk!。整个包的核心抽象分为三层层次类职责高层 APICDPClient/CDPClientSession启动浏览器、导航、拦截请求、执行 JS、同步 Cookie连接层CDPConnection/CDPSession/CDPBase/CDPEventListenerWebSocket 消息收发、命令响应、事件分发浏览器层Webbrowser/ChromiumWebbrowser解析可执行文件路径、生成启动参数、拉起浏览器进程一、浏览器层Webbrowser 与 ChromiumWebbrowser1.1 基类Webbrowsersrc/streamlink/webbrowser/webbrowser.py 中的Webbrowser是所有浏览器实现的基类负责三件事解析可执行文件构造时调用resolve_executable(executable, self.names(), self.fallback_paths())若找不到可执行文件则抛出WebbrowserError错误信息会提示用户通过--webbrowser-executable指定路径提供启动参数names()返回可识别的进程名列表fallback_paths()返回兜底的绝对路径launch_args()返回命令行参数管理进程生命周期launch()通过内部_WebbrowserLauncher在trio的 nursery 中启动子进程stdout/stderr被重定向到DEVNULL启动时默认超时TIMEOUT 10秒并挂载一个进程监视任务——如果用户主动关闭浏览器或进程提前退出整个任务组会被取消。1.2 默认实现ChromiumWebbrowsersrc/streamlink/webbrowser/chromium.py 中的ChromiumWebbrowser是当前唯一的浏览器实现可识别进程名chromium、chromium-browser、chrome、google-chrome、google-chrome-stableWindows 兜底路径msedge.exe和chrome.exe的常见安装路径含 Edge Beta/Dev、Chrome Canary 等变体macOS 兜底路径/Applications/Chromium.app/...与/Applications/Google Chrome.app/...等标准位置默认启动参数launch_args()非常克制专为无头脚本化场景调优主要包括--autoplay-policyuser-gesture-required禁止自动播放视频--deny-permission-prompts自动拒绝所有权限弹窗--disable-background-networking、--disable-extensions、--disable-component-update关闭扩展与后台网络服务--mute-audio静音--silent-launch不创建初始的空标签页即不产生默认 CDP target--window-size0,0非 headless 模式下尽量不打扰用户--use-mock-keychainmacOS 上避免是否允许接入网络的弹窗。在launch()中若未指定port会通过 src/streamlink/utils/socket.py 的find_free_port_ipv4()/find_free_port_ipv6()自动挑选空闲端口并追加--remote-debugging-host、--remote-debugging-port与临时--user-data-dir参数headless 模式下额外追加--headlessnew。随后通过get_websocket_url()轮询http://host:port/json/version解析出webSocketDebuggerUrl带 10 次重试、retry_backoff0.25、timeout0.1作为后续 CDP WebSocket 连接的入口。二、连接层CDPConnection、CDPSession 与事件监听2.1CDPBase命令与事件的统一基类src/streamlink/webbrowser/cdp/connection.py 中的CDPBase是连接与会话的共同基类其设计参考了trio-chrome-devtools-protocol0.6.0 项目。核心成员send(cmd, timeoutNone)发送一条 CDP 命令并等待响应。每条命令分配自增 ID通过_CDPCmdBuffer暂存配合trio.Event在响应到达时唤醒等待者默认单命令超时CMD_TIMEOUT 2秒超时或连接关闭时抛出CDPError。可选的timeout参数主要用于覆盖 JS 求值这类耗时操作listen(event, max_buffer_sizeNone)注册对某类 CDP 事件的监听返回一个CDPEventListener。默认内存通道缓冲上限为MAX_BUFFER_SIZE 10收到事件后通过send_nowait广播给所有订阅者通道满时会记录错误日志内部_handle_data()根据消息中是否有id区分命令响应与事件事件会经parse_json_event()解析为类型化对象。CDPBase常量还包括MAX_MESSAGE_SIZE 2**24约 16 MiB用于限制 WebSocket 单条消息大小。2.2CDPConnectionWebSocket 连接管理CDPConnection继承CDPBase和trio.abc.AsyncResource不直接实例化而是使用CDPConnection.create(url, timeoutNone)这个异步上下文管理器在trionursery 中通过connect_websocket_url()建立 WebSocket 连接启动_task_reader后台任务持续读取消息全局消息交给连接自身处理带sessionId的消息则路由到对应的CDPSession退出上下文时调用aclose()关闭 WebSocket、关闭所有内存通道并清理会话。它提供了两个关键方法new_target(url)创建一个新的浏览器标签页target并返回绑定该标签的CDPSession。文档注释建议留空 URL以便后续用page.navigate()进行规范的导航处理get_session(target_id)向目标发送attach_to_target命令返回一个CDPSession实例并登记在self.sessions中。2.3CDPSession与CDPEventListenerCDPSession本身是一个空子类class CDPSession(CDPBase): pass语义上代表绑定到某个 target/sessionId 的命令与事件上下文。CDPEventListener是listen()的返回对象提供两种消费方式# 方式一异步 for 循环持续监听事件 async for request in cdp_session.listen(devtools.fetch.RequestPaused): ... # 方式二异步上下文管理器只取一个事件后自动关闭监听器 async with cdp_session.listen(devtools.fetch.RequestPaused) as request: ...它基于trio.MemorySendChannel/MemoryReceiveChannel实现另有receive()取单个事件但不关闭通道与close()两个方法。三、高层 APICDPClient 与 CDPClientSession3.1CDPClient.launch()一站式启动入口src/streamlink/webbrowser/cdp/client.py 中的CDPClient是围绕ChromiumWebbrowser与CDPConnection的公共接口不要直接实例化应使用类方法CDPClient.launch(...)它会做四件事启动一个新的trio事件循环trio.run(run_wrapper, strict_exception_groupsTrue)用传入参数或对应的 session 选项启动 Chromium 浏览器初始化CDPConnection并连接远程调试接口以CDPClient实例为唯一参数执行异步 runner 回调并返回其返回值。launch()的签名与默认值来源如下参数作用未指定时的回退顺序executable浏览器可执行文件路径webbrowser-executable会话选项 → 按ChromiumBrowser规则查找timeout全局超时含浏览器启动耗时webbrowser-timeout会话选项cdp_hostCDP 监听主机webbrowser-cdp-host→127.0.0.1cdp_portCDP 监听端口webbrowser-cdp-port→ 随机空闲端口cdp_timeout单条 CDP 命令超时webbrowser-cdp-timeout会话选项headless是否无头模式webbrowser-headless会话选项此外如果会话选项webbrowser被设置为Falselaunch()会直接抛出CDPError(The webbrowser API has been disabled by the user)。内部实现中CDPClient.run()负责串联启动浏览器 → 拿 WebSocket URL → 建立 CDPConnection → yield CDPClient的完整链路。3.2CDPClient.session()创建标签页会话asynccontextmanager async def session(self, fail_unhandled_requests: bool False, max_buffer_size: int | None None):在空的浏览器标签页target上创建一个新 CDP 会话fail_unhandled_requestsTrue时未被任何请求处理器匹配的网络请求会被拦截失败fail_request默认False时则直接放行continue_requestmax_buffer_size用于设置被暂停的 HTTP 请求/响应内存通道的大小。3.3CDPClientSession导航、拦截与求值CDPClientSession是插件最常打交道的类官方文档用一段完整的示例展示了它的核心用法与 src/streamlink/plugins/twitch.py 中的真实用法几乎一致async def fake_response(client_session: CDPClientSession, request: devtools.fetch.RequestPaused): if request.response_status_code is not None and 300 request.response_status_code 400: await client_session.continue_request(request) else: async with client_session.alter_request(request) as cmproxy: cmproxy.body !doctype htmlhtmlbodyfoo/body/html async def my_app_logic(client: CDPClient): async with client.session() as client_session: client_session.add_request_handler(fake_response, *) async with client_session.navigate(https://google.com) as frame_id: await client_session.loaded(frame_id) return await client_session.evaluate(document.body.innerText) assert CDPClient.launch(session, my_app_logic) foo下面拆解这段流程涉及的核心方法。注册请求处理器add_request_handler()def add_request_handler(self, async_handler, url_pattern: str *, on_request: bool False):async_handler异步处理器接收(client_session, RequestPaused)两个参数。它必须调用continue_request()、fail_request()、fulfill_request()或alter_request()之一否则下一个匹配的处理器会被继续执行若最终没有任何处理器处理该请求则按fail_unhandled_requests决定是放行还是拦截url_patternURL 通配符字符串默认*。只有匹配的 URL 才会产生Fetch.requestPaused事件。内部由RequestPausedHandlerclient.py 中的 dataclass通过_url_pattern_to_regex_pattern()把*/?通配符编译成正则支持\*这类转义on_requestTrue拦截请求阶段REQUESTFalse拦截响应阶段RESPONSE。该标志会映射为 src/streamlink/webbrowser/cdp/devtools/fetch.py 中fetch.RequestPattern的request_stage字段。导航navigate()与loaded()asynccontextmanager async def navigate(self, url: str, referrer: str | None None):异步上下文管理器负责打开 URL可带 referrer并启动可选的请求/响应拦截进入时按需调用fetch.enable()、page.enable()随后发送page.navigate()若导航返回 error 则抛出CDPError退出时自动page.disable()、fetch.disable()并取消后台任务不等待页面加载完成yield出FrameID供loaded()使用注意如果 target 被分离例如用户关闭了标签页整个 CDP 连接会被终止包括其他并发会话_on_target_detached_from_target会抛出CDPError(Target has been detached)headless 模式下还会先执行_update_user_agent()读取navigator.userAgent并移除其中的Headless字样避免被网站识别为无头浏览器。async def loaded(self, frame_id: page.FrameId):等待目标frame_id触发Page.frameStoppedLoading事件后返回。求值evaluate()async def evaluate(self, expression: str, await_promise: bool True, timeout: float | None None):通过runtime.evaluate执行可选异步的JavaScript 表达式await_promiseTrue时等待返回的 Promise仅支持 JS 原始类型返回值字符串、数字等其他类型需要自行序列化例如JSON.stringify()求值出错或结果为window.Error子类型时抛出CDPErrortimeout参数可覆盖会话级默认的 CDP 命令超时即webbrowser-cdp-timeout默认 2 秒——Twitch 插件的完整性 token 脚本就通过session.get_option(webbrowser-timeout)默认 20 秒来传入更宽松的超时。四种请求处置方法方法语义关键细节continue_request(request, urlNone, methodNone, post_dataNone, headersNone)放行请求可改写方法/URL/POST 数据/请求头POST 数据经base64.b64encode编码后传给Fetch.continueRequestfail_request(request, error_reasonNone)让请求失败默认错误原因BlockedByClientfulfill_request(request, response_code200, response_headersNone, bodyNone)直接返回自定义响应body 需 base64 编码alter_request(request, response_code200, response_headersNone)上下文管理器形式的取回并改写响应内部调用Fetch.getResponseBody读取原响应体通过CMRequestProxy暴露body/response_code/response_headers三个可写字段退出上下文时自动调用fulfill_request()其中CMRequestProxyclient.py 中的 dataclass包含body: str、response_code: int、response_headers三个字段官方文档将其作为独立 API 列出是alter_request()与调用方之间的数据交换载体。Cookie 双向同步apply_cookies()把 Streamlink HTTP 会话streamlink.http.cookies中的 Cookie 全部复制到 CDP 会话Network.setCookiesretrieve_cookies()把 CDP 会话中的 Cookie 写回 Streamlink 的 HTTP 会话。Kick 插件正是用这个能力把浏览器求解 JS challenge 后得到的 Cookie 注入后续 API 请求。四、会话选项与配置webbrowser API 的默认行为完全由 Streamlink session 选项驱动定义在 src/streamlink/session/options.py选项类型默认值说明webbrowserboolTrue启用或禁用 Streamlink 的 webbrowser API关闭后CDPClient.launch()抛CDPErrorwebbrowser-executablestr \| NoneNone浏览器可执行文件路径webbrowser-timeoutfloat20.0浏览器启动并执行的最大耗时webbrowser-cdp-hoststr \| NoneNoneCDP 接口的自定义主机webbrowser-cdp-portint \| NoneNoneCDP 接口的自定义端口webbrowser-cdp-timeoutfloat2.0等待单条 CDP 命令响应的最大时长webbrowser-headlessboolFalse是否以无头模式启动浏览器这些选项既可以在代码中通过session.get_option()/session.set_option()读写CDPClient.launch()的默认值回退正是读取它们也可以在 CLI 上以--webbrowser-*参数传入。五、实战参考Twitch 插件如何用它获取 client-integrity tokensrc/streamlink/plugins/twitch.py 是这套 API 在仓库内最完整的落地案例整体流程是定义请求处理器on_main对主页面 URL 的请求阶段on_requestTrue执行alter_request()把响应体替换为!doctype html避免页面执行真实逻辑、加快加载定义 runneracquire_client_integrity_token进入client.session()→ 注册处理器 →navigate(url)→loaded(frame_id)→evaluate(js_get_integrity_token, timeouteval_timeout)执行一段注入到页面里的 JS内嵌在插件源码JS_INTEGRITY_TOKEN中动态替换了脚本源、请求头与设备 ID得到包含token与expiration的 JSON 字符串调用CDPClient.launch(session, acquire_client_integrity_token)同步等待结果最后用validate.Schema解析出 token 及其毫秒级过期时间除以 1000 转为秒。Kick 插件src/streamlink/plugins/kick.py则展示了另一种组合求解 JS challenge 后调用retrieve_cookies()把浏览器里的 Cookie 写回session.http供后续 API 请求复用。这两个案例证明了 webbrowser API 的两大典型用途——拦截改写响应 执行页面内 JS 取证与浏览器环境下的 Cookie 获取。六、使用约束与注意事项API 不稳定官方在 docs/api/webbrowser.rst 开头即以 warning 声明该包 API 不稳定插件作者与二次开发者应对接口变动有所预期必须要有可用的浏览器Webbrowser.__init__在解析不到可执行文件时直接抛WebbrowserError用户需安装 Chromium 系浏览器或显式设置--webbrowser-executable底层 devtools 域有限streamlink.webbrowser.cdp.devtools包src/streamlink/webbrowser/cdp/devtools是自动生成的 CDP 类型绑定仅覆盖浏览器、调试器、DOM、Fetch、Network、Page、Runtime、Target 等部分域如需使用未包含的域需自行扩展并发会话共享连接所有会话共用同一条 WebSocket 连接事件按sessionId路由一旦某个标签页被分离导致 target 脱离会波及整个连接见_on_target_detached_from_target进程生命周期浏览器进程由 trio 管理应用逻辑结束、超时或用户关闭浏览器都会触发任务组取消临时user-data-dir在退出后清理源码中甚至在进程终止后额外等待 0.5 秒以确保临时目录可被安全删除。延伸阅读完整 API 参考docs/api/webbrowser.rst高层实现src/streamlink/webbrowser/cdp/client.py连接与会话实现src/streamlink/webbrowser/cdp/connection.py浏览器封装src/streamlink/webbrowser/chromium.py、src/streamlink/webbrowser/webbrowser.py会话选项定义src/streamlink/session/options.py实战用例src/streamlink/plugins/twitch.py、src/streamlink/plugins/kick.py【免费下载链接】streamlinkStreamlink is a CLI utility which pipes video streams from various services into a video player项目地址: https://gitcode.com/gh_mirrors/st/streamlink创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表