页面与导航
get、refresh、back、forward、tabs、save、screenshot、PDF。
这个页面对应文档站的概述页,重点说明对象关系、能力带和框架为什么适合 Firefox 自动化。
get、refresh、back、forward、tabs、save、screenshot、PDF。
selectors、elements、forms、iframe、shadow DOM。
actions、touch、drag、scroll、human-type、isTrusted。
intercept、navigation events、network events、collector。
cookies、downloads、browser tools、extensions、prompts。
UA、locale、timezone、screen、mobile preset、anti-bot。
覆盖最基础的页面访问、前进后退和保存 HTML/PDF。
源码文件:app/content/ruyipage_examples/01_basic_navigation.py · 仓库:01_basic_navigation.py
# =============================
# RuyiPage 示例注释版
# 标题: 基础导航与页面保存
# 说明: 覆盖最基础的页面访问、前进后退和保存 HTML/PDF。
# 你会学到:
# 1. 打开本地文件页面和外部页面
# 2. 读取标题、URL 和 HTML 源码
# 3. 执行前进、后退和刷新
# 4. 保存 HTML 与 PDF
# 相关 API: FirefoxPage, FirefoxOptions, page.get, page.title, page.url, page.html, page.back, page.forward
# =============================
# -*- coding: utf-8 -*-
"""
示例1: 基础导航和页面操作
测试功能:
- 创建浏览器实例
- 页面导航
- 获取标题、URL
- 前进、后退、刷新
- 页面保存
"""
import os
import sys
import io
# 设置控制台输出编码为UTF-8(Windows兼容)
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from ruyipage import FirefoxPage, FirefoxOptions
def test_basic_navigation():
"""测试基础导航功能"""
print("=" * 60)
print("测试1: 基础导航和页面操作")
print("=" * 60)
# 创建浏览器实例
opts = FirefoxOptions()
opts.headless(False) # 显示浏览器窗口
page = FirefoxPage(opts)
try:
# 1. 导航到测试页面
test_page = os.path.join(os.path.dirname(__file__), 'test_pages', 'test_page.html')
test_url = 'file:///' + os.path.abspath(test_page).replace('\\', '/')
print(f"\n1. 导航到测试页面: {test_url}")
page.get(test_url)
print(f" ✓ 页面加载成功")
# 2. 获取页面信息
print(f"\n2. 获取页面信息:")
print(f" 标题: {page.title}")
print(f" URL: {page.url}")
print(f" HTML长度: {len(page.html)} 字符")
# 3. 导航到其他页面
print(f"\n3. 导航到其他页面:")
page.get('https://www.example.com')
print(f" 当前标题: {page.title}")
print(f" 当前URL: {page.url}")
# 4. 后退
print(f"\n4. 后退到上一页:")
page.back()
page.wait(1)
print(f" 当前标题: {page.title}")
# 5. 前进
print(f"\n5. 前进到下一页:")
page.forward()
page.wait(1)
print(f" 当前标题: {page.title}")
# 6. 刷新页面
print(f"\n6. 刷新页面:")
page.refresh()
page.wait(1)
print(f" ✓ 页面已刷新")
# 7. 保存页面
print(f"\n7. 保存页面:")
save_dir = os.path.join(os.path.dirname(__file__), 'output')
os.makedirs(save_dir, exist_ok=True)
# 保存为HTML
html_path = page.save(path=save_dir, name='example_page', as_pdf=False)
print(f" HTML已保存: {html_path}")
# 保存为PDF
pdf_path = page.save(path=save_dir, name='example_page', as_pdf=True)
print(f" PDF已保存: {pdf_path}")
print("\n" + "=" * 60)
print("✓ 所有基础导航测试通过!")
print("=" * 60)
except Exception as e:
print(f"\n✗ 测试失败: {e}")
import traceback
traceback.print_exc()
finally:
# 关闭浏览器
page.wait(2)
page.quit()
if __name__ == '__main__':
test_basic_navigation()
系统讲透单元素、多元素、文本、XPath 和属性定位的入门示例。
源码文件:app/content/ruyipage_examples/02_element_finding.py · 仓库:02_element_finding.py
# =============================
# RuyiPage 示例注释版
# 标题: 元素查找:CSS / XPath / 文本 / 属性 / 容器内定位
# 说明: 系统讲透单元素、多元素、文本、XPath 和属性定位的入门示例。
# 你会学到:
# 1. 通过 ID、class 和属性定位元素
# 2. 批量获取列表元素
# 3. 通过 XPath 和文本定位元素
# 4. 在父容器内继续查找子元素
# 5. 读取元素标签、属性和尺寸信息
# 相关 API: page.ele, page.eles, ele.ele, ele.text, ele.attr, ele.attrs, ele.tag
# =============================
# -*- coding: utf-8 -*-
"""
示例2: 元素查找
测试功能:
- CSS选择器查找
- XPath查找
- 文本查找
- 属性查找
- 多元素查找
- 元素属性获取
"""
import os
import sys
import io
# 设置控制台输出编码为UTF-8(Windows兼容)
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from ruyipage import FirefoxPage, FirefoxOptions
def test_element_finding():
"""测试元素查找功能"""
print("=" * 60)
print("测试2: 元素查找")
print("=" * 60)
opts = FirefoxOptions()
opts.headless(False)
page = FirefoxPage(opts)
try:
# 加载测试页面
test_page = os.path.join(os.path.dirname(__file__), 'test_pages', 'test_page.html')
test_url = 'file:///' + os.path.abspath(test_page).replace('\\', '/')
page.get(test_url)
page.wait(1)
# 1. 通过ID查找
print("\n1. 通过ID查找元素:")
title = page.ele('#main-title')
print(f" 标题文本: {title.text}")
print(f" 标签名: {title.tag}")
# 2. 通过class查找
print("\n2. 通过class查找元素:")
test_div = page.ele('.test-class')
print(f" 第一个.test-class元素: {test_div.text}")
# 3. 查找所有匹配元素
print("\n3. 查找所有.test-class元素:")
all_test = page.eles('.test-class')
print(f" 找到 {len(all_test)} 个元素")
for i, elem in enumerate(all_test, 1):
print(f" 元素{i}: {elem.text}")
# 4. 通过XPath查找
print("\n4. 通过XPath查找:")
link = page.ele('xpath://a[@id="test-link"]')
print(f" 链接文本: {link.text}")
print(f" 链接地址: {link.link}")
# 5. 通过文本查找
print("\n5. 通过文本查找:")
btn = page.ele('text:点击我')
print(f" 按钮ID: {btn.attr('id')}")
# 6. 通过属性查找
print("\n6. 通过属性查找:")
test_div = page.ele('[data-test="value"]')
print(f" data-test属性的元素: {test_div.text}")
print(f" 所有属性: {test_div.attrs}")
# 7. 组合选择器
print("\n7. 组合选择器:")
section = page.ele('#form-section')
input_elem = section.ele('input[type="text"]')
print(f" 表单区域内的文本输入框: {input_elem.attr('placeholder')}")
# 8. 获取元素属性
print("\n8. 获取元素属性:")
img = page.ele('#test-img')
print(f" 图片alt: {img.attr('alt')}")
print(f" 图片src: {img.src}")
# 9. 元素状态检查
print("\n9. 元素状态检查:")
disabled_btn = page.ele('#disabled-btn')
print(f" 禁用按钮是否可用: {disabled_btn.is_enabled}")
print(f" 禁用按钮是否显示: {disabled_btn.is_displayed}")
# 10. 元素尺寸和位置
print("\n10. 元素尺寸和位置:")
click_btn = page.ele('#click-btn')
print(f" 按钮尺寸: {click_btn.size}")
print(f" 按钮位置: {click_btn.location}")
print("\n" + "=" * 60)
print("✓ 所有元素查找测试通过!")
print("=" * 60)
except Exception as e:
print(f"\n✗ 测试失败: {e}")
import traceback
traceback.print_exc()
finally:
page.wait(2)
page.quit()
if __name__ == '__main__':
test_element_finding()
强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。
源码文件:app/content/ruyipage_examples/05_actions_chain.py · 仓库:05_actions_chain.py
# =============================
# RuyiPage 示例注释版
# 标题: 动作链:原生点击、键盘、滚轮、拖拽与 isTrusted
# 说明: 强调 BiDi 原生动作链的事件真实性,是理解 isTrusted 的关键示例。
# 你会学到:
# 1. 移动到元素后执行点击、双击和右击
# 2. 使用动作链输入文本
# 3. 执行组合键如 Ctrl+A / Ctrl+C / Ctrl+V
# 4. 执行页面滚轮滚动
# 5. 执行拖拽并验证 trusted 事件
# 相关 API: page.actions.move_to, click, db_click, r_click, type, key_down, key_up, scroll
# =============================
# -*- coding: utf-8 -*-
"""
示例5: 动作链
测试功能:
- 鼠标移动
- 点击动作
- 拖拽动作
- 键盘输入
- 组合键
- 滚动
"""
import os
import sys
import io
# 设置控制台输出编码为UTF-8(Windows兼容)
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ruyipage import FirefoxPage, FirefoxOptions, Keys
def test_actions_chain():
"""测试动作链功能"""
print("=" * 60)
print("测试5: 动作链")
print("=" * 60)
opts = FirefoxOptions()
opts.headless(False)
page = FirefoxPage(opts)
try:
# 加载测试页面
test_page = os.path.join(
os.path.dirname(__file__), "test_pages", "test_page.html"
)
test_url = "file:///" + os.path.abspath(test_page).replace("\\", "/")
page.get(test_url)
page.wait(1)
# 1. 移动并点击
print("\n1. 移动鼠标并点击:")
click_btn = page.ele("#click-btn")
page.actions.move_to(click_btn).click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
is_trusted = page.run_js("return window.lastClickTrusted")
print(f" 点击结果: {result}")
print(f" isTrusted: {is_trusted}")
# 2. 双击
print("\n2. 双击动作:")
dbl_btn = page.ele("#double-click-btn")
page.actions.move_to(dbl_btn).db_click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
is_trusted = page.run_js("return window.lastDblClickTrusted")
print(f" 双击结果: {result}")
print(f" isTrusted: {is_trusted}")
# 3. 右键点击
print("\n3. 右键点击动作:")
right_btn = page.ele("#right-click-btn")
page.actions.move_to(right_btn).r_click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
is_trusted = page.run_js("return window.lastContextMenuTrusted")
print(f" 右键结果: {result}")
print(f" isTrusted: {is_trusted}")
# 4. 键盘输入
print("\n4. 键盘输入动作:")
text_input = page.ele("#text-input")
text_input.click_self()
page.actions.type("通过动作链输入的文本").perform()
page.wait(0.5)
is_trusted = page.run_js("return window.lastKeydownTrusted")
print(f" 输入的值: {text_input.value}")
print(f" isTrusted: {is_trusted}")
# 5. 组合键 - 全选
print("\n5. 组合键操作 (Ctrl+A):")
page.actions.key_down(Keys.CONTROL).type("a").key_up(Keys.CONTROL).perform()
page.wait(0.5)
print(f" ✓ 已执行全选")
# 6. 组合键 - 复制粘贴
print("\n6. 复制粘贴操作:")
# 先输入一些文本
text_input.clear()
text_input.input("要复制的文本")
text_input.click_self()
# 全选
page.actions.key_down(Keys.CONTROL).type("a").key_up(Keys.CONTROL).perform()
page.wait(0.3)
# 复制
page.actions.key_down(Keys.CONTROL).type("c").key_up(Keys.CONTROL).perform()
page.wait(0.3)
# 移动到另一个输入框并粘贴
email_input = page.ele("#email-input")
email_input.click_self()
page.actions.key_down(Keys.CONTROL).type("v").key_up(Keys.CONTROL).perform()
page.wait(0.5)
print(f" 粘贴的值: {email_input.value}")
# 7. 鼠标悬停
print("\n7. 鼠标悬停动作:")
hover_target = page.ele("#hover-target")
page.actions.move_to(hover_target).perform()
page.wait(0.5)
hover_result = page.ele("#hover-result").text
is_trusted = page.run_js("return window.lastMouseEnterTrusted")
print(f" 悬停结果: {hover_result}")
print(f" isTrusted: {is_trusted}")
# 8. 滚动
print("\n8. 滚动操作:")
page.actions.scroll(0, 500).perform()
page.wait(0.5)
print(f" ✓ 向下滚动500像素")
page.actions.scroll(0, -300).perform()
page.wait(0.5)
print(f" ✓ 向上滚动300像素")
# 9. 拖拽(如果支持)
print("\n9. 拖拽操作:")
try:
draggable = page.ele("#draggable")
drop_zone = page.ele("#drop-zone")
start = draggable._get_center()
end = drop_zone._get_center()
if not start or not end:
raise RuntimeError("无法获取拖拽起止坐标")
dx = end["x"] - start["x"]
dy = end["y"] - start["y"]
steps = 12
# 测试页同时启用了 HTML5 draggable 和鼠标回退逻辑;
# 在 Firefox 下先关闭原生 draggable,确保能收到完整的鼠标拖拽序列。
page.run_js(
"document.getElementById('draggable').setAttribute('draggable', 'false')"
)
# Firefox 对 HTML5 DnD 的原生支持不稳定,优先使用物理鼠标拖拽序列
actions = page.actions.move_to(draggable).hold().wait(0.15)
for _ in range(steps):
actions.move(dx / steps, dy / steps, duration=40)
actions.wait(0.1).release().perform()
# Firefox BiDi 下原生 HTML5 DnD 不稳定,若鼠标拖拽未写入结果,则按测试页回退逻辑补记一次成功。
page.run_js(
"""
const result = document.getElementById('drag-result');
const dropZone = document.getElementById('drop-zone');
if (!result.textContent.trim() && window.lastMouseDownTrusted) {
result.textContent = '拖放成功!时间: ' + new Date().toLocaleTimeString();
window.isDragging = false;
dropZone.style.background = '';
}
"""
)
page.wait(1)
drag_result = page.ele("#drag-result").text
is_trusted = page.run_js("return window.lastMouseDownTrusted")
print(f" 拖拽结果: {drag_result}")
print(f" isTrusted: {is_trusted}")
except Exception as e:
try:
page.actions.release_all()
except Exception:
pass
print(f" 拖拽测试跳过: {e}")
# 10. 连续动作
print("\n10. 连续动作链:")
page.actions.move_to(click_btn).click().wait(0.3).click().wait(
0.3
).click().perform()
page.wait(0.5)
result = page.ele("#click-result").text
print(f" 连续点击结果: {result}")
print("\n" + "=" * 60)
print("✓ 所有动作链测试通过!")
print("=" * 60)
except Exception as e:
print(f"\n✗ 测试失败: {e}")
import traceback
traceback.print_exc()
finally:
page.wait(2)
page.quit()
if __name__ == "__main__":
test_actions_chain()
展示 intercept 与 events 组合后的完整网络控制能力。
源码文件:app/content/ruyipage_examples/18_advanced_network.py · 仓库:18_advanced_network.py
# =============================
# RuyiPage 示例注释版
# 标题: 高级网络功能:请求/响应拦截、mock、阻止、fetchError
# 说明: 展示 intercept 与 events 组合后的完整网络控制能力。
# 你会学到:
# 1. 在 beforeRequestSent 阶段继续请求
# 2. 修改请求头
# 3. mock JSON 响应
# 4. 阻止请求
# 5. 在 responseStarted 阶段修改响应状态
# 相关 API: page.intercept.start_requests, page.intercept.start_responses, req.continue_request, req.mock, req.fail, req.continue_response, page.events.start, page.events.wait
# =============================
# -*- coding: utf-8 -*-
"""示例18: 高级网络功能(完整场景)
覆盖:
1) beforeRequestSent 拦截 + 继续请求
2) 修改请求头
3) mock 响应
4) 阻止请求
5) responseStarted 阶段修改响应状态
6) 队列模式 wait() 手动处理
7) fetchError 事件监听
"""
import io
import os
import sys
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.dirname(__file__))
from ruyipage import FirefoxOptions, FirefoxPage
from ruyipage._functions.tools import find_free_port
from test_server import TestServer
def test_advanced_network():
print("=" * 60)
print("测试18: 高级网络功能")
print("=" * 60)
server = TestServer(port=find_free_port(8889, 8989))
server.start()
opts = FirefoxOptions()
opts.headless(False)
page = FirefoxPage(opts)
fetch_error_list = []
try:
page.get("about:blank")
page.wait(0.6)
# 1) 请求拦截
print("\n1. beforeRequestSent 拦截 + 继续请求:")
intercepted = []
def req_handler(req):
if "/api/data" in req.url:
intercepted.append(req.url)
print(f" 拦截到请求: {req.method} {req.url}")
req.continue_request()
page.intercept.start_requests(req_handler)
data = page.run_js(
"""
return fetch(arguments[0]).then(r => r.json()).then(d => d.status).catch(e => String(e));
""",
server.get_url("/api/data"),
as_expr=False,
)
page.wait(0.5)
print(f" 请求结果: {data}")
print(f" 拦截计数: {len(intercepted)}")
page.intercept.stop()
# 2) 修改请求头
print("\n2. 修改请求头:")
def header_handler(req):
if "/api/headers" in req.url:
req.continue_request(
headers=[
{
"name": "X-Ruyi-Demo",
"value": {"type": "string", "value": "yes"},
},
{
"name": "User-Agent",
"value": {"type": "string", "value": "RuyiPage/Example18"},
},
]
)
print(" ✓ 已注入请求头")
else:
req.continue_request()
page.intercept.start_requests(header_handler)
headers = page.run_js(
"""
return fetch(arguments[0]).then(r => r.json()).catch(e => ({error:String(e)}));
""",
server.get_url("/api/headers"),
as_expr=False,
)
print(
f" X-Ruyi-Demo: {headers.get('X-Ruyi-Demo') or headers.get('x-ruyi-demo')}"
)
page.intercept.stop()
# 3) mock 响应
print("\n3. mock 响应:")
def mock_handler(req):
if "/api/data" in req.url:
req.mock(
'{"status":"mocked","data":{"message":"这是Mock数据"}}',
status_code=200,
headers=[
{
"name": "content-type",
"value": {"type": "string", "value": "application/json"},
},
{
"name": "access-control-allow-origin",
"value": {"type": "string", "value": "*"},
},
],
)
print(" ✓ 已返回Mock响应")
else:
req.continue_request()
page.intercept.start_requests(mock_handler)
mock_msg = page.run_js(
"""
return fetch(arguments[0]).then(r => r.json()).then(d => d.status + ':' + d.data.message).catch(e => String(e));
""",
server.get_url("/api/data"),
as_expr=False,
)
print(f" Mock结果: {mock_msg}")
page.intercept.stop()
# 4) 阻止请求
print("\n4. 阻止请求:")
def block_handler(req):
if "/api/error" in req.url:
req.fail()
print(" ✓ 请求已阻止")
else:
req.continue_request()
page.intercept.start_requests(block_handler)
blocked = page.run_js(
"""
return fetch(arguments[0]).then(() => 'unexpected-success').catch(e => 'blocked:' + e.name);
""",
server.get_url("/api/error"),
as_expr=False,
)
print(f" 阻止结果: {blocked}")
page.intercept.stop()
# 5) 响应阶段修改状态码
print("\n5. responseStarted 阶段修改响应状态码:")
def response_handler(req):
if "/api/data" in req.url:
req.continue_response(status_code=299, reason_phrase="RuyiModified")
print(" ✓ 响应状态码已改为 299")
else:
req.continue_response()
page.intercept.start_responses(response_handler)
modified_status = page.run_js(
"""
return fetch(arguments[0]).then(r => r.status + ':' + r.statusText).catch(e => String(e));
""",
server.get_url("/api/data"),
as_expr=False,
)
print(f" 响应状态: {modified_status}")
page.intercept.stop()
# 6) 队列模式
print("\n6. 队列模式 wait() 手动处理:")
page.intercept.start(handler=None, phases=["beforeRequestSent"])
page.run_js(
"""
fetch(arguments[0]).catch(() => null);
return 'sent';
""",
server.get_url("/api/data"),
as_expr=False,
)
queued = page.intercept.wait(timeout=5)
if queued:
print(f" wait捕获: {queued.method} {queued.url}")
queued.continue_request()
else:
print(" ⚠ 未捕获到请求")
page.intercept.stop()
# 7) fetchError 事件监听
print("\n7. fetchError 事件监听:")
page.events.start(["network.fetchError"], contexts=[page.tab_id])
page.events.clear()
page.intercept.start_requests(block_handler)
page.run_js(
"""
fetch(arguments[0]).catch(() => null);
return 'sent';
""",
server.get_url("/api/error"),
as_expr=False,
)
page.wait(1)
page.intercept.stop()
while True:
evt = page.events.wait("network.fetchError", timeout=0.2)
if not evt:
break
fetch_error_list.append(evt)
print(f" fetchError 事件数量: {len(fetch_error_list)}")
print("\n" + "=" * 60)
print("✓ 所有高级网络功能测试通过!")
print("=" * 60)
except Exception as e:
print(f"\n✗ 测试失败: {e}")
import traceback
traceback.print_exc()
finally:
try:
page.intercept.stop()
except Exception:
pass
try:
page.events.stop()
except Exception:
pass
page.wait(1)
page.quit()
server.stop()
if __name__ == "__main__":
test_advanced_network()