← 开源
LoseNine

ruyipage

下一代Python的web自动化过检测框架,AI自我调试运行,trace日志可分析一切网页链路,通过一切网站检测的Firefox指纹浏览器。RuyiPage is a Python-based Firefox automation framework built on the next-generation WebDriver BiDi protocol.

AI EngineeringGive agent toolsPython
在 GitHub 打开
增长势头
+024 小时新增 Star0.0%
1.84k
Star
222
Fork
+12
本周
2
贡献者
创建于 2026-04-07 · 更新于 2026-10-15 · 今日第 9393 名
主要开发者
README

ruyiPage

简体中文 | English

专用于 AI 分析 和 数据采集 场景,可拦截任意请求响应包。自带好用的过检测 Win/Linux 火狐指纹浏览器内核。

Built for AI analysis and data capture workflows, with the ability to intercept arbitrary request and response packets. Ships with a battle-tested anti-detection Firefox fingerprint browser engine for Windows and Linux.

下一代自动化框架

  • 自带过检测火狐内核
  • 大量 isTrusted 原生动作,无自动化检测点
  • 支持多种 JS 事件构造附加 ruyi: true,让 Event / InputEvent / MouseEvent / KeyboardEvent 等事件的 isTrusted 更贴近真实交互
  • 支持 ADS 等指纹浏览器直接自动化接管
  • 自带 HTTP / SOCKS5 密码代理支持,支持一个 tab 一个密码代理
  • 基于 Firefox + WebDriver BiDi
  • 可以直接获取 closed shadow root 节点
  • 内置 JS 断点调试(page.debugger):断点 / 条件断点 / 日志断点 / 单步 / 调用栈 / 作用域 / 读源码 / 异常暂停 / 属性监视点
  • 更适合高风控场景

❤️ 赞助商

想出现在这里?

🎉 感谢 FastAIToken 对本项目的赞助!FastAIToken 是面向开发者的 AI API 聚合平台,支持 OpenAI、Claude、Gemini 等主流大模型,充值 1:1,1 元 = 1 美元 API 额度,让开发者以更低成本、更便捷地使用全球领先的大模型服务,国产模型 deepseek、glm、kimi 统一满血官 key 低价。

🚀 平台提供多种渠道自由选择:超级低价的 0.06x OpenAI 福利分组(限时)、低至 0.2x OpenAI 分组、0.7x Claude 95% 固定缓存、1.5x Claude Max 渠道;同时提供公开状态页,实时展示各分组的可用率、延迟及运行状态,服务透明可靠,并提供 7×24 小时真人技术支持(非机器人),快速响应开发者需求。

🦊 包含 GPT 全系列,以及 Claude Opus 的 Kiro 和 Max 渠道,包纯度,实用耐蹬,自用:http://www.fastaitoken.com/register

实战展示

下面这些图放的是实际场景展示。为了在 GitHub 首页里更紧凑,我这里用两列表格展示。

可直接通过 Cloudflare 5s 盾

可直接通过 hCaptcha

可直接通过 DataDome

可直接进入 Outlook Mail

可直接进入 Google Mail

bet365 实战展示

reCAPTCHA 评分 0.9 展示

指纹浏览器指纹页展示

Firefox 路线真实场景能力
更适合高风控页面、登录流、验证码与真实交互场景

这些展示图用于说明 ruyiPage 在 Firefox 路线下的真实场景能力。 如果目标站点风控更强,仍建议优先配合本项目推荐的 Firefox 内核方案,或任意可用的火狐指纹浏览器使用。


内核两项底层能力:指纹与追踪

配套内核在 Firefox 源码层面加了两套东西。它们不依赖 BiDi,页面里的 JS 看不见。

指纹(fpfile)

一个 --fpfile= 文本文件决定浏览器对外呈现的整套指纹:UA、语言、时区、屏幕、CPU 核数、 触摸、Canvas / 音频扰动、WebGL 显卡参数、字体白名单、WebRTC、语音列表、地理位置,以及 HTTP / SOCKS5 代理凭据。所有值都在 C++ 原生 getter 里改写,不包装任何 JS 函数, 所以 toString() 仍是 [native code],原型链形状不变。navigator.webdriver 恒为 false。

多数场景不用手写:opts.smart_fingerprint() 会探测出口 IP、匹配语言/时区、从 22 套真机 硬件特征里抽一套自动生成。要固定某台机型或调整单个字段时,看 fingerprint/fpfile指纹说明.md (English)——每个 key 的取值、别名、影响的 API 和验收方法。

追踪(MOZ_DOM_* 环境变量)

启动前设几个环境变量,内核就会把页面运行时发生的事写成日志:每一次 JS 函数调用 (含参数、返回值、闭包变量、调用树)、DOM 属性读写、Cookie / Storage 操作、异常、 WASM 模块与跨边界调用、HTTP 报文、WebSocket 帧;还能一次性导出本构建暴露的全部宿主接口, 用来对照页面探测了什么、什么在这个环境里不存在。另有一组 MOZ_DOM_API_* 可以直接 定制 Date.now / performance.now / Math.random / crypto.getRandomValues 的返回值。

$env:MOZ_DOM_TRACE = "1"
$env:MOZ_DOM_TRACE_FILE = "D:\trace\run.jsonl"
$env:MOZ_DOM_JSCALL_TRACE = "1"
& firefox.exe --new-instance -no-remote -profile "D:\profile"

这是分析反爬脚本、补 JS 运行环境时的主要工具。开关速查见 trace/trace-cheatsheet.md(63 个开关,含取值、默认值、 生效条件),输出格式、因果链查法和排障见 trace/README.md, JSCall 二进制输出的解码器在 trace/tools/jscall_decode.py。


安装与使用

安装

pip install ruyiPage --upgrade

如果你是首次安装,也可以直接用上面的命令获取最新版。

如果需要异步(async/await)支持:

pip install ruyiPage[async] --upgrade

这会额外安装 greenlet 和 websockets,同步 API 完全不受影响。

如果你是从源码运行,或给学员分发项目源码,建议同时安装项目依赖:

pip install -r requirements.txt

安装后建议先确认:

python -c "import ruyipage; print(ruyipage.__version__)"

安装配套 Firefox runtime

ruyiPage 提供了类似 Playwright 的浏览器安装方式。安装 Python 包后,建议执行:

python -m ruyipage install

这个命令会从 ruyiPage 的 GitHub release 下载推荐的 Firefox runtime 并安装到用户缓存目录,默认不校验 hash,便于项目方直接更新 release 资产。后续直接调用 launch() 时,会优先使用这个 runtime;如果你显式传入 browser_path,仍然以你传入的路径为准。

常用命令:

python -m ruyipage install --dry-run     # 只查看下载和安装计划,不下载文件
python -m ruyipage install --force       # 强制重新下载安装
python -m ruyipage install --from-file ./firefox.zip
python -m ruyipage path                  # 输出当前已安装 runtime 的 Firefox 路径
python -m ruyipage doctor                # 查看当前 runtime 安装状态

如果你本机已经装好了 Firefox,或者你使用的是便携版 / 指纹浏览器,也可以跳过这个步骤,继续通过 browser_path 显式指定浏览器路径。

最简单启动

from ruyipage import FirefoxPage

page = FirefoxPage()
page.get("https://www.example.com")
print(page.title)
page.quit()

异步(async/await)启动

import asyncio
from ruyipage.aio import launch

async def main():
    page = await launch()
    await page.get("https://www.example.com")
    title = await page.get_title()
    print(title)

    el = await page.ele("#search")
    await el.click_self()
    await el.input("hello async")

    await page.quit()

asyncio.run(main())

异步 API 的方法名与同步版完全一致,只需加 async/await。 属性(如 page.title)变为异步方法(如 await page.get_title())。 完整示例见根目录 quickstart_bing_search_async.py 和 quickstart_cloudflare_async.py。

JS 事件 isTrusted 对比能力

ruyiPage 不只是支持原生点击、输入、悬停这类高 isTrusted 动作,也支持在多种 JS 事件构造里附加 ruyi: true,用于让事件的 isTrusted 表现与真实交互更一致。

例如:

new Event('change', { bubbles: true, ruyi: true })
new InputEvent('input', { bubbles: true, data: 'A', inputType: 'insertText', ruyi: true })
new MouseEvent('click', { bubbles: true, clientX: 12, clientY: 24, ruyi: true })
new KeyboardEvent('keydown', { bubbles: true, key: 'Enter', code: 'Enter', ruyi: true })

可直接运行综合示例:

python examples/45_js_setter_untrusted_input.py

这个示例会对比普通 JS 事件与 ruyi: true 事件的 isTrusted,覆盖:

  • Event
  • InputEvent
  • KeyboardEvent
  • MouseEvent
  • FocusEvent
  • CustomEvent
  • PointerEvent
  • WheelEvent

指定 Firefox 路径和 userdir

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions()
opts.set_browser_path(r"D:\Firefox\firefox.exe")
opts.set_user_dir(r"D:\ruyipage_userdir")

page = FirefoxPage(opts)
page.get("https://www.example.com")
print(page.title)
page.quit()

其中:

  • browser_path:Firefox 可执行文件路径。适合 Firefox 不在默认安装目录、有多个版本或使用便携版的情况。
  • user_dir:Firefox 的 profile / 用户目录。适合想复用登录状态、保留 Cookie / 本地存储、复用扩展和首选项的情况。如果不设置,ruyiPage 会自动创建临时 profile,适合一次性测试,关闭后通常会被清理。

更适合新手的 launch

from ruyipage import launch

page = launch(
    browser_path=r"D:\Firefox\firefox.exe",
    user_dir=r"D:\ruyipage_userdir",
    headless=False,
    close_on_exit=True,
)

page.get("https://www.example.com")
print(page.title)
page.quit()

其中:

  • close_on_exit=True 表示 Python 程序退出时,自动关闭由 ruyiPage 启动的浏览器。
  • 默认启动会在 10000-32767 中随机选择可用远程调试端口(避开系统 TCP 动态端口段,以免探测通过后端口被出站连接抢走)。需要给其他进程接管时,用 page.browser.address 获取真实地址;确实需要固定地址时再传 port=12000 或其他 1w 以上端口。
  • 如果你希望脚本退出后保留浏览器窗口继续手动操作,可以改成 close_on_exit=False。
  • 如果你用的是 attach() 或 existing_only(True) 接管已有浏览器,即使开启 close_on_exit=True,退出时也只会断开连接,不会误关外部浏览器。

FirefoxOptions 常用 API

如果你准备把浏览器启动行为写得更可控,推荐直接使用 FirefoxOptions。

先看一个覆盖常见选项的完整例子:

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions()
opts.set_browser_path(r"D:\Firefox\firefox.exe")
opts.set_user_dir(r"D:\ruyipage_userdir")
opts.set_proxy("http://127.0.0.1:7890")
opts.set_window_size(1440, 900)
opts.headless(False)
opts.private_mode(False)
opts.close_on_exit(True)

page = FirefoxPage(opts)
page.get("https://www.example.com")
print(page.title)
page.quit()

下面这张表把目前用户可直接使用的 opt 选项做一个集中说明。

方法 作用 常见场景
set_browser_path(path) 指定 Firefox 可执行文件路径 Firefox 不在默认目录、使用便携版、机器上装了多个 Firefox
set_address(address) 设置调试地址 host:port 你已经有固定调试地址,想直接连指定实例
set_port(port) 设置固定远程调试端口,并关闭随机端口 需要稳定地址用于 attach / 调试,或明确控制端口
set_random_port(start=10000, end=65535) 随机选择可用远程调试端口 新启动的默认策略,用于避开 9222 这类可预测端口
set_auto_port(True) 按顺序自动寻找可用端口 想要可预测的批量启动行为,但不想手动挑每个端口
existing_only(True) 只接管已有浏览器,不启动新浏览器 连接手动启动的 Firefox、ADS、指纹浏览器
set_retry(times, interval) 设置连接重试次数和间隔 启动慢、远程环境抖动、端口就绪较慢
set_profile(path) 指定 Firefox profile 目录 想长期复用登录态、Cookie、扩展、首选项
set_user_dir(path) set_profile() 的新手友好别名 教程或团队脚本里更习惯写 user_dir
close_on_exit(True/False) 设置 Python 退出时是否自动关闭浏览器 默认 True,适合脚本跑完自动收尾;传 False 可保留浏览器继续手动操作
private_mode(True/False) 开启 Firefox 原生私密模式 不想带上普通窗口历史状态,想走私密会话
headless(True/False) 设置无头模式 服务器运行、后台任务、无需显示界面
set_argument(arg, value=None) 追加自定义启动参数 需要透传 Firefox 原生启动参数
remove_argument(arg) 移除之前设置过的启动参数 复用配置对象时撤销某个参数
allow_system_access(True/False) 允许或禁止 WebDriver 访问 Firefox 特权上下文 仅在父进程、浏览器 UI、扩展或其他特权上下文确实需要自动化时开启
set_pref(key, value) 写入 Firefox 首选项 调整 about:config、代理策略、下载行为等
set_window_size(width, height) 设置启动窗口大小 控制初始分辨率、适配目标站点布局
set_proxy(proxy) 设置 HTTP / HTTPS / SOCKS 代理 需要代理出口、IP 切换、地域访问
set_download_path(path) 设置默认下载目录 自动下载文件并落盘到固定目录
set_load_mode(mode) 设置页面加载等待策略 在速度和稳定性之间做取舍
set_timeouts(base, page_load, script) 设置元素查找、页面加载、脚本执行超时 页面慢、接口慢、脚本执行时间较长
set_user_prompt_handler(handler) 设置 alert / confirm / prompt 默认处理策略 自动接受或取消弹窗,避免流程被阻塞
set_fpfile(path) 通过 --fpfile 传入指纹配置文件 配合支持该参数的 Firefox / 指纹浏览器使用
enable_xpath_picker(True/False) 启用页面 XPath 选择浮窗 录元素、看 XPath、生成定位代码
enable_action_visual(True/False) 启用鼠标行为可视化调试 调试拟人移动、点击轨迹、键盘输入
quick_start(...) 一次性设置常用启动参数 给新手脚本或快速演示准备统一入口

说明:

  • close_on_exit(True) 默认开启,但只会自动关闭 ruyiPage 自己启动的浏览器。
  • 如果你是通过 existing_only(True) 或 attach() 接管外部浏览器,Python 退出时只会断开连接,不会误关用户手动打开的浏览器。
  • 不设置 user_dir / profile 时,ruyiPage 会自动创建临时 profile,更适合一次性脚本。
  • set_fpfile() 当前主要是把路径通过 --fpfile=... 传给浏览器,并读取其中的代理认证字段;如果 fpfile 中包含 SOCKS5 的 host/port,且指定了 user_dir,ruyipage 会写入对应的 profile 代理 prefs。它不是一个自动填充所有浏览器指纹参数的万能入口。
  • quick_start() 适合快速开始,但不是全部配置项的替代品;需要精细控制时,仍建议直接组合 FirefoxOptions 的各个方法。
  • Windows 提权会话会自动启用 system access,以兼容 Firefox 155 的远程调试限制;其他环境默认关闭,因为该能力会扩大远程调试权限。可用 allow_system_access(True/False) 显式覆盖。它只能在 Firefox 启动时生效;使用 attach() 时,外部 Firefox 必须预先带 --remote-allow-system-access 启动。

如果你只是想快速启动,优先用 launch();如果你想把浏览器行为写得更明确、更适合对外给用户使用,优先用 FirefoxOptions。

开启隐私模式

from ruyipage import FirefoxOptions, FirefoxPage, launch

# 方式一:在配置对象上开启 Firefox 私密浏览模式
opts = FirefoxOptions()
opts.private_mode(True)

page = FirefoxPage(opts)
page.get("https://www.example.com")
page.quit()

# 方式二:直接用 launch()
page = launch(private=True)

# 带代理快速启动
page = launch(proxy="http://127.0.0.1:7890")
page.get("https://www.example.com")
page.quit()

说明:

  • private=True / opts.private_mode(True) 会为 Firefox 增加 -private 启动参数
  • 这和默认的临时 profile 不是一回事
  • 如果你只是想要一次性会话,不复用历史数据,不传 user_dir 也可以
  • 完整示例可参考 examples/ 目录

启用 XPath Picker

from ruyipage import FirefoxOptions, FirefoxPage, launch

# 方式一:在 FirefoxOptions 上开启
opts = FirefoxOptions()
opts.enable_xpath_picker(True)

page = FirefoxPage(opts)
page.get("https://www.example.com")

# 方式二:直接用 launch()
page = launch(xpath_picker=True)
page.get("https://www.example.com")

启用后,页面右下角会出现一个半透明磨砂玻璃浮窗:

  • 点击页面元素时,会锁定当前结果
  • 浮窗会显示元素名字、文本、XPath 绝对路径、XPath 相对路径、元素中心 (x, y)
  • 内置 ruyiPage代码生成 选项卡,会自动生成对应元素获取代码
  • iframe、嵌套 iframe、open / closed shadow root 场景会自动拼好访问链
  • XPath (absolute)、XPath (relative)、ruyiPage代码生成 都支持一键复制
  • 锁定后不会继续切换到其他元素
  • 点击浮窗里的 继续选择 后,才会重新允许选择其他元素
  • 点击浮窗里的 暂停选择 可暂时停止拦截页面点击
  • 点击浮窗里的 收起 可折叠为右下角小胶囊,再点击即可展开

推荐直接运行用户示例:

python examples/42_xpath_picker_complex_showcase.py

这个示例会打开一套专门的复杂测试页面,覆盖:

  • 普通页面元素
  • 同源 iframe / 嵌套 iframe
  • open / closed shadow root
  • 复杂文本节点与 SVG 节点

也可以一次性获取当前页面和所有子 frame 内的 open / closed shadow root:

roots = page.shadow_roots(mode="all")  # all / open / closed
for root in roots:
    item = root.ele("#inside")

如果只想扫描当前 browsing context,不递归 iframe:

roots = page.shadow_roots(mode="all", include_frames=False)

鼠标行为可视化调试

ruyiPage 现在支持 action_visual=True 的鼠标行为可视化调试模式,适合排查自动化流程里“鼠标到底移动到了哪里、实际点到了哪里”这类问题。

开启后会显示:

  • BiDi 鼠标移动轨迹可视化
  • BiDi 点击位置高亮 / 闪烁提示
  • 当前鼠标坐标
  • 当前点击目标元素高亮
  • 框架内置 JS click / JS input 的鼠标反馈

当前这套调试模式聚焦在鼠标行为,主要覆盖:

  • page.actions.move_to() / move() / human_move()
  • page.actions.click() / double_click() / human_click()
  • page.actions.drag_to() / hold() / release()
  • ele.click.left() / click_self() / double_click()
  • ele.click.by_js()
  • ele.input(..., by_js=True) 的鼠标定位反馈

最简单启动方式:

from ruyipage import launch

page = launch(action_visual=True, headless=False)

如果你是通过 options 配置,也可以这样开启:

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions()
opts.enable_action_visual(True)
opts.headless(False)

page = FirefoxPage(opts)
page.get('https://www.example.com')

如果你想直接看本地完整演示,可以运行:

python examples/42_2_action_visual_showcase.py

如果你想专门演示 human_move() / human_click() 的拟人轨迹,并同时观察 两套算法(bezier / windmouse)的鼠标路径差异,可以运行:

python examples/46_human_behavior_showcase.py

这个示例会:

  • 自动打开专门的本地 HTML 演示页
  • 开启 action_visual=True,直接显示鼠标轨迹
  • 依次演示 bezier 和 windmouse 两套拟人轨迹算法
  • 再演示 human_type() 的拟人输入效果

相关接口:

opts.set_human_algorithm("windmouse")

page.actions.human_move(ele, algorithm="bezier", style="arc").perform()
page.actions.human_move(ele, algorithm="windmouse").perform()
page.actions.human_click(ele, algorithm="windmouse").perform()

说明:

  • algorithm 可选 "bezier" 或 "windmouse"
  • style 仅对 bezier 生效
  • 如果不传 algorithm,会优先使用 FirefoxOptions.set_human_algorithm() 的默认值

该示例会使用专门的本地鼠标演示页,集中展示:

  • BiDi 鼠标轨迹
  • 点击位置与目标高亮
  • 拖拽轨迹
  • JS click 的可视化反馈

多进程 / 跨脚本连接已有浏览器

如果你的 Firefox 已经由 ruyiPage 启动(或其他方式以 --remote-debugging-port 参数在后台运行),另一个 Python 进程 可以通过 attach() 直接连上去,不会启动新浏览器。

用法:

from ruyipage import attach

# 连接到之前由 ruyiPage 启动的 Firefox。
# 默认随机端口启动时,可从 page.browser.address 获取这个地址。
page = attach("127.0.0.1:12000")

print(page.title)
print(page.url)

典型场景:

  • 主进程启动 Firefox 并保持运行
  • 其他进程 / 脚本通过 attach() 连上去,复用同一个浏览器实例
  • 多个进程可以同时 attach 到同一端口,各拥有独立的 BiDi 会话

等价写法(直接使用 FirefoxOptions):

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions().set_address(addr).existing_only(True)
page = FirefoxPage(opts)

注意: 直接传地址字符串 FirefoxPage('127.0.0.1:12000') 会尝试启动新浏览器, 而不是连接已有的。如果你要连接已有实例,请使用 attach() 或 显式设置 existing_only(True)。


接管已打开的浏览器

如果 Firefox 已经是你手动打开的,或者是指纹浏览器先打开的,也可以直接接管现有实例。

这套方式适用于任意 Firefox 内核指纹浏览器,包括 ADS / FlowerBrowser 这类产品。 如果浏览器允许固定启动参数,建议使用 1w 以上端口,避免继续使用 9222:

--remote-debugging-port=12000

如果后台会把它改写成随机端口,也可以直接使用按进程特征的自动探测接管。

from ruyipage import auto_attach_exist_browser_by_process

page = auto_attach_exist_browser_by_process(
    latest_tab=True,
)

print(page.browser.address)
print(page.title)
print(page.url)

适合这些情况:

  • 你想先手动打开 Firefox,再让 ruyiPage 接管
  • 你想先启动指纹浏览器,再从业务脚本里连进去
  • 你使用的是 ADS / FlowerBrowser,真实调试端口会随机变化
  • 你不想手动维护端口范围,希望直接按 Firefox 进程特征自动探测

项目定位与技术路线

ruyiPage 是一个面向 Firefox 浏览器自动化 的 Python 库,底层协议来自:

面向 Firefox 的高层自动化框架,核心思想是 用 WebDriver BiDi 做底层、用新手易用 API 做上层。

与大量依赖 CDP(Chrome DevTools Protocol)的自动化库不同,ruyiPage:

  • 以 Firefox 为核心浏览器,以 WebDriver BiDi 为核心协议,不依赖 CDP
  • 天然没有 CDP 路线的暴露面,更贴近 W3C 新一代浏览器自动化协议方向
  • 原生动作链优先,尽量让输入、拖拽、点击等行为保持 isTrusted
  • 内置拟人行为能力,更适合高风控页面的真实交互场景
  • 支持网络劫持、拦截、mock、collector 等能力
  • 支持 user context 隔离,适合同浏览器多账号、多会话并行
  • 高层 API 可直接上手,更适合新手和团队统一维护

BiDi 规范同步

仓库内维护了可复现的 W3C Editor's Draft 快照和源码覆盖报告:

当前核心规范快照中的 67 个命令均有 _bidi 底层封装,24 个事件均可通过 page.events 通用入口订阅。Bluetooth、Permissions 等外部扩展规范单独统计; 浏览器是否实现某个新命令仍取决于 Firefox 版本。

python examples/w3c_bidi/extract_w3c_bidi.py --check
python examples/w3c_bidi/generate_comparison.py --check

高风控场景推荐

如果你的目标站点对自动化非常敏感,优先推荐使用本项目提供的 Firefox 内核方案,或配合任意 Firefox 指纹浏览器使用:

建议流程:1) 优先使用 Firefox 内核方案 → 2) 再用 ruyiPage 做自动化控制,整体效果更稳定。


能力总览

在看详细文档前,你可以先看这张总表,快速了解 ruyiPage 现在已经能做什么。

能力大类 高层入口 典型能力
页面导航 page.get() / page.back() / page.forward() 打开页面、刷新、前进后退
元素查找 page.ele() / page.eles() / ele.ele() CSS/XPath/Text 定位、容器内继续查找
元素交互 ele.click_self() / ele.input() / ele.attr() / ele.text 点击、输入、取属性、读文本
动作链 page.actions 键盘、鼠标、拖拽、滚轮、拟人动作
触摸输入 page.touch tap、long press 等触摸操作
Cookies page.get_cookies() / page.set_cookies() / page.delete_cookies() 读取、设置、删除 Cookie
下载 page.downloads 设置下载目录、等待下载事件、验证落盘
PDF / 截图 page.save_pdf() / page.screenshot() 页面打印 PDF、保存截图
弹窗处理 page.wait_prompt() / page.accept_prompt() / page.set_prompt_handler() alert / confirm / prompt
导航事件 page.navigation navigationStarted、load、historyUpdated 等
通用事件 page.events browsingContext / network / script / input / log 事件
网络控制 page.capture / page.network / page.intercept 被动抓包、请求头、缓存控制、拦截、mock、fail、collector
浏览上下文 page.contexts getTree、create tab/window、reload、viewport、screencast
浏览器级能力 page.browser_tools user context、client window
脚本能力 page.get_realms() / page.eval_handle() / page.disown_handles() realms、远程对象句柄、preload script
Emulation page.emulation UA、viewport、screen、orientation、媒体特征、viewport meta、JS 开关
JS 断点调试 page.debugger 断点、条件断点、日志断点、事件/XHR 断点、属性监视点、单步、调用栈、作用域、读源码、对象展开、Map/Set 内容、调用 getter、远程调用函数、Promise 状态、异常暂停、黑盒化
WebExtension page.extensions 安装目录扩展、安装 xpi、卸载
本地存储 page.local_storage / page.session_storage 读写本地存储和会话存储

和其他框架怎么选

下面这个表不讨论“谁绝对更强”,只突出你最关心的几个点:

  • 各自主要偏向什么浏览器
  • 是否依赖 CDP
  • CDP 暴露面强不强
  • Firefox / BiDi 支持度怎么样
  • 针对性被检测情况怎么样
框架 主要浏览器方向 底层协议 CDP 暴露面 Firefox / BiDi 支持度 针对性被检测
ruyiPage Firefox WebDriver BiDi 无 CDP 暴露面 高,主路线就是 Firefox + BiDi 低,原生 BiDi + isTrusted 行为 + 拟人操作,更适合高风控场景;配合本项目推荐的 Firefox 内核方案或任意火狐指纹浏览器会更稳定
Playwright Chromium / Firefox / WebKit 自有协议,很多能力仍偏 Chromium 中到高 中,支持 Firefox,但不是以 Firefox BiDi 为核心设计 中到高,很多站点会优先针对主流自动化指纹做识别
Selenium 多浏览器 WebDriver Classic + 部分 BiDi 低到中 中,兼容广,但高层 BiDi 能力不算强 中,传统自动化特征和使用面都比较广
Puppeteer Chromium CDP 高 低,基本不是 Firefox 主战场 高,CDP 路线暴露面更明显,也更容易被针对性检测
DrissionPage Chromium 混合驱动思路,核心仍偏 Chromium 中到高 低,Firefox 不是主方向 中到高,更偏 Chromium 自动化场景,同样容易落入主流检测面

一句话建议

  • 你主做 Firefox 自动化:优先 ruyiPage
  • 你要 多浏览器统一自动化:优先 Playwright / Selenium
  • 你主做 Chromium/CDP:优先 Puppeteer / Playwright
  • 你想要 Firefox + 不依赖 CDP + BiDi 高层封装:ruyiPage 是更对路的选择

根目录快速开始示例

1. Bing 搜索示例

文件:quickstart_bing_search.py

它会:

  • 打开 Bing
  • 输入关键词
  • 回车搜索
  • 抓取前 3 页结果
  • 打印标题、URL、摘要

核心写法:

from ruyipage import FirefoxOptions, FirefoxPage, Keys

opts = FirefoxOptions()
page = FirefoxPage(opts)

page.get("https://cn.bing.com/")
page.ele("#sb_form_q").input("小肩膀教育")
page.actions.press(Keys.ENTER).perform()

for item in page.eles("css:#b_results > li.b_algo"):
    title_ele = item.ele("css:h2 a")
    title = title_ele.text
    url = title_ele.attr("href")

2. Cloudflare / Copilot 示例

文件:quickstart_cloudfare.py

它会:

  • 打开 Copilot
  • 尝试寻找输入框并发问
  • 自动尝试处理 Cloudflare
  • 最后打印完整 Cookie

这个示例更适合你理解:

  • page.handle_cloudflare_challenge()
  • page.get_cookies(all_info=True)
  • FirefoxOptions 如何写进新手脚本

3. 指纹浏览器示例

文件:quickstart_fingerprint_browser.py

它会:

  • 启动 Firefox 指纹浏览器
  • 通过 --fpfile=... 加载指纹文件
  • 打开 browserscan 检查指纹结果
  • 叠加地理位置、时区、语言、请求头、屏幕尺寸模拟

核心写法:

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions()
opts.set_browser_path(r"C:\Program Files\Mozilla Firefox\firefox.exe")
opts.set_fpfile(r"C:\fingerprints\profile1.txt")

page = FirefoxPage(opts)
page.get("https://www.browserscan.net/zh")

page.emulation.set_geolocation(39.9042, 116.4074, accuracy=100)
page.emulation.set_timezone("Asia/Tokyo")
page.emulation.set_locale(["ja-JP", "ja"])
page.network.set_extra_headers({
    "Accept-Language": "ja-JP,ja;q=0.9"
})
page.emulation.set_screen_size(1366, 768, device_pixel_ratio=2.0)
page.refresh()

适用场景:

  • 需要把 Firefox 指纹浏览器和 ruyiPage 配合使用
  • 希望把指纹文件、语言、请求头、屏幕参数一起带上
  • 想直接验证 browserscan 等站点上的指纹表现

4. HTTP 密码代理示例

如果你使用的是本项目自己的 Firefox 内核,那么内核已经支持从 fpfile 自动读取 HTTP 代理用户名密码。

也就是说,业务层只需要:

  • opts.set_proxy("http://host:port")
  • opts.set_fpfile("...")

当 fpfile 中存在以下字段时,内核会自动处理 HTTP 代理认证,不需要再额外调用认证 API:

httpauth.username:your-proxy-username
httpauth.password:your-proxy-password

完整示例见:examples/38_proxy_auth_ipinfo.py

核心写法:

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions()
opts.set_proxy("http://your-proxy-host:8080")
opts.set_fpfile(r"C:\path\to\your\profile1.txt")

page = FirefoxPage(opts)
page.get("http://ipinfo.io/json")

SOCKS5 密码代理:user_dir + fpfile

如果 SOCKS5 代理地址和密码保存在 fpfile 中,业务代码不需要提前准备 Firefox profile。传入 user_dir 和 fpfile 后,ruyipage 会在该目录写入 user.js,配置 network.proxy.socks、network.proxy.socks_port、network.proxy.socks_version=5 和 network.proxy.socks_remote_dns=true。用户名和密码继续留在 fpfile,不会写入 user.js。

fpfile 可以使用你的 Firefox 内核支持的格式,例如:

socksauth.host:proxy.example.com
socksauth.port:1000
socksauth.username:<从安全配置读取,不要写入公开文档或代码>
socksauth.password:<从安全配置读取,不要写入公开文档或代码>
from ruyipage import launch

page = launch(
    browser_path=r"C:\firefox\firefox.exe",
    user_dir=r"C:\firefox\socks5-profile",
    fpfile=r"C:\firefox\fp.txt",
    headless=False,
)
page.get("https://ipinfo.io/json")

也可以继续显式传 proxy="socks5://host:port";这种情况下 ruyipage 使用 proxy 写入 profile,认证信息仍由支持 --fpfile 的 Firefox 内核从 fpfile 读取。

适用场景:

  • 你在自己的 Firefox 内核里已经实现了 fpfile 驱动的 HTTP 代理认证
  • 希望业务层只保留最小代理配置入口
  • 想让代理用户名密码完全留在 fpfile 中,而不是写进业务脚本

Per-tab SOCKS5 代理池:set_per_tab_proxies() + new_container_tabs()

如果你的 Firefox 指纹内核已经支持 proxy.rotate.*,ruyipage 现在可以直接帮你:

  1. 生成运行期 session fpfile;
  2. 写入 proxy.rotate.enabled=true / proxy.rotate.exhausted=... / 多条 proxy.rotate.proxy=...;
  3. 创建真正的 Firefox container tabs;
  4. 让每个 container tab 使用不同的 SOCKS5 密码代理。

最简写法:

from ruyipage import FirefoxOptions, FirefoxPage

proxies = [
    "proxy.example.com:1000:user1:pass1",
    "proxy.example.com:1000:user2:pass2",
    "proxy.example.com:1000:user3:pass3",
]

opts = FirefoxOptions()
opts.set_browser_path(r"C:\firefox\firefox.exe")
opts.set_per_tab_proxies(proxies, exhausted="wrap")

page = FirefoxPage(opts)
tabs = page.new_container_tabs(count=len(proxies), url="https://browserscan.net/")

可接受的代理格式:

  • host:port:username:password
  • socks5://host:port:username:password

说明:

  • set_per_tab_proxies() 会在 profile 目录中自动生成 session fpfile,不会直接修改你原始的 fpfile。
  • 如果你已经先调用了 set_fpfile(),ruyipage 会复制原始 fpfile 的内容,再追加 proxy.rotate.* 配置。
  • new_container_tabs() 通过 Firefox BiDi browser.createUserContext + browsingContext.create(userContext=...) 创建 container tabs,不要求用户自己安装额外扩展。
  • 这个能力依赖你们的定制 Firefox 内核;普通官方 Firefox 不保证支持 proxy.rotate.*。

完整示例见:examples/52_per_tab_socks5_proxy_browserscan.py


最常用 API 文档

下面不是底层 BiDi 命令表,而是 新手最常直接用到的高层 API。

阅读建议:

  1. 先看 FirefoxPage
    • 这是最核心的页面对象,绝大多数操作都从这里开始。
  2. 再看 ele() / eles()
    • 元素定位是最常用的基础能力。
  3. 再看 actions / downloads / network / events
    • 这些是自动化中最常扩展的高级能力。

文档风格说明:

  • 这里优先写 最常用、最实用 的高层接口。
  • 不会把底层 BiDi 命令原样堆出来让新手自己拼参数。
  • 每个能力尽量说明:
    • 它是做什么的
    • 什么时候该用
    • 最常见的写法是什么
    • 返回值你能继续怎么用

1. 页面对象:FirefoxPage

创建页面

from ruyipage import FirefoxPage, FirefoxOptions

page = FirefoxPage()

opts = FirefoxOptions()
page = FirefoxPage(opts)

常用属性

API 说明 返回值
page.title 当前页面标题 str
page.url 当前页面 URL str
page.html 当前页面 HTML str
page.tab_id 当前 tab 的 browsingContext ID str
page.cookies 当前页面可见 Cookie 列表 list[CookieInfo]

常用导航

page.get("https://www.example.com")
page.refresh()
page.back()
page.forward()
page.quit()

page.get(url, wait='complete')

打开一个页面。

page.get("https://www.example.com")
page.get("https://www.example.com", wait="interactive")

参数说明:

  • url
    • 要访问的地址
    • 常见值:https://...、file:///...、data:text/html,...
  • wait
    • 页面等待策略
    • 常见值:
      • complete:等页面完全加载
      • interactive:等 DOMContentLoaded
      • none:发出导航后立即返回

适用场景:

  • 日常页面打开:用默认 complete
  • 页面很慢但你只想先拿 DOM:用 interactive
  • 你后面会自己监听事件或手动等:用 none

page.back() / page.forward() / page.refresh()

这些分别用于:

  • 后退
  • 前进
  • 刷新
page.back()
page.forward()
page.refresh()

如果你需要验证导航事件,建议和 page.navigation 配合使用。


2. 元素查找:ele() / eles()

page.ele(locator)

查找一个元素。

最常用写法:

page.ele("#kw")
page.ele("css:.item")
page.ele("css:div.card > a")
page.ele("xpath://button[text()='登录']")
page.ele("tag:input")
page.ele("text:登录")

新手建议优先顺序:

  1. #id
  2. css:...
  3. xpath:...

page.eles(locator)

查找所有匹配元素。

items = page.eles("css:.card")
rows = page.eles("css:table tbody tr")
links = page.eles("tag:a")

在元素内部继续查找

card = page.ele("css:.card")
title = card.ele("css:h2 a")
desc = card.ele("css:.desc")

常用元素 API

API 说明 返回值
ele.text 元素文本 str
ele.html outerHTML str
ele.value 表单值 `str
ele.attr("href") 属性值 str
ele.click_self() 直接点击元素 self
ele.input("abc") 输入文本 self
ele.clear() 清空内容 self
ele.hover() 鼠标悬停 self
ele.drag_to(target) 拖到目标 self

ele.text

读取元素文本。

title = page.ele("css:h1").text

适合读取:

  • 标题
  • 按钮文案
  • 搜索结果摘要

ele.attr(name)

读取元素属性。

url = page.ele("css:a").attr("href")
src = page.ele("css:img").attr("src")

常见属性:

  • href
  • src
  • value
  • placeholder
  • class
  • id

ele.click_self()

直接点击元素。

page.ele("text:登录").click_self()

这是最推荐新手使用的点击方法。

ele.input(text, clear=True)

给输入框输入内容。

page.ele("#kw").input("ruyiPage")
page.ele("#kw").input("ruyiPage", clear=True)

适用场景:

  • 文本输入
  • 搜索框输入
  • 文件输入框上传文件

如果元素本身是 ``,传文件路径即可:

page.ele("#upload").input(r"D:\test.txt")
page.ele("#upload").input([r"D:\1.txt", r"D:\2.txt"])

3. 动作链:page.actions

用于原生 BiDi 输入动作。

page.actions.press(Keys.ENTER).perform()
page.actions.move_to(page.ele("#btn")).click().perform()
page.actions.drag(page.ele("#a"), page.ele("#b")).perform()
page.actions.release()

常见用途:

  • 键盘输入
  • 鼠标点击
  • 拖拽
  • 滚轮滚动
  • 拟人化移动和点击

常见写法

回车

from ruyipage import Keys

page.actions.press(Keys.ENTER).perform()

点击某个元素

page.actions.move_to(page.ele("#btn")).click().perform()

拖拽

page.actions.drag(page.ele("#source"), page.ele("#target")).perform()
page.actions.release()

为什么推荐 page.actions

因为这条链更接近原生 BiDi 输入模型,很多动作事件能保持更真实的浏览器输入行为。


4. Cookies

获取 Cookie

cookies = page.get_cookies()
for cookie in cookies:
    print(cookie.name, cookie.value)

返回对象通常是 CookieInfo,常用字段:

  • cookie.name
  • cookie.value
  • cookie.domain
  • cookie.path
  • cookie.http_only
  • cookie.secure
  • cookie.same_site
  • cookie.expiry

按条件过滤 Cookie

cookies = page.get_cookies_filtered(name="session_id", all_info=True)

设置 Cookie

page.set_cookies() 支持直接回放 browser.cookies(all_info=True) 返回的完整 Cookie, 并会在当前浏览上下文与 Cookie 域不匹配时自动避免错误的 BiDi context partition, 以保证跨站登录 Cookie 能正确落地。

page.set_cookies({
    "name": "token",
    "value": "abc",
    "domain": "127.0.0.1",
    "path": "/",
})

也可以一次传多个:

page.set_cookies([
    {"name": "a", "value": "1", "domain": "127.0.0.1", "path": "/"},
    {"name": "b", "value": "2", "domain": "127.0.0.1", "path": "/"},
])

删除 Cookie

page.delete_cookies(name="token")
page.delete_cookies()

5. 下载

高层入口:page.downloads

page.downloads.set_behavior("allow", path=r"D:\downloads")
page.downloads.start()

page.ele("#download").click_self()

event = page.downloads.wait("browsingContext.downloadEnd", timeout=5)
print(event.status)

常用方法:

  • set_behavior()
  • set_path()
  • start()
  • stop()
  • wait()
  • wait_chain()
  • wait_file()

典型下载流程

page.downloads.set_behavior("allow", path=r"D:\downloads")
page.downloads.start()

page.ele("#download").click_self()

begin = page.downloads.wait("browsingContext.downloadWillBegin", timeout=5)
end = page.downloads.wait("browsingContext.downloadEnd", timeout=5)

print(begin.suggested_filename)
print(end.status)

6. 导航事件

高层入口:page.navigation

page.navigation.start()
page.get("https://www.example.com")

event = page.navigation.wait("browsingContext.load", timeout=5)
print(event.url)

page.navigation.stop()

适合验证:

  • navigationStarted
  • domContentLoaded
  • load
  • historyUpdated
  • navigationCommitted

7. 通用事件监听

高层入口:page.events

page.events.start(["network.beforeRequestSent"], contexts=[page.tab_id])
event = page.events.wait("network.beforeRequestSent", timeout=5)
page.events.stop()

适合统一承接:

  • browsingContext.*
  • network.*
  • script.*
  • input.*
  • log.*

返回对象:BidiEvent

常用字段:

  • method
  • context
  • url
  • request
  • response
  • error_text
  • channel
  • data
  • multiple
  • message

什么时候用 page.events

当你想直接监听协议事件,而不是只关心页面最终状态时,用它最合适。

例如:

  • 监听 network.beforeRequestSent
  • 监听 browsingContext.contextCreated
  • 监听 script.message
  • 监听 input.fileDialogOpened

8. 网络能力

高层入口:page.intercept(拦截)、page.listen(监听)、page.network(配置)

请求拦截

拦截请求阶段(beforeRequestSent),可修改、Mock 或阻止请求:

# 回调模式:拦截并 Mock 响应
def handler(req):
    if '/api/data' in req.url:
        req.mock(
            '{"status":"ok","data":"mocked"}',
            headers={"content-type": "application/json",
                     "access-control-allow-origin": "*"},
        )
    else:
        req.continue_request()

page.intercept.start_requests(handler)
page.get("https://example.com")
page.intercept.stop()
# 修改请求头(headers 支持 dict 简洁格式)
def handler(req):
    req.continue_request(headers={
        "X-Token": "abc123",
        "User-Agent": "RuyiPage/1.0",
    })

page.intercept.start_requests(handler)
# 阻止请求
def handler(req):
    if req.url.endswith(('.png', '.jpg', '.gif')):
        req.fail()
    else:
        req.continue_request()

page.intercept.start_requests(handler)
# 队列模式:手动处理
page.intercept.start_requests()
# ... 触发网络请求 ...
req = page.intercept.wait(timeout=5)
print(req.method, req.url, req.body)
req.continue_request()
page.intercept.stop()

响应拦截

拦截响应阶段(responseStarted),可读取、修改响应信息:

# 读取原始响应状态码、头和响应体
def handler(req):
    print(f"状态码: {req.response_status}")
    print(f"Content-Type: {req.response_headers.get('content-type')}")
    req.continue_response()
    # start_responses 默认 collect_response=True,
    # continue_response 后可直接读取响应体
    print(f"响应体: {req.response_body}")

page.intercept.start_responses(handler)
# 修改响应状态码
def handler(req):
    if '/api' in req.url:
        req.continue_response(status_code=200, reason_phrase="OK")
    else:
        req.continue_response()

page.intercept.start_responses(handler)

一步读取响应体

启用 collect_response=True 后,可通过 req.response_body 一步读取响应体,无需手动编排 DataCollector:

page.intercept.start_requests(collect_response=True)
# ... 触发网络请求 ...
req = page.intercept.wait(timeout=5)
req.continue_request()
body = req.response_body  # 自动等待响应完成 + 解码
print(body)
page.intercept.stop()     # 自动清理内部 collector

被动抓包

page.capture 用于按 URL 和 method 被动抓取完整请求/响应信息,不会拦截或暂停请求。适合页面打开后自动加载的接口,也支持一次抓多个包:

page.capture.start("/api/", method="POST")

# 外部正常触发请求:打开页面、点击按钮、执行 JS 都可以
page.get("https://example.com")

# count=1 返回单个 CapturePacket 或 None
packet = page.capture.wait(timeout=10)

# count>1 返回 list,超时会返回已经抓到的部分
packets = page.capture.wait(timeout=10, count=5)

for p in packets:
    print(p.url, p.method)
    print(p.request_headers)
    print(p.request_body)
    print(p.response_status)
    print(p.response_headers)
    print(p.response_body)

# 已抓到的历史包
all_packets = page.capture.steps

page.capture.stop()

抓自动加载包时要先 page.capture.start(),再打开页面;已经发出的历史请求不能补抓。

设置额外请求头

page.network.set_extra_headers({"X-Test": "yes"})

这通常用于:

  • 给接口加测试请求头
  • 做环境标记
  • 配合拦截验证请求头是否真的发出

设置缓存行为

page.network.set_cache_behavior("bypass")

其中:

  • default: 浏览器默认缓存策略,命中缓存时可能不再发真实请求
  • bypass: 尽量绕过缓存,强制重新请求资源

Data Collector

collector = page.network.add_data_collector(
    data_types=["response"],
)

data = collector.get(request_id, data_type="response")
collector.disown(request_id, data_type="response")
collector.remove()

其中:

  • events
    • beforeRequestSent:在请求发出阶段采集
    • responseCompleted:在响应完成阶段采集
  • data_types
    • request:收集请求体
    • response:收集响应体

9. 浏览上下文

高层入口:page.contexts

tree = page.contexts.get_tree()
print(len(tree.contexts))

tab_id = page.contexts.create_tab()
page.contexts.close(tab_id)

page.contexts.reload()
page.contexts.set_viewport(800, 600)

常用方法:

  • get_tree()
  • create_tab()
  • create_window()
  • close()
  • reload()
  • set_viewport()
  • set_bypass_csp()

tree = page.contexts.get_tree()

返回的不是裸 dict,而是高层结果对象。

tree = page.contexts.get_tree()
print(len(tree.contexts))

first = tree.contexts[0]
print(first.context)
print(first.url)

10. 浏览器级能力

高层入口:page.browser_tools

user_context = page.browser_tools.create_user_context()
contexts = page.browser_tools.get_user_contexts()
page.browser_tools.remove_user_context(user_context)

windows = page.browser_tools.get_client_windows()
page.browser_tools.set_window_state(windows[0]["clientWindow"], state="maximized")

适合做:

  • user context 管理
  • client window 管理

典型用法

ctx = page.browser_tools.create_user_context()
tab_id = page.browser_tools.create_tab(user_context=ctx)
page.contexts.close(tab_id)
page.browser_tools.remove_user_context(ctx)

11. Script 能力

获取 realms

realms = page.get_realms()
for realm in realms:
    print(realm.type, realm.context)

执行脚本并拿 handle

result = page.eval_handle("({a: 1, b: 2})")
print(result.success)
print(result.result.handle)

page.disown_handles([result.result.handle])

这个流程适合:

  • 需要拿远程 JS 对象句柄
  • 用完后再手动释放 handle

preload script

preload = page.add_preload_script("""
() => {
    window.__ready = 'ok';
}
""")

page.get("https://www.example.com")
print(page.run_js("return window.__ready"))

page.remove_preload_script(preload)

适用场景:

  • 在页面脚本执行前先注入一段初始化逻辑
  • 给页面预先挂钩子、打标记、注入辅助函数

12. 弹窗

高层入口:

  • page.wait_prompt()
  • page.accept_prompt()
  • page.dismiss_prompt()
  • page.input_prompt(text)
  • page.set_prompt_handler(...)
  • page.clear_prompt_handler()
  • page.prompts.set_auto(accept=True)

典型写法

等待后手动处理

page.run_js("alert('hello')", as_expr=False)
prompt = page.wait_prompt(timeout=3)
page.accept_prompt()

自动处理 prompt

page.set_prompt_handler(prompt="ignore", prompt_text="张三")
page.run_js("prompt('请输入姓名')", as_expr=False)
page.clear_prompt_handler()

兼容 manager 写法

page.prompts.set_auto(accept=True)
page.run_js("alert('hello')", as_expr=False)
page.prompts.clear()

13. Emulation

高层入口:page.emulation

page.emulation.set_network_offline(True)
page.emulation.set_javascript_enabled(False)
page.emulation.set_scrollbar_type("overlay")
page.emulation.apply_mobile_preset(
    width=390,
    height=844,
    device_pixel_ratio=3,
    user_agent="...",
)

H5 触摸能力

# 运行时覆盖当前标签页的 navigator.maxTouchPoints
result = page.emulation.set_touch_enabled_result(
    True,
    max_touch_points=5,
    scope="context",
    strict=True,
)
print(result.applied, result.source)

# 发送真实 pointerType=touch 的输入动作
page.touch.tap((120, 60)).perform()

# 清除当前标签页的覆盖
page.emulation.set_touch_enabled(False, scope="context")

旧版 Firefox 可在启动前配置 fpfile fallback:

options = FirefoxOptions().set_touch_fallback(max_touch_points=5)
page = FirefoxPage(options)

scope 支持 context、user_context 和 global。fallback 只在浏览器启动时 生效,运行时状态和失败原因可通过 TouchOverrideResult 查看。

注意:

  • 某些 emulation 命令在当前 Firefox 版本中可能未实现
  • 示例里会区分“成功”和“不支持”

典型用法

page.emulation.apply_mobile_preset(
    width=390,
    height=844,
    device_pixel_ratio=3,
    user_agent="Mozilla/5.0 ...",
)

14. WebExtension

高层入口:page.extensions

ext_id = page.extensions.install_dir(r"D:\my_extension")
page.extensions.uninstall(ext_id)

适用场景:

  • 验证 content script 是否生效
  • 测试目录扩展和 xpi 安装流程

15. JS 断点调试

高层入口:page.debugger

WebDriver BiDi 规范里没有 debugger 模块,Firefox 也在 141 版彻底移除了 CDP,所以断点级调试在 BiDi 框架里本来是做不到的。ruyiPage 额外接了一条 Firefox 自己的 DevTools 远程调试通道(RDP),与 BiDi 连接并行工作、互不干扰。

启用

调试通道需要 Firefox 以 devtools server 启动,所以要在创建页面前配置:

from ruyipage import FirefoxOptions, FirefoxPage

opts = FirefoxOptions()
opts.enable_debugger()          # 写入所需 pref 并加 --start-debugger-server
page = FirefoxPage(opts)

page.debugger.start()           # 连接并 attach 当前标签页

enable_debugger(port=6000) 可以指定端口。start(auto_resume_after=30) 可以开启暂停看门狗(见下文注意事项)。

读源码

for s in page.debugger.sources():
    print(s.url)

code = page.debugger.source_text('app.js')          # 也接受 Source 对象
lines = page.debugger.breakable_lines('app.js')     # 哪些行可以下断点

source_text() 返回的是JS 引擎正在执行的那份文本,行号与断点位置、调用栈里的 line 完全对齐。这一点自己下载 URL 做不到:

  • eval / new Function / blob 脚本根本没有可下载的地址
  • 内联 `` 的行号是相对整个 HTML 文档的

下断点

bp = page.debugger.set_breakpoint('app.js', 42)
bp = page.debugger.set_breakpoint('app.js', 42, condition='quantity > 3')  # 条件断点

page.debugger.remove_breakpoint(bp)
page.debugger.clear_breakpoints()

column 省略时会自动查询该行的有效列。Firefox 会静默忽略落在无效位置的断点(不报错也不触发),所以不要自己猜列号。

断点按 URL 记录,页面导航后服务端会自动重新应用,不需要重新下。

日志断点

给 log_value 就变成日志断点:命中时不暂停,只把表达式的求值结果记下来。因为暂停期间求不了任意表达式(见「当前限制」),这是观察运行中变量最省事的办法——尤其是循环里每轮的值。

page.debugger.set_breakpoint('app.js', 42, log_value='quantity, subtotal')

page.run_js('return window.buildCart();')     # 不会阻塞,无需后台线程

for entry in page.debugger.wait_logs(count=3):
    print(entry['values'], entry['line'])     # [1, 12.5] 42

条件断点和日志断点可以叠加,只在满足条件时记录。log_stacktrace=True 会附带调用栈。

这类消息是调试器直接注入 DevTools 控制台管道的,不经过真实的 console API,所以 page.console 看不到,只能从 debugger.logs() / wait_logs() 读。反过来页面自己的 console.log 也不会混进来。

暂停与单步

state = page.debugger.wait_paused(timeout=30)   # -> PausedState
print(state.why, state.url, state.line)

page.debugger.step_over()
page.debugger.step_into()
page.debugger.step_out()
page.debugger.resume()

page.debugger.pause()                            # 立即暂停
page.debugger.on_paused(lambda s: print(s))      # 回调方式

读调用栈与作用域

for f in page.debugger.frames():
    print(f.display_name, f.url, f.line, f.arguments, f.this_object)

scope = page.debugger.scope()                    # 局部变量 + 函数参数 + this
scope = page.debugger.scope(include_parents=True)  # 继续读闭包与全局

scope() 默认沿作用域链读到函数边界为止(当前块 + 所属函数),不越过全局。只读最内层块的话,断点停在 const x = ... 上时只能看到一个尚未初始化的变量,函数参数会全部缺失。

this 不属于环境绑定,它挂在帧上,scope() 会以 'this' 为键一并给出(this 是保留字,不会和局部变量重名)。

全局对象同理不出现在环境链里——window 上的东西要这样拿:

window = page.debugger.global_object()
page.debugger.get_property(window, 'appConfig')

展开对象

作用域里的对象是 RemoteObject,已经把协议内嵌的 preview 解码成可读内容(数组变 list,普通对象变 dict),这一层零额外请求:

items = scope['items']
print(items.class_name, items.value, items.truncated)

preview 最多带十项,装不下时 truncated 为 True。要看完整内容:

page.debugger.expand(obj, depth=3)          # 递归展开
page.debugger.get_property(obj, 'probe')    # 按名字定向取(大对象用这个)
page.debugger.constructor_name(obj)         # 取真实类名,例如 'Cart'

class_name 反映的是 JS 的 [[Class]],普通类实例统一是 'Object';真实类名要用 constructor_name() 从原型链派生。

window 有上千个属性,全量 expand() 会被 max_items 截断(会打警告),这种情况用 get_property()。

get_property() 按 JS 的正常语义解析:自有属性没有就继续沿原型链找,所以类的方法也取得到(它们挂在原型上)。只要自有属性时传 own_only=True。

RemoteObject 的相等性只比较类名和内容,不比较 actor id。对象 actor 每次 resume 都会重建,否则「单步后哪些变量变了」这类对比会把所有对象都误报成变化。

读取普通属性以外的内容

expand() 只能看到普通属性,下面这些各有各的取法:

# Map / Set 的条目不是属性,preview 也只带前十项
page.debugger.entries(obj)          # Map -> {键: 值};Set -> [值, ...]

# 访问器属性默认只显示 '',读它意味着执行页面代码
page.debugger.invoke_getter(obj, 'total')

# 超长字符串随包只回开头一段,当普通 str 用得到的是截断版
page.debugger.read_string(scope['html'])

# Promise 的状态与结果
page.debugger.promise_state(obj)    # {'state': 'fulfilled', 'value': 99, ...}

远程调用函数

暂停期间求不了任意表达式,但可以调用页面里已有的函数——包括业务函数本身:

fn = page.debugger.get_property(scope['app'], 'formatPrice')
print(page.debugger.call(fn, args=[12.5]))            # '¥12.50'

# 参数和 this 都可以传远端对象
page.debugger.call(fn, args=[scope['item']], this=scope['app'])

函数内部抛异常会转成 DebuggerError,异常内容在消息里。

属性监视点

排查「这个值到底是被谁改掉的」——CDP 没有对应能力:

page.debugger.watch_property(obj, 'token', on='set')   # 也可 'get' / 'getorset'

state = page.debugger.wait_paused(timeout=30)
print(page.debugger.frames())        # 谁在写它,一目了然

page.debugger.unwatch_property(obj)  # 省略属性名则清除该对象上的全部监视点

目标属性必须已经存在、可配置、且是数据属性(不是 getter/setter)。不满足时服务端会静默忽略——这个请求没有回执,无法从客户端判断。

异常时自动暂停

不用先猜出错位置再下断点,直接让页面跑到抛异常的现场停下:

page.debugger.pause_on_exceptions(True, ignore_caught=True)

state = page.debugger.wait_paused(timeout=30)
if state.is_exception:
    print(state.exception)      # 已解码的 TypeError 等,含 message 与 stack

ignore_caught=True(默认)会忽略被 catch 接住的异常,否则页面正常的 try/catch 流程会不停触发暂停。

另有 pause_on_debugger_statement() 控制是否在 JS 的 debugger 语句处暂停。

事件断点与 XHR 断点

不需要预先知道处理器写在哪个文件哪一行,直接按行为下断点:

# 看看内核支持哪些事件(分组名 -> 事件 id 列表)
print(page.debugger.available_event_breakpoints())

# 任何 click 处理器执行时暂停
page.debugger.set_event_breakpoints(['event.mouse.click'])

state = page.debugger.wait_paused(timeout=30)
if state.is_event_breakpoint:
    print(state.event_breakpoint)      # 'event.mouse.click'
    print(page.debugger.frames())      # 处理器在哪,一目了然

page.debugger.set_event_breakpoints([])   # 传空列表清除
# 请求 URL 含 /api/ 时暂停
page.debugger.set_xhr_breakpoint('/api/', method='GET')

state = page.debugger.wait_paused(timeout=30)
if state.is_xhr:
    print(page.debugger.frames())      # 是谁发起的这个请求

page.debugger.remove_xhr_breakpoint('/api/', method='GET')

path 传空串、method 传 'ANY' 即匹配所有请求。这两类断点对「点了没反应」「这个请求哪来的」这种排查特别有效。

黑盒化框架代码

真实页面里不做黑盒化,step_into 会一头扎进 React / jQuery 内部,很难走回自己的代码:

page.debugger.blackbox('https://cdn.example.com/react.min.js')
page.debugger.blackbox('vendor.js', start_line=1, end_line=5000)   # 也可只黑盒某个行区间

print(page.debugger.blackboxed())      # 当前被标记的源
page.debugger.unblackbox('vendor.js')

同一个 HTML 里的多段内联脚本共用 URL,会被一并标记。

其他执行控制

page.debugger.skip_breakpoints(True)    # 临时忽略全部断点,不必逐个删除
page.debugger.restart_frame()           # 回到当前帧入口重新执行(副作用不会撤销)
page.debugger.include_async_frames()    # 调用栈包含异步父帧

include_async_frames() 对现代页面很有必要——大量 async/await 之下同步栈往往只剩一层,看不出是谁发起的。打开后帧上的 asyncCause 会说明它是被什么衔接过来的。

⚠️ 必读注意事项

JS 暂停期间,所有依赖 JS 线程的 BiDi 调用都会阻塞到超时(run_js、点击、取元素文本等)。所以触发断点的调用必须放在后台线程:

import threading

page.debugger.set_breakpoint('app.js', 42)

threading.Thread(
    target=lambda: page.run_js('return window.doWork();', timeout=60),
    daemon=True,
).start()

state = page.debugger.wait_paused(timeout=30)
# ... 检查现场,只用 page.debugger 的接口 ...
page.debugger.resume()

无人值守的脚本建议开看门狗兜底,避免漏调 resume() 让页面永久卡死:

page.debugger.start(auto_resume_after=30)

stop() 在断开前会自动恢复执行,不会把页面留在暂停状态。

当前限制

  • 暂停时无法求任意表达式。这是协议限制而非未实现:Firefox 的 evaluateJSAsync 在暂停期间不投递结果,frame actor 也没有 eval 方法。替代做法有三条:scope() + expand() + get_property() 读状态、call() 调用页面已有的函数、以及用日志断点在不暂停的前提下记录任意表达式。
  • 无法修改变量值。environment actor 的 spec 里 methods 是空的,只能读不能写。
  • 无法热替换脚本源码。Firefox 从未实现 CDP 的 setScriptSource 那类 live edit。
  • 只覆盖顶层标签页,iframe 和 Worker 里的 JS 调试不到。
  • 不做 source map 解析,服务端只提供 sourceMapURL 元数据,压缩代码的行号需要自行映射。
  • RDP 是 Firefox 私有协议,没有跨大版本兼容承诺。

参考示例:examples/55_js_debugger.py、examples/56_ai_autonomous_debug.py(后者演示只给页面地址、由程序自己发现代码并定位断点的完整闭环)。


16. 代表性示例

仓库里已经包含大量示例,建议按编号学习。

推荐顺序:

入门

  • 01_basic_navigation.py
  • 02_element_finding.py
  • 03_element_interaction.py
  • 05_actions_chain.py
  • 06_screenshot.py

页面与脚本

  • 07_javascript.py
  • 08_cookies.py
  • 09_tabs.py
  • 13_iframe.py
  • 14_shadow_dom.py

高级能力

  • 17_user_prompts.py
  • 18_advanced_network.py
  • 19_pdf_printing.py
  • 20_advanced_input.py
  • 21_emulation.py

严格结果版示例

  • 23_download.py
  • 24_navigation_events.py
  • 25_browser_user_context.py
  • 37_three_isolated_user_context_tabs.py 单浏览器多 tab 使用不同 user context,实现 Cookie 隔离
  • 26_browsing_context_advanced.py
  • 27_emulation_advanced.py
  • 28_network_data_collector.py
  • 29_script_input_advanced.py
  • 30_browsing_context_events.py
  • 31_network_events.py
  • 32_script_events.py
  • 33_log_input_events.py
  • 34_remaining_commands.py
  • 35_native_bidi_drag.py
  • 36_native_bidi_select.py
  • 39_attach_exist_browser.py 自动探测可接管实例,再接管已打开的 Firefox/指纹浏览器
  • 42_xpath_picker_complex_showcase.py 启动 XPath picker,并打开包含复杂节点、shadow root、嵌套 iframe 的综合展示页
  • 42_3_debug_px_context_probe.py 直接打开 debug_px.html,打印 PX challenge iframe 的 browsing context 树,并尝试 attach 到 child context 做最小 DOM / canvas 诊断
  • 46_human_behavior_showcase.py 演示 bezier / windmouse 两套拟人轨迹算法,并开启鼠标行为可视化
  • 48_smart_fingerprint.py 演示 apply_smart_fingerprint() 一站式智能指纹(geo 探测 + 内核 fpfile + BiDi 仿真)
  • 52_per_tab_socks5_proxy_browserscan.py 单浏览器创建多个 container tabs,并让每个 tab 走不同 SOCKS5 密码代理
  • 53_duckai_eventstream_capture.py 使用 Firefox 打开 Duck.ai,提交聊天内容,并拦截 POST /duckchat/v1/chat 的 EventStream 响应体
  • 54_bing_passive_capture.py 使用 page.capture 先启动被动抓包,再打开 Bing 搜索页,抓自动加载请求的请求头/请求体/响应头/响应体
  • 55_js_debugger.py page.debugger 常用 API:源码、断点、条件/日志断点、单步、栈与作用域、对象检查、异常/事件/XHR 断点、监视点、黑盒
  • 56_ai_autonomous_debug.py 自主调试闭环:只给页面地址,自己发现源码、断点、异常现场和点击处理器

智能指纹一站式 API

ruyiPage 在 firefox-fingerprintBrowser 内核之上提供了开箱即用的 智能指纹 能力:一行代码完成「探测出口 IP → 匹配语言/时区/语音 → 抽取 22 套真机硬件特征 → 写出 fpfile.txt」全流程。 apply_smart_fingerprint() 默认不设置外部窗口;推荐顺序是先取得 ctx,再创建 FirefoxPage(opts),最后调用 ctx.apply_emulation(page)。

推荐顺序

from ruyipage import FirefoxOptions, FirefoxPage, CountryMismatchError

opts = FirefoxOptions()
opts.set_browser_path(r"C:/Program Files/Mozilla Firefox/firefox.exe")

ctx = opts.smart_fingerprint(
    proxy_host="proxy.example.com", proxy_port=8080,
    proxy_user="u", proxy_pwd="p",
    require_country="US",      # 出口 IP 国家不一致 → CountryMismatchError
    logger=print,
)

page = FirefoxPage(opts)
ctx.apply_emulation(page)       # 顺序必须是 ctx -> page -> ctx.apply_emulation(page)
page.get("https://browserleaks.com/webgl")

流水线说明

  1. build_proxies_dict(...) — 组装 requests 风格的 proxies。
  2. fetch_geo_info(...) — 10 数据源回退。require_country 不匹配时不会 因为单个数据源就判死:各家 geo 库对同一 IP 时有分歧,个别源还只能报 ASN 注册国,所以要有两个源给出同一个错误国家才抛 CountryMismatchError;只有一个源有异议时继续问下一个源。
  3. fetch_public_ipv6(...) — best-effort,仅富化出口诊断信息;不会自动把代理 Geo IP 写成 WebRTC ICE 地址。
  4. 自动生成 / 复用 userdir,写入符合内核字段顺序的 fpfile.txt;不再写入 width / height。配置了代理时同时写入 webrtc.ice_proxy_only:true, 把 ICE 钉在代理上,避免 srflx candidate 单独暴露真实出口 IP。 内核支持、但本流水线不覆盖的字段(touch.maxTouchPoints、 fonts.whitelist、screen.devicePixelRatio、media.devices、 其余 webgl.* / webgl2.*、webgpu.* 等)可通过 extra={...} 追加。
  5. 默认向 Options 加入 about:blank 启动页,使页面创建后可立即应用 BiDi 覆盖且不需要 remote-allow-system-access;已有自定义启动页时传 set_startup_page_on_opts=False。set_window_size_on_opts 仅为兼容保留且已忽略;智能指纹不会把 screen.width/height 映射为 Firefox 外窗尺寸。确需设置外窗时,请在创建 FirefoxPage 前由调用方显式调用 opts.set_window_size(width, height)。
  6. 返回 FingerprintContext:
    • ctx.summary() — 单行日志;
    • ctx.apply_emulation(page) — 默认通过 page.emulation.set_screen_size(hw.width, hw.height) 设置 screen.width / screen.height / screen.avail*;outerWidth / innerWidth / viewport 继续由 Firefox 原生维护并随窗口变化。返回结果包含 screen、geolocation、locale、timezone、headers;
    • 异步页面使用 await ctx.apply_emulation_async(async_page);该入口始终可等待;
    • ctx.to_dict() — 持久化指纹身份(账号库等)。

内置数据资产

  • 22 套 Windows 真机硬件特征(NVIDIA RTX 系 + AMD RX 系 + Intel UHD/Arc)。 传 profile_id="win-rtx3060" 可钉选其中一套(list_hardware_profiles() 枚举全部 id);不传则随机抽取。
  • WebGL 共 53 个字段一次写全,不留回落到真实显卡的缺口。取值来自真机 ANGLE/D3D11 实测:webgl.vendor 为 Mozilla,webgl.renderer 与 webgl.unmasked_renderer 同为 ANGLE (...), or similar(这是上游 Firefox 的真实形状,不是占位符),版本串为 WebGL 1.0 / WebGL GLSL ES 1.0。各项上限、扩展表和 shader 精度都是 ANGLE/D3D11 常量,只有 webgl.unmasked_* 随机型变化。
  • 30+ 国语言 / Accept-Language / 微软语音映射,含 _default 兜底。
  • UA 优先使用 opts.browser_path 对应 Firefox 的实际主版本;可执行文件无法查询时 才回退到内置基准版本,且不再随机抖动主版本。
  • WebRTC:配置代理时默认写入 webrtc.ice_proxy_only:true,srflx candidate 随之 消失;传 webrtc_ice_proxy_only=False 可退回原生 ICE,但此时原生 srflx 地址 可能不同于代理出口。local/public_webrtc_* 仍只在调用方提供真实地址时写入, 且不会筛除所有其他 host candidate。

异常体系

FingerprintError
├── FingerprintConfigError      # 内置 JSON 损坏(部署期错误)
└── GeoError                    # 10 个 geo 数据源全部失败
    └── CountryMismatchError    # 出口 IP 国家与 require_country 不一致
        # 属性:actual / required

完整字段定义、低层接口(pick_fingerprint / write_fpfile / list_hardware_profiles / get_country_profile)见 ruyipage/_fingerprint/README.md 与示例 examples/48_smart_fingerprint.py。

fpfile 指纹字段完整说明

上面的智能指纹 API 会自动写出 fpfile,多数场景无需手动干预。当你需要固定某台 真机机型、手动调整智能指纹未覆盖的字段(WebGPU、语音列表、地理位置细项等), 或只用内核不经过 ruyiPage 时,fpfile 的每一个字段——取值范围、别名、默认值、 影响的 JS/HTTP API、以及对应检测点的验收方法——都记录在:

fingerprint/fpfile指纹说明.md (English: fpfile-fingerprint.md)

覆盖自动化检测、硬件/设备、Canvas、WebGL、音频、字体、Navigator 一致性、 WebRTC/媒体、时区/语言、反 Hook 十类检测点,外加网络代理与认证、WebGPU、 必填清单和一键自检脚本。


协议来源

ruyiPage 的底层核心能力对照并基于:

这也是本项目很多高层 API 的设计来源,例如:

  • browsingContext.*
  • network.*
  • script.*
  • input.*
  • browser.*
  • emulation.*

Star History


使用声明与免责声明

本项目仅用于:

  • 探索下一代自动化框架
  • 学习 Firefox 自动化能力
  • 学习 WebDriver BiDi 协议
  • 学习浏览器自动化高层 API 设计
  • 合法、合规、非盈利的个人研究与技术交流

授权范围

允许任何人以个人身份使用或分发本项目源代码,但仅限于:

  • 学习目的
  • 技术研究目的
  • 合法、合规、非盈利目的

个人或组织如未获得版权持有人授权,不得将本项目以源代码或二进制形式用于商业行为。

使用条款

使用本项目需满足以下条款,如使用过程中出现违反任意一项条款的情形,授权自动失效。

  • 禁止将 ruyiPage 应用于任何可能违反当地法律规定和道德约束的项目中
  • 禁止将 ruyiPage 用于任何可能有损他人利益的项目中
  • 禁止将 ruyiPage 用于攻击、骚扰、批量滥用、恶意注册、撞库、刷量等行为
  • 禁止将 ruyiPage 用于规避平台安全机制后实施违法行为
  • 使用者应遵守目标网站或系统的 Robots、服务条款及当地法律法规
  • 禁止将 ruyiPage 用于采集法律、条款或 Robots 协议明确不允许的数据

风险与责任

使用 ruyiPage 发生的一切行为,均由使用人自行负责。

因使用 ruyiPage 进行任何行为所产生的一切纠纷及后果,均与版权持有人无关。

版权持有人不承担任何因使用 ruyiPage 带来的风险、损失、封号、限制、数据问题、法律后果或间接损失。

版权持有人也不对 ruyiPage 可能存在的缺陷、兼容性问题、误操作风险或目标网站策略变化导致的任何损失承担责任。

特别说明

本项目强调:

  • Firefox 自动化
  • BiDi 协议能力
  • isTrusted 行为
  • 拟人化行为能力
  • 高风控场景适配

但这些能力仅限于合法、合规、正当的技术研究和自动化应用场景。


配套项目

如果你准备把 ruyiPage 用在 AI 自动化分析、复杂网页采集或高风控页面场景,建议先看这些配套项目:

  • 📘 官方文档 / 自动化文档 更系统地查看 ruyiPage 相关自动化说明、接入方式和配套能力说明:
  • 🦊 Firefox 指纹浏览器项目 用于需要 Firefox 指纹环境、浏览器接管或更高真实度自动化场景,适合和 ruyiPage 搭配使用:
  • 🔌 MCP Server:ruyi-mcp 基于 TypeScript MCP SDK 和常驻 Python Bridge 的社区维护集成,将 ruyiPage 的浏览器自动化、网络采集、指纹分析、拟人交互和 WebDriver BiDi JSON Trace 能力提供给 Claude Code、Codex、Cursor 等 MCP 客户端。感谢 @Facetomyself 的实现与维护:
  • 🟨 JavaScript 实现:ruyipage-js 面向 JavaScript / Node.js 生态的配套实现,适合希望在 JS 项目里接入 ruyiPage 思路与能力的场景:
  • 🐹 Go 语言实现:ruyipage-go 由社区实现的 Go 版本,适合需要在 Go 项目中接入 Firefox 自动化能力的场景。感谢 @pll177 的实现与维护:
  • 🖥️ 桌面端 GUI 管理工具:ruyiBrowser-GUI 基于 Electron + Vue3 的 Firefox 指纹浏览器图形化管理工具,无需命令行即可创建、管理和启动多个独立指纹环境。感谢 @jacklaigougou 的实现与维护:

请我喝咖啡

如果这个项目对你有帮助,欢迎请我喝杯咖啡,支持我继续完善 ruyiPage。

  公众号  

   ![公众号二维码](images/gzh.jpg) 




  QQ 社群  

   ![QQ 社群二维码](images/qq.jpg) 




  联系我 / 个人微信  

   ![个人微信二维码](images/weixin.jpg) 




  请我喝咖啡  

   ![收款码](images/weixingoot.jpg) 
排名相邻
同类项目