全球动态代理 · 静态住宅代理 · 极高质量 · 包月不限量 · 学员低价 · 联系如意进行免费测试 · 欢迎公司及学员采购 · 另有任何海外数据采集需求,随时联系 · 商务合作微信:Charleval
Automation / 示例中心
Examples Center

RuyiPage 示例中心

这里不是简单的文件清单,而是将 RuyiPage 仓库中的 examples 渲染成案例卡。每个例子都带有文件名、分类、难度、不同用法、涉及 API、适用场景与相关页面,并提供 GitHub 源码入口,帮助你从“例子”回到“文档”,也从“文档”回到“例子”。

示例测试服务器:为 network、downloads、cookies、navigation 提供本地接口

examples/test_server.py
GitHub 仓库 已同步源码 查看 GitHub 源码
tools tool
一句话定位

大量示例依赖的本地稳定环境,是测试型章节的重要配套工具。

涉及 API
TestServer.startTestServer.stopTestServer.get_url
展示的不同用法
  • 为网络拦截、下载、Cookie 等示例提供稳定服务端
  • 避免外网波动影响案例复现
适用场景

适合 testing 页面。

相关文档页面
推荐阅读
network-intercept-11downloads-23
完整代码

来源:app/content/ruyipage_examples/test_server.py · 仓库文件:test_server.py

python 注释版
# =============================
# RuyiPage 示例注释版
# 标题: 示例测试服务器:为 network、downloads、cookies、navigation 提供本地接口
# 说明: 大量示例依赖的本地稳定环境,是测试型章节的重要配套工具。
# 你会学到:
#   1. 为网络拦截、下载、Cookie 等示例提供稳定服务端
#   2. 避免外网波动影响案例复现
# 相关 API: TestServer.start, TestServer.stop, TestServer.get_url
# =============================

# -*- coding: utf-8 -*-
"""示例用本地测试服务器。"""

import base64
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse


class TestServer(object):
    """用于 examples 的轻量 HTTP 测试服务器。"""

    def __init__(self, host="127.0.0.1", port=8888):
        self.host = host
        self.port = port
        self._server = None
        self._thread = None

    def start(self):
        if self._server:
            return self

        server = self

        class Handler(BaseHTTPRequestHandler):
            def log_message(self, format, *args):
                return

            def _cors_headers(self):
                return [
                    ("Access-Control-Allow-Origin", "*"),
                    ("Access-Control-Allow-Methods", "GET, POST, OPTIONS"),
                    (
                        "Access-Control-Allow-Headers",
                        "Content-Type, X-Ruyi-Demo, User-Agent",
                    ),
                ]

            def _write_text(self, status, body, headers=None):
                body_bytes = body.encode("utf-8")
                self.send_response(status)
                self.send_header("Content-Type", "text/plain; charset=utf-8")
                self.send_header("Content-Length", str(len(body_bytes)))
                if headers:
                    for name, value in headers:
                        self.send_header(name, value)
                self.end_headers()
                self.wfile.write(body_bytes)

            def _write_json(self, status, payload, headers=None):
                body = json.dumps(payload, ensure_ascii=False)
                body_bytes = body.encode("utf-8")
                self.send_response(status)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Content-Length", str(len(body_bytes)))
                if headers:
                    for name, value in headers:
                        self.send_header(name, value)
                self.end_headers()
                self.wfile.write(body_bytes)

            def do_GET(self):
                parsed = urlparse(self.path)
                path = parsed.path

                if path == "/set-cookie":
                    self._write_text(
                        200,
                        "cookie set ok",
                        headers=[
                            ("Set-Cookie", "server_cookie=server_value; Path=/"),
                            ("Set-Cookie", "session_id=abc123; Path=/; HttpOnly"),
                        ],
                    )
                    return

                if path == "/get-cookie":
                    cookie_header = self.headers.get("Cookie", "")
                    self._write_text(200, cookie_header or "")
                    return

                if path == "/api/data":
                    self._write_json(
                        200,
                        {
                            "status": "ok",
                            "data": {"message": "来自测试服务器的数据"},
                        },
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/headers":
                    headers = {key: value for key, value in self.headers.items()}
                    self._write_json(200, headers, headers=self._cors_headers())
                    return

                if path == "/api/collector":
                    headers = {key: value for key, value in self.headers.items()}
                    self._write_json(
                        200,
                        {
                            "status": "ok",
                            "source": "collector",
                            "message": "stable response body for data collector",
                            "headers": headers,
                        },
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/error":
                    self._write_json(
                        500,
                        {"status": "error", "message": "server error"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/mock-source":
                    self._write_json(
                        200,
                        {"status": "ok", "source": "real-server"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/slow":
                    self._write_json(
                        200,
                        {"status": "ok", "source": "slow-server"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/api/auth":
                    auth_header = self.headers.get("Authorization", "")
                    expected = "Basic " + base64.b64encode(b"user:pass").decode("ascii")
                    if auth_header != expected:
                        self.send_response(401)
                        self.send_header("WWW-Authenticate", 'Basic realm="RuyiTest"')
                        self.send_header("Content-Length", "0")
                        self.end_headers()
                        return

                    self._write_json(
                        200,
                        {"status": "ok", "auth": True, "user": "user"},
                        headers=self._cors_headers(),
                    )
                    return

                if path == "/download/text":
                    self._write_text(
                        200,
                        "hello download",
                        headers=[
                            ("Content-Disposition", 'attachment; filename="test.txt"'),
                            ("Cache-Control", "no-store"),
                        ],
                    )
                    return

                if path == "/download/json":
                    self._write_json(
                        200,
                        {"ok": True},
                        headers=[
                            ("Content-Disposition", 'attachment; filename="test.json"'),
                            ("Cache-Control", "no-store"),
                        ],
                    )
                    return

                if path == "/nav/basic":
                    body = """<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Nav Basic</title></head>
<body><h1>Nav Basic</h1></body></html>"""
                    body_bytes = body.encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html; charset=utf-8")
                    self.send_header("Content-Length", str(len(body_bytes)))
                    self.end_headers()
                    self.wfile.write(body_bytes)
                    return

                if path == "/nav/fragment":
                    body = """<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Nav Fragment</title></head>
<body>
<h1 id='a'>A</h1>
<div style='height:1200px'></div>
<h1 id='b'>B</h1>
</body></html>"""
                    body_bytes = body.encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html; charset=utf-8")
                    self.send_header("Content-Length", str(len(body_bytes)))
                    self.end_headers()
                    self.wfile.write(body_bytes)
                    return

                if path == "/nav/history":
                    body = """<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Nav History</title></head>
<body><h1>Nav History</h1></body></html>"""
                    body_bytes = body.encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html; charset=utf-8")
                    self.send_header("Content-Length", str(len(body_bytes)))
                    self.end_headers()
                    self.wfile.write(body_bytes)
                    return

                self._write_text(404, "not found")

            def do_OPTIONS(self):
                self.send_response(204)
                for name, value in self._cors_headers():
                    self.send_header(name, value)
                self.send_header("Content-Length", "0")
                self.end_headers()

        self._server = ThreadingHTTPServer((self.host, self.port), Handler)
        self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
        self._thread.start()
        return self

    def stop(self):
        if not self._server:
            return
        self._server.shutdown()
        self._server.server_close()
        self._server = None
        self._thread = None

    def get_url(self, path="/"):
        if not path.startswith("/"):
            path = "/" + path
        return f"http://{self.host}:{self.port}{path}"

XPath Picker 综合演示

examples/42_xpath_picker_complex_showcase.py
GitHub 仓库 待同步 查看 GitHub 源码
tools advanced
一句话定位

完整展示 XPath Picker 在复杂节点、iframe、嵌套 iframe 和 open shadow root 中的拾取与代码生成能力。

涉及 API
opts.enable_xpath_pickerlaunch(xpath_picker=True)page.getpicker 自动生成代码链
展示的不同用法
  • 启用 XPath Picker 浮窗
  • 查看绝对 XPath 与相对 XPath
  • 自动生成 RuyiPage 定位代码
  • 处理 iframe 与 shadow root 嵌套结构
适用场景

适合 xpath-picker、selectors、iframe 和 shadow-dom 页面。

推荐阅读
elements-02iframe-13shadow-14

提取 W3C WebDriver BiDi 规范 API 清单

examples/w3c_bidi/extract_w3c_bidi.py
GitHub 仓库 已同步源码 查看 GitHub 源码
tools tool
一句话定位

适合放在 BiDi 对照页,强调项目对协议演进的关注。

涉及 API
FirefoxPagepage.getpage.run_js
展示的不同用法
  • 抓取 W3C BiDi 规范目录和 API 清单
  • 为对照表和文档生成提供原始数据
适用场景

适合 bidi 页面。

相关文档页面
推荐阅读
w3c-compare
完整代码

来源:app/content/ruyipage_examples/w3c_bidi/extract_w3c_bidi.py · 仓库文件:w3c_bidi/extract_w3c_bidi.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 提取 W3C WebDriver BiDi 规范 API 清单
# 说明: 适合放在 BiDi 对照页,强调项目对协议演进的关注。
# 你会学到:
#   1. 抓取 W3C BiDi 规范目录和 API 清单
#   2. 为对照表和文档生成提供原始数据
# 相关 API: FirefoxPage, page.get, page.run_js
# =============================

# -*- coding: utf-8 -*-
"""从W3C WebDriver BiDi规范网页提取所有API(使用正确的选择器)"""

import sys
import io

if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")

from ruyipage import *
import os
import re

# 创建浏览器
page = FirefoxPage()

try:
    print("正在访问 W3C WebDriver BiDi 规范...")
    page.get("https://w3c.github.io/webdriver-bidi/")

    print("等待页面加载...")
    page.wait(3)

    print("\n从目录 (ol.toc li a) 提取所有API...")

    # 提取目录中的所有链接
    toc_script = """
    (() => {
        const items = [];
        document.querySelectorAll('ol.toc li a').forEach(link => {
            const href = link.getAttribute('href');
            const text = link.textContent.trim();
            if (href && text) {
                items.push({
                    href: href,
                    text: text,
                    id: href.replace('#', '')
                });
            }
        });
        return items;
    })()
    """

    toc_items = page.run_js(toc_script)

    print(f"\n找到 {len(toc_items)} 个目录项")

    # 分析提取命令和事件
    commands = {}
    events = {}
    modules = set()

    for item in toc_items:
        text = item["text"]
        id_str = item["id"]

        # 提取命令(格式:The module.command Command)
        cmd_match = re.search(r"The\s+(\w+)\.(\w+)\s+Command", text)
        if cmd_match:
            module = cmd_match.group(1)
            cmd_name = f"{module}.{cmd_match.group(2)}"
            modules.add(module)
            if module not in commands:
                commands[module] = []
            commands[module].append(cmd_name)
            print(f"  命令: {cmd_name}")

        # 提取事件(格式:The module.event Event)
        evt_match = re.search(r"The\s+(\w+)\.(\w+)\s+Event", text)
        if evt_match:
            module = evt_match.group(1)
            evt_name = f"{module}.{evt_match.group(2)}"
            modules.add(module)
            if module not in events:
                events[module] = []
            events[module].append(evt_name)
            print(f"  事件: {evt_name}")

    print(f"\n\n=== 统计结果 ===")
    print(f"模块数: {len(modules)}")
    print(f"命令数: {sum(len(v) for v in commands.values())}")
    print(f"事件数: {sum(len(v) for v in events.values())}")

    print("\n\n=== 按模块分类的命令 ===")
    for module in sorted(commands.keys()):
        print(f"\n{module} 模块 ({len(commands[module])} 个命令):")
        for cmd in sorted(commands[module]):
            print(f"  - {cmd}")

    print("\n\n=== 按模块分类的事件 ===")
    for module in sorted(events.keys()):
        print(f"\n{module} 模块 ({len(events[module])} 个事件):")
        for evt in sorted(events[module]):
            print(f"  - {evt}")

    # 保存结构化数据
    import json

    result = {
        "modules": sorted(list(modules)),
        "commands": commands,
        "events": events,
        "total_commands": sum(len(v) for v in commands.values()),
        "total_events": sum(len(v) for v in events.values()),
    }

    _out_path = os.path.join(os.path.dirname(__file__), "..", "w3c_bidi_apis.json")
    with open(_out_path, "w", encoding="utf-8") as f:
        json.dump(result, f, indent=2, ensure_ascii=False)

    print(f"\n\n结构化数据已保存到: {os.path.abspath(_out_path)}")

finally:
    page.quit()

生成 RuyiPage 与 W3C BiDi 规范对比表

examples/w3c_bidi/generate_comparison.py
GitHub 仓库 已同步源码 查看 GitHub 源码
tools tool
一句话定位

用于补充规范实现率和测试率的对照视图。

涉及 API
离线文档工具
展示的不同用法
  • 读取规范 API 清单
  • 生成实现率和测试率表格
  • 统计缺失 API 并输出 Markdown
适用场景

适合 bidi 页面,不作为新手入门内容。

相关文档页面
推荐阅读
w3c-extract
完整代码

来源:app/content/ruyipage_examples/w3c_bidi/generate_comparison.py · 仓库文件:w3c_bidi/generate_comparison.py

python 注释版
#!/usr/bin/env python
# =============================
# RuyiPage 示例注释版
# 标题: 生成 RuyiPage 与 W3C BiDi 规范对比表
# 说明: 用于补充规范实现率和测试率的对照视图。
# 你会学到:
#   1. 读取规范 API 清单
#   2. 生成实现率和测试率表格
#   3. 统计缺失 API 并输出 Markdown
# 相关 API: 离线文档工具
# =============================

# -*- coding: utf-8 -*-
"""生成RuyiPage与W3C BiDi规范的精确对比表格"""

import sys
import io

if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")

import json
import os

# 读取W3C规范数据
with open("w3c_bidi_apis.json", "r", encoding="utf-8") as f:
    w3c_data = json.load(f)

# RuyiPage已实现的API
ruyipage_commands = {
    "session": [
        "session.status",
        "session.new",
        "session.end",
        "session.subscribe",
        "session.unsubscribe",
    ],
    "browser": [
        "browser.close",
        "browser.createUserContext",
        "browser.getUserContexts",
        "browser.removeUserContext",
        "browser.getClientWindows",
        "browser.setClientWindowState",
        "browser.setDownloadBehavior",
    ],
    "browsingContext": [
        "browsingContext.activate",
        "browsingContext.captureScreenshot",
        "browsingContext.close",
        "browsingContext.create",
        "browsingContext.getTree",
        "browsingContext.handleUserPrompt",
        "browsingContext.locateNodes",
        "browsingContext.navigate",
        "browsingContext.print",
        "browsingContext.reload",
        "browsingContext.setViewport",
        "browsingContext.traverseHistory",
        # 缺少: browsingContext.setBypassCSP
    ],
    "emulation": [
        "emulation.setUserAgentOverride",
        "emulation.setGeolocationOverride",
        "emulation.setTimezoneOverride",
        "emulation.setLocaleOverride",
        "emulation.setScreenOrientationOverride",
        "emulation.setScreenSettingsOverride",
        "emulation.setNetworkConditions",
        "emulation.setTouchOverride",
        # 缺少: setScriptingEnabled, setScrollbarTypeOverride, setForcedColorsModeThemeOverride
    ],
    "network": [
        "network.addIntercept",
        "network.removeIntercept",
        "network.continueRequest",
        "network.continueResponse",
        "network.continueWithAuth",
        "network.failRequest",
        "network.provideResponse",
        "network.addDataCollector",
        "network.removeDataCollector",
        "network.getData",
        "network.disownData",
        "network.setCacheBehavior",
        "network.setExtraHeaders",
    ],
    "script": [
        "script.evaluate",
        "script.callFunction",
        "script.addPreloadScript",
        "script.removePreloadScript",
        "script.getRealms",
        "script.disown",
    ],
    "storage": ["storage.getCookies", "storage.setCookie", "storage.deleteCookies"],
    "input": ["input.performActions", "input.releaseActions", "input.setFiles"],
    "webExtension": ["webExtension.install", "webExtension.uninstall"],
}

ruyipage_events = {
    "browsingContext": [
        "browsingContext.contextCreated",
        "browsingContext.contextDestroyed",
        "browsingContext.domContentLoaded",
        "browsingContext.load",
        "browsingContext.navigationStarted",
        "browsingContext.userPromptOpened",
        "browsingContext.userPromptClosed",
        # 缺少: downloadWillBegin, downloadEnd, fragmentNavigated, historyUpdated,
        #       navigationAborted, navigationCommitted, navigationFailed
    ],
    "network": [
        "network.beforeRequestSent",
        "network.responseStarted",
        "network.responseCompleted",
        "network.fetchError",
        "network.authRequired",
    ],
    "script": [
        "script.realmCreated",
        "script.realmDestroyed",
        # 缺少: script.message
    ],
    "log": ["log.entryAdded"],
    # 缺少: input.fileDialogOpened
}

# 测试覆盖情况
tested_commands = {
    "session": [
        "session.status",
        "session.new",
        "session.end",
        "session.subscribe",
        "session.unsubscribe",
    ],
    "browser": [
        "browser.close",
        "browser.createUserContext",
        "browser.getUserContexts",
        "browser.removeUserContext",
        "browser.getClientWindows",
        "browser.setClientWindowState",
    ],
    "browsingContext": [
        "browsingContext.activate",
        "browsingContext.captureScreenshot",
        "browsingContext.close",
        "browsingContext.create",
        "browsingContext.getTree",
        "browsingContext.handleUserPrompt",
        "browsingContext.locateNodes",
        "browsingContext.navigate",
        "browsingContext.print",
        "browsingContext.reload",
        "browsingContext.setViewport",
        "browsingContext.traverseHistory",
    ],
    "emulation": [],  # 未测试
    "network": [
        "network.addIntercept",
        "network.removeIntercept",
        "network.continueRequest",
        "network.continueResponse",
        "network.continueWithAuth",
        "network.failRequest",
        "network.provideResponse",
        "network.addDataCollector",
        "network.removeDataCollector",
        "network.getData",
        "network.disownData",
        "network.setCacheBehavior",
        "network.setExtraHeaders",
    ],
    "script": [
        "script.evaluate",
        "script.callFunction",
        "script.addPreloadScript",
        "script.removePreloadScript",
        "script.getRealms",
        "script.disown",
    ],
    "storage": ["storage.getCookies", "storage.setCookie", "storage.deleteCookies"],
    "input": ["input.performActions", "input.releaseActions", "input.setFiles"],
    "webExtension": [],  # 未测试
}

# 生成Markdown表格
output = []
output.append("# WebDriver BiDi API 完整对比表(基于W3C官方规范)\n")
output.append("## 📊 总体统计\n")
output.append("| 项目 | W3C规范 | RuyiPage实现 | 已测试 | 实现率 | 测试率 |")
output.append("|------|---------|-------------|--------|--------|--------|")

total_w3c_cmds = w3c_data["total_commands"]
total_w3c_evts = w3c_data["total_events"]
total_rp_cmds = sum(len(v) for v in ruyipage_commands.values())
total_rp_evts = sum(len(v) for v in ruyipage_events.values())
total_tested_cmds = sum(len(v) for v in tested_commands.values())

output.append(
    f"| 命令 | {total_w3c_cmds} | {total_rp_cmds} | {total_tested_cmds} | {total_rp_cmds / total_w3c_cmds * 100:.1f}% | {total_tested_cmds / total_w3c_cmds * 100:.1f}% |"
)
output.append(
    f"| 事件 | {total_w3c_evts} | {total_rp_evts} | {total_rp_evts} | {total_rp_evts / total_w3c_evts * 100:.1f}% | {total_rp_evts / total_w3c_evts * 100:.1f}% |"
)
output.append(
    f"| **总计** | **{total_w3c_cmds + total_w3c_evts}** | **{total_rp_cmds + total_rp_evts}** | **{total_tested_cmds + total_rp_evts}** | **{(total_rp_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%** | **{(total_tested_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%** |"
)

output.append("\n---\n")

# 按模块生成详细对比
for module in sorted(w3c_data["modules"]):
    output.append(f"\n## {module} 模块\n")

    # 命令对比
    if module in w3c_data["commands"]:
        w3c_cmds = w3c_data["commands"][module]
        rp_cmds = ruyipage_commands.get(module, [])
        tested_cmds = tested_commands.get(module, [])

        output.append(
            f"### 命令 ({len(rp_cmds)}/{len(w3c_cmds)} = {len(rp_cmds) / len(w3c_cmds) * 100:.1f}%)\n"
        )
        output.append("| W3C命令 | RuyiPage | 测试 | 状态 |")
        output.append("|---------|----------|------|------|")

        for cmd in sorted(w3c_cmds):
            implemented = "✅" if cmd in rp_cmds else "❌"
            tested = "✅" if cmd in tested_cmds else ("⚠️" if cmd in rp_cmds else "❌")
            status = (
                "完成"
                if cmd in tested_cmds
                else ("已实现未测试" if cmd in rp_cmds else "未实现")
            )
            output.append(f"| {cmd} | {implemented} | {tested} | {status} |")

    # 事件对比
    if module in w3c_data["events"]:
        w3c_evts = w3c_data["events"][module]
        rp_evts = ruyipage_events.get(module, [])

        output.append(
            f"\n### 事件 ({len(rp_evts)}/{len(w3c_evts)} = {len(rp_evts) / len(w3c_evts) * 100:.1f}%)\n"
        )
        output.append("| W3C事件 | RuyiPage | 测试 | 状态 |")
        output.append("|---------|----------|------|------|")

        for evt in sorted(w3c_evts):
            implemented = "✅" if evt in rp_evts else "❌"
            tested = "✅" if evt in rp_evts else "❌"
            status = "完成" if evt in rp_evts else "未实现"
            output.append(f"| {evt} | {implemented} | {tested} | {status} |")

    output.append("")

# 未实现的API清单
output.append("\n## 🚫 未实现的W3C API清单\n")
output.append("### 命令\n")
missing_cmds = []
for module in w3c_data["commands"]:
    w3c_cmds = w3c_data["commands"][module]
    rp_cmds = ruyipage_commands.get(module, [])
    for cmd in w3c_cmds:
        if cmd not in rp_cmds:
            missing_cmds.append(cmd)

for i, cmd in enumerate(missing_cmds, 1):
    output.append(f"{i}. ❌ **{cmd}**")

output.append("\n### 事件\n")
missing_evts = []
for module in w3c_data["events"]:
    w3c_evts = w3c_data["events"][module]
    rp_evts = ruyipage_events.get(module, [])
    for evt in w3c_evts:
        if evt not in rp_evts:
            missing_evts.append(evt)

for i, evt in enumerate(missing_evts, 1):
    output.append(f"{i}. ❌ **{evt}**")

# 未测试的API清单
output.append("\n## ⚠️ 已实现但未测试的API\n")
untested = []
for module in ruyipage_commands:
    rp_cmds = ruyipage_commands[module]
    tested_cmds = tested_commands.get(module, [])
    for cmd in rp_cmds:
        if cmd not in tested_cmds:
            untested.append(cmd)

for i, cmd in enumerate(untested, 1):
    output.append(f"{i}. ⚠️ **{cmd}**")

# 总结
output.append("\n## 🏆 总结\n")
output.append(
    f"- ✅ **命令实现率**: {total_rp_cmds}/{total_w3c_cmds} = **{total_rp_cmds / total_w3c_cmds * 100:.1f}%**"
)
output.append(
    f"- ✅ **事件实现率**: {total_rp_evts}/{total_w3c_evts} = **{total_rp_evts / total_w3c_evts * 100:.1f}%**"
)
output.append(
    f"- ✅ **总体实现率**: {total_rp_cmds + total_rp_evts}/{total_w3c_cmds + total_w3c_evts} = **{(total_rp_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%**"
)
output.append(
    f"- ✅ **测试覆盖率**: {total_tested_cmds + total_rp_evts}/{total_w3c_cmds + total_w3c_evts} = **{(total_tested_cmds + total_rp_evts) / (total_w3c_cmds + total_w3c_evts) * 100:.1f}%**"
)
output.append(
    f"- ❌ **缺失API数**: {len(missing_cmds) + len(missing_evts)} 个({len(missing_cmds)}命令 + {len(missing_evts)}事件)"
)
output.append(f"- ⚠️ **未测试API数**: {len(untested)} 个")

output.append("\n---\n")
output.append(
    "**数据来源**: W3C WebDriver BiDi 规范 (https://w3c.github.io/webdriver-bidi/)"
)
output.append("\n**生成时间**: 2026-04-06")

# 保存到文件
result_text = "\n".join(output)
_out_path = os.path.join(os.path.dirname(__file__), "W3C_BIDI_COMPARISON.md")
with open(_out_path, "w", encoding="utf-8") as f:
    f.write(result_text)

print(result_text)
print(f"\n\n对比表格已保存到: {os.path.abspath(_out_path)}")