首页
/ Selenium 中的 CDP 协议资产库:Chromium DevTools Protocol PDL 的多版本维护与 PDL 到 JSON 的转换机制

Selenium 中的 CDP 协议资产库:Chromium DevTools Protocol PDL 的多版本维护与 PDL 到 JSON 的转换机制

2026-09-05 10:04:24作者:伍希望

本文围绕 Selenium 仓库中 common/devtools/README.md 文档展开:它说明了为什么要在仓库中保留多个版本的 Chrome 调试协议(CDP)定义,以及如何为新的 Chromium 版本引入 browser_protocol.pdljs_protocol.pdl 两个协议源文件。读完本文,你将掌握 Selenium 的 CDP 协议资产目录结构、新协议版本的引入流程(含仓库中自动化的更新脚本),以及 PDL 到 JSON 的完整转换调用链——这套机制正是 Java、Python、.NET、Ruby、JavaScript 各语言绑定中 DevTools 支持的共同底层数据源。

一、为什么在仓库内保留多份协议版本

common/devtools/README.md 开宗明义:

"We keep multiple versions of the protocol in the tree in order to allow us to generate bindings as needed."(我们在代码树中保留多份协议版本,以便按需生成各语言的绑定。)

也就是说,各语言绑定(//java//py//dotnet//rb//javascript/selenium-webdriver)中 DevTools API 的可用方法集合,取决于绑定所支持的 Chromium 里程碑;不同版本的 Selenium 客户端需要面向不同稳定版的 Chrome 生成协议绑定,因此不能只留一份"最新"协议文件。README 还指出这些 PDL 文件通常下载自 Chromium 的 devtools source,而 Chrome 与 Edge 都基于开源的 Chromium 发行版,这使得按版本精确追溯协议定义成为可能。

当前仓库中实际保留了三个 Chromium 版本的协议目录,每个目录包含三个文件:

  • common/devtools/chromium/v150/v151/v152/
  • browser_protocol.pdl:浏览器侧协议(Accessibility、Browser、DOM、Network、Page、Security、ServiceWorker 等域)
  • js_protocol.pdl:V8 引擎侧协议(Console、Debugger、HeapProfiler、Profiler、Runtime、Schema 等域)
  • BUILD.bazel:Bazel 构建规则,把两份 PDL 编译为 JSON

common/devtools/chromium/v152/browser_protocol.pdl 为例,文件头部即为协议元信息:

version
  major 1
  minor 3

随后是形如 experimental domain Accessibilitytype AXNodeId extends stringdeprecated domain Console 的 PDL 声明。v152 版本的 browser_protocol.pdl 中声明了 39 个 domain(从 Accessibility 到 ServiceWorker 等),而 js_protocol.pdl 则包含 V8 引擎的 6 个域,其中 Console 域被标记为 deprecated(README 对应的源文件中注明"建议改用 Runtime 或 Log")。

二、引入一个新协议版本的手工流程

README 的核心内容,是一份为特定 Chrome 发行版引入 vXX 协议目录的操作清单。其步骤与仓库中自动化脚本的实现一一对应,这里先完整给出文档原步骤:

  1. 确定 Chrome 稳定版号。查询 Chrome 桌面端稳定版频道更新信息,得到形如 96.0.4664.45 的版本号。
  2. 创建版本目录。在 common/devtools/chromium/ 下新建 vXX 目录(例如 96.0.4664.45 对应 v96)。
  3. 复制上一版本的 BUILD 文件。从 vXX-1 目录复制 BUILD.bazel 到新目录——这正是当前 v150/v151/v152 三个 BUILD.bazel 内容完全相同的原因。
  4. 下载浏览器侧 PDL。到 Chromium 源码树,打开与发行版本号对应的 tag,找到 //third_party/blink/public/devtools_protocol/browser_protocol.pdl 并下载到 //common/devtools/chromium/vXX
  5. 确定该 Chromium 版本使用的 V8 revision。在 Chromium 源码的 //:DEPS 文件中查找 v8_revision 字段。
  6. 下载引擎侧 PDL。切到 V8 源码对应的 revision,找到 //include/js_protocol.pdl 并下载到同一 vXX 目录。

README 末尾还补充:同样的版本映射信息也可以在 OmahaProxy 的 CSV 查看工具中查到(这是历史上 Chrome 发布渠道数据的常用来源)。

三、自动化更新脚本:scripts/update_cdp.py

上述手工流程在仓库中已有完整的自动化实现——scripts/update_cdp.py。脚本的主流程(L213-L222)依次执行:获取 Chrome 里程碑 → 新增 PDL 目录 → 更新 Java、.NET、Ruby、Python、JavaScript 五个语言绑定的版本引用。它逐条落地的就是 README 的手工步骤:

1. 自动解析稳定版里程碑get_chrome_milestone,L17-L41)。脚本查询 Chrome-for-Testing 的 last-known-good-versions.json,取 Stable 频道的版本号首位(如 152.0.7823.x 得到里程碑 152),再从 known-good-versions-with-downloads.json 中筛选出该里程碑下的最高版本。这与 README 第 1 步"找到最新 Stable Channel Update"等价,只是数据源换成了 Chrome-for-Testing 的版本接口。

2. 滚动保留三个版本add_pdls,L86-L124)。脚本定义三个版本位:new_chrome(里程碑本身)、previous_chrome(里程碑减一)、old_chrome(里程碑减三)。执行时:

  • 删除 v{old} 目录(滚动淘汰,保持目录数恒定——这正是树中只有 v150/v151/v152 三个目录的由来);
  • v{previous} 复制出 v{new} 目录(对应 README 第 3 步"从 vXX-1 复制 BUILD.bazel");
  • fetch_and_save(L44-L49)下载 browser_protocol.pdljs_protocol.pdl
  • 从该版本的 DEPS 文件中用 "v8_revision" in line 定位 V8 revision(L106-L113),再到 V8 源码对应 revision 下取 include/js_protocol.pdl——精确实现了 README 第 5、6 步。

3. PDL 的"扁平化"处理flatten_browser_pdl,L64-L83)。新版 Chromium 的 browser_protocol.pdl 通过 include domains/*.pdl 语句引用拆散的域文件,而 Selenium 树中的 PDL 解析器(见下文第四节)只认单一文件。该函数会提取 version major/minor 版本块,逐一抓取所有被 include 的 domains/*.pdl 文件并拼接成单文件回写。脚本中还有一处细节修复:把 PDL 描述文本中的 `<script>` 替换为 `script`(L119-L124),注释注明是因为 Javadoc 生成器不喜 script 标签——这说明 PDL 里的 # 注释最终会进入各语言绑定的 API 文档。

4. 同步更新语言绑定侧的版本引用(L161-L210)。以 Java 为例(update_java):把 java/src/org/openqa/selenium/devtoolsv{previous} 目录复制到 v{new} 目录、替换文件名与内容中的版本号,并同步修改 java/src/org/openqa/selenium/devtools/versions.bzlrake_tasks/java.rake 中的版本引用;.NET、Ruby、Python(py/BUILD.bazel)、JavaScript(javascript/selenium-webdriver/BUILD.bazel)各有对应的更新逻辑。也就是说,README 描述的是"如何手工拿协议文件",而该脚本把"拿文件 + 刷新所有语言绑定引用"串成了一次性操作。

四、PDL 到 JSON 的转换链:pdl_to_json 与 pdl.py

拿到两份 PDL 后,各语言绑定消费的实际是 JSON 形态的协议描述。这一转换由三层完成:

1. 各版本目录的 genrule。以 common/devtools/chromium/v152/BUILD.bazel 为例(v150、v151 与之相同),声明了两个 genrule(L11-L37):

genrule(
    name = "browser_protocol",
    srcs = ["browser_protocol.pdl"],
    outs = ["browser_protocol.json"],
    cmd = "$(location //common/devtools:pdl_to_json) $(location :browser_protocol.pdl) --map_binary_to_string=true $@",
    tools = ["//common/devtools:pdl_to_json"],
)

js_protocol 的 genrule 结构相同。default_visibility 则限定了消费方://dotnet/src/webdriver//java/src/org/openqa/selenium/devtools//javascript/selenium-webdriver//py//rb/lib/selenium/devtools——与 README 所述"按需生成各语言绑定"的定位一致。

2. 转换工具 pdl_to_jsoncommon/devtools/BUILD.bazel 中定义(L15-L25):

py_binary(
    name = "pdl_to_json",
    srcs = ["convert_protocol_to_json.py", "pdl.py"],
    main = "convert_protocol_to_json.py",
)

入口脚本 common/devtools/convert_protocol_to_json.py 是 README 中提到的"对 Chromium 官方脚本(third_party/inspector_protocol/convert_protocol_to_json.py)的修改版"。其命令行接口(main,L14-L35):

参数 说明
pdl_file 待解析的 .pdl 输入文件
json_file 输出 .json 文件路径
--map_binary_to_string 设为 true 时,PDL 中的 binary 类型在 JSON 中映射为 string,客户端需要自行做 base64 解码

脚本读取 PDL 文本后交给 pdl.loads() 解析,再以 indent=4 写为 JSON。genrule 中统一传入 --map_binary_to_string=true,意味着 Selenium 生成的协议 JSON 里二进制字段都是 base64 字符串——这对纯语言客户端(C#、Java、Python 等直接发 JSON-RPC 的实现)是必需的形态。

3. PDL 解析器 pdl.pycommon/devtools/pdl.py 是一个逐行正则驱动的解析器(parse,L59-L181),它定义了 PDL 到 JSON 的完整语法规则:

PDL 语法 解析行为
# 开头的行 累积为 description 文本,附到下一个条目上(L76-L80)
(experimental )(deprecated )?domain Name 新建一个 domain 对象,追加到 protocol["domains"](L87-L91)
depends on X 记入该 domain 的 dependencies 列表(L93-L98)
(experimental )(deprecated )?type Id extends (array of )?Base 新增类型定义;基础类型命中 primitiveTypes(integer/number/boolean/string/object/any/array/binary)时写入 type,否则写入 $ref 指向其他定义(L26-L40、L100-L110)
(experimental )(deprecated )?(command|event) Name 新建命令/事件条目(L112-L128)
parameters / returns / properties 建立参数子项列表(L144-L147)
(optional )(array of )?Type param 参数定义;optional 标记可选,array of 生成嵌套 items,参数类型若为 enum 则就地建立枚举字面量列表(L130-L142)
enum 块与缩进字面量行 收集枚举取值(L149-L152、L173-L177)
version + major N / minor N 写入 protocol["version"](L154-L166)
redirect Name 命令重定向(L168-L171)

两个值得注意的实现细节:其一,assignType(L26-L40)对 enum 类型统一降级为 string、对 binarymap_binary_to_string 开关转换为 string,这正是 --map_binary_to_string=true 的落点;其二,任何不匹配上述模式的非空行都会触发 Error in {file}:{i}, illegal token 并直接 sys.exit(1)(L179-L180)——即解析器对 PDL 文本采取严格模式,格式稍有偏差构建即失败,这也是 update_cdp.py 必须先做扁平化等预处理的原因。

五、协议产物与语言侧的消费方式

转换后的 JSON 就是各语言 DevTools 模块的数据源。仓库根目录下的 common/devtools/browser_protocol.json(约 1.7 万行)与 common/devtools/js_protocol.json(约 3400 行)是被显式 exports_files 导出的顶层副本(common/devtools/BUILD.bazel L3-L13),供 //javascript/selenium-webdriver//py//rb/lib/selenium/devtools 直接引用;而 Bazel 构建路径上,各语言则通过版本目录的 genrule 产物生成代码,例如:

  • Python:py/BUILD.bazelbrowser_protocol = "//common/devtools/chromium/{version}:browser_protocol",生成代码输出到 selenium/webdriver/common/devtools/{version}
  • Ruby:rb/lib/selenium/devtools/BUILD.bazel 以同样的 genrule 目标作为输入;
  • Java:java/src/org/openqa/selenium/devtools/ 目录提供 CdpClientGeneratorDevToolsProviderHasDevTools 等基础设施,其可用域集合受所用协议 JSON 约束;
  • .NET:dotnet/src/webdriver/DevTools 目录中的生成代码与 DevToolsDomains.cs(见 scripts/update_cdp.py 的更新逻辑)随协议版本滚动刷新。

从源码结构看,整条链路是:Chromium/V8 源码中的 PDL → common/devtools/chromium/vXX(按版本归档)→ genrule 调 pdl_to_json 转 JSON → 各语言绑定消费 JSON 生成类型化 DevTools API

六、小结与适用前提

回顾 common/devtools/README.md 的主旨并在仓库中得到印证:

  1. 多版本并存是设计而非冗余:滚动保留最近三个 Chromium 里程碑(当前为 v150/v151/v152),由 scripts/update_cdp.py 自动增删;
  2. 手工流程与脚本流程一一对应:查稳定版号 → 建 vXX 目录 → 复制上一版 BUILD → 取 browser_protocol.pdl → 从 DEPSv8_revision → 取 js_protocol.pdl,脚本额外完成 include 扁平化、<script> 标签清洗与各语言绑定引用刷新;
  3. 转换工具链独立可验证convert_protocol_to_json.py 是 Chromium 官方转换脚本的修改版,配合 pdl.py 的严格逐行解析,任何 PDL 语法偏差都会使 Bazel 构建直接失败,保证了入库协议文件与生成 JSON 的一致性;
  4. 适用前提:该机制面向 Chromium 系浏览器(Chrome/Edge 等)的 CDP;协议 JSON 中的 binary 字段已按 --map_binary_to_string=true 映射为 base64 字符串,消费方需自行解码;各语言绑定支持的里程碑以对应目录(如 java/src/org/openqa/selenium/devtoolspy/BUILD.bazel)中的版本引用为准。
登录后查看全文
热门项目推荐
相关项目推荐