欢迎光临
我们一直在努力

影刀RPA进阶教程:Selenium混合开发与复杂页面交互方案

影刀RPA进阶教程:Selenium混合开发与复杂页面交互方案

作者:林焱 | 标签:#RPA #Selenium #混合开发 #页面交互 #影刀进阶


痛点切入

影刀RPA的「点击元素」和「输入文本」指令在90%的场景下都够用。但剩下10%的复杂场景会让你怀疑人生。

去年做一个ERP系统自动化项目时遇到的几个典型翻车场景:

在这里插入图片描述

  • iframe嵌套:页面里套了3层iframe,影刀的元素定位器在第三层iframe里完全找不到目标。
  • Shadow DOM:用了Web Components技术的页面,元素藏在 Shadow Root 里面,影刀原生指令无法穿透。
  • 动态加载列表:下拉滚动才加载更多数据,影刀没有原生的滚动到底部指令。
  • Canvas图表点击:数据分析平台的图表用Canvas渲染,影刀无法定位Canvas内部的坐标。

这些问题不是影刀的缺陷,而是RPA工具本质上是基于DOM元素定位的,面对这些非常规页面结构,需要另一套武器。

影刀负责流程编排和数据处理,Selenium负责复杂页面交互。 这就是本文要讲的混合方案。


核心方法

架构思路:

影刀 RPA(主帅)
├── 流程控制(循环、条件判断、异常处理)
├── 数据处理(Excel读写、数据库、变量管理)
├── 简单页面操作(点击、输入、提取文本)
└── 调用 Selenium(特种兵)
├── iframe 跨越
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9c4ad367ee854ae3b6bb432d3268ea1e.png#pic_center)

├── Shadow DOM 穿透
├── Canvas 操作
├── 复杂滚动
└── 文件上传下载

技术实现路径:

影刀通过「执行Python脚本」指令来驱动Selenium。Python脚本内部创建独立的Chrome驱动,完成复杂交互操作后,将结果数据返回给影刀。两个浏览器实例(影刀内置浏览器 + Selenium浏览器)可以访问同一个网站,用同一个登录态。


代码实现

完整混合开发框架

# ============================================================
# 影刀RPA × Selenium 混合开发框架 v2.0
#
# 使用方式:
# 在影刀流程中插入「执行Python脚本」指令
# 复制以下代码,根据场景调用对应方法
# ============================================================

import json
import time
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
TimeoutException,
NoSuchElementException,
StaleElementReferenceException,
ElementClickInterceptedException
)

class SeleniumHelper:
"""
Selenium辅助类 —— 为影刀RPA定制的复杂页面操作工具

每个方法设计为:
1. 接收必要的输入参数
2. 返回JSON格式的结果(方便影刀解析)
3. 内置异常处理和重试逻辑
"""

def __init__(self, headless=False):
"""
初始化Selenium驱动

参数:
headless: 是否无头模式(调试时建议False,线上运行可用True)

踩坑:headless模式下的渲染行为和正常模式有差异,
某些网站的懒加载在headless下不触发,需手动执行scroll
"""
options = webdriver.ChromeOptions()

# — 反检测配置 —
# 去掉自动化标记
options.add_argument("–disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)

# 设置窗口大小(避免 mobile 布局)
options.add_argument("–window-size=1920,1080")

# — 性能优化 —
options.add_argument("–disable-gpu")
options.add_argument("–disable-dev-shm-usage")
options.add_argument("–no-sandbox")
options.add_argument("–disable-extensions")

# 无头模式
if headless:
options.add_argument("–headless=new") # 新版headless参数

# — 加载用户数据(共享影刀的登录态)—
# 关键:设置 user-data-dir 指向影刀的Chrome用户数据目录
# 这样Selenium打开的浏览器和影刀内置浏览器共享Cookie/登录态
# 默认路径示例,实际路径需在影刀中确认
# user_data_dir = os.path.expanduser(
# "~/.yingdao-browser/Default"
# )
# options.add_argument(f"–user-data-dir={user_data_dir}")

self.driver = webdriver.Chrome(options=options)
self.wait = WebDriverWait(self.driver, 10)
print("[Selenium] 驱动初始化完成")

# =======================================================
# 场景一:iframe 嵌套操作
# =======================================================

def operate_in_iframe(self, iframe_selectors: list, action: dict) –> str:
"""
跨越多层iframe执行操作

参数:
iframe_selectors: iframe的CSS选择器列表,从外到内
示例: ["#frame1", "#frame2", "#inner"]
action: 要在最内层iframe中执行的操作
示例: {"type": "click", "selector": "#submit-btn"}

返回:
JSON字符串:{"success": true, "message": "…"}

踩坑记录:
– 切换到iframe后,WebDriver的上下文就变成iframe内的DOM
– 如果操作完不回切到默认上下文,后续操作会找不到元素
– frame标签加载是异步的,必须等待iframe可用后再切换
"""
try:
# 逐层进入iframe
for i, selector in enumerate(iframe_selectors):
print(f"[Selenium] 进入第{i+1}层iframe: {selector}")
# 等待iframe可用
iframe = self.wait.until(
EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, selector))
)

# 在最内层iframe中执行操作
action_type = action.get("type", "click")
target_selector = action.get("selector", "")

if action_type == "click":
element = self.wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, target_selector))
)
element.click()

elif action_type == "input":
element = self.wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, target_selector))
)
element.clear()
element.send_keys(action.get("value", ""))

elif action_type == "get_text":
element = self.wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, target_selector))
)
text = element.text
# 切回默认上下文
self.driver.switch_to.default_content()
return json.dumps({"success": True, "data": text})

# 操作完成后切回默认上下文(非常重要!)
self.driver.switch_to.default_content()
return json.dumps({"success": True, "message": "操作成功"})

except TimeoutException:
self.driver.switch_to.default_content()
return json.dumps({"success": False, "error": "iframe或元素加载超时"})
except Exception as e:
self.driver.switch_to.default_content()
return json.dumps({"success": False, "error": str(e)})

# =======================================================
# 场景二:Shadow DOM 穿透
# =======================================================

def operate_in_shadow_dom(self, host_selector: str,
inner_selectors: list) –> str:
"""
穿透Shadow DOM操作元素

参数:
host_selector: Shadow Host元素的CSS选择器
例如: "user-profile-card"
inner_selectors: Shadow DOM内部元素的选择器列表
例如: ["#name", "#email"]

返回:
内部元素的文本内容列表

原理:
Shadow DOM是封装的,普通的 document.querySelector 无法穿透
必须通过 host.shadowRoot 属性进入内部DOM
"""
try:
# 找到Shadow Host元素
host = self.wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, host_selector))
)

# 通过JavaScript穿透Shadow Root
results = []
for selector in inner_selectors:
# 执行JS在Shadow DOM内部查找
js_code = f"""
const host = document.querySelector('
{host_selector}');
if (!host || !host.shadowRoot) return null;
const el = host.shadowRoot.querySelector('
{selector}');
return el ? el.textContent || el.value || 'found' : null;
"""

result = self.driver.execute_script(js_code)
results.append({"selector": selector, "value": result})
print(f"[Shadow DOM] {selector} => {result}")

return json.dumps({"success": True, "data": results})

except Exception as e:
return json.dumps({"success": False, "error": str(e)})

# =======================================================
# 场景三:无限滚动加载(下拉到底)
# =======================================================

def scroll_to_bottom_and_collect(self, scroll_selector: str = "body",
item_selector: str = "",
max_scrolls: int = 50) –> str:
"""
持续滚动到底部,收集所有可见元素

参数:
scroll_selector: 滚动容器的选择器(默认是body)
item_selector: 要收集的元素CSS选择器
max_scrolls: 最大滚动次数(防止死循环)

原理:
不断执行 scrollTop = scrollHeight,每次滚动后等待0.5秒
比较前后元素数量,如果数量不变说明到底了
"""
items = []
prev_count = 0
no_change_count = 0

for i in range(max_scrolls):
# 当前可见元素
elements = self.driver.find_elements(
By.CSS_SELECTOR, item_selector
)
current_count = len(elements)

# 检查是否到达底部
if current_count == prev_count:
no_change_count += 1
if no_change_count >= 3: # 连续3次没有新内容,认定到底了
print(f"[滚动] 已到达底部,共收集 {current_count} 条")
break
else:
no_change_count = 0

prev_count = current_count

# 滚动到底部
self.driver.execute_script(f"""
const el = document.querySelector('
{scroll_selector}');
el.scrollTop = el.scrollHeight;
"""
)

# 等待新内容加载
time.sleep(1.5)

if (i + 1) % 10 == 0:
print(f"[滚动] 第{i+1}次,当前 {current_count} 条")

# 收集最终数据
elements = self.driver.find_elements(By.CSS_SELECTOR, item_selector)
for el in elements:
try:
items.append(el.text.strip())
except StaleElementReferenceException:
continue # DOM刷新导致的元素失效,跳过

return json.dumps({
"success": True,
"count": len(items),
"data": items
})

# =======================================================
# 场景四:Canvas图表数据提取
# =======================================================

def extract_canvas_coords(self, canvas_selector: str,
target_points: list) –> str:
"""
精准定位Canvas内部的元素坐标

适用场景:数据分析平台、图表工具中的Canvas元素

参数:
canvas_selector: Canvas元素的CSS选择器
target_points: 要点击的坐标列表(相对于Canvas左上角)
例如: [{"x": 150, "y": 200, "action": "click"}]

原理:
先获取Canvas在页面中的绝对位置,
然后在绝对坐标执行ActionChains点击
"""
canvas = self.wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, canvas_selector))
)

# 获取Canvas在页面中的绝对位置
location = canvas.location
size = canvas.size

print(f"[Canvas] 位置: ({location['x']}, {location['y']})")
print(f"[Canvas] 尺寸: {size['width']}x{size['height']}")

results = []
for point in target_points:
# 计算绝对坐标 = Canvas左上角 + 内部偏移
abs_x = location['x'] + point['x']
abs_y = location['y'] + point['y']

action = point.get('action', 'click')

try:
if action == 'click':
ActionChains(self.driver)\\
.move_by_offset(abs_x, abs_y)\\
.click()\\
.perform()
# 重置鼠标位置
ActionChains(self.driver)\\
.move_by_offset(–abs_x, –abs_y)\\
.perform()
results.append({"point": point, "status": "clicked"})
elif action == 'hover':
ActionChains(self.driver)\\
.move_by_offset(abs_x, abs_y)\\
.perform()
results.append({"point": point, "status": "hovered"})
except Exception as e:
results.append({"point": point, "status": f"error: {e}"})

return json.dumps({"success": True, "results": results})

def close(self):
"""安全关闭浏览器"""
try:
self.driver.quit()
except:
pass
print("[Selenium] 驱动已关闭")

# ============================================================
# 影刀RPA集成调用示例
#
# 在影刀流程中:
# 1. 插入「执行Python脚本」指令
# 2. 粘贴以下代码(根据场景选择对应方法)
# 3. 输出结果自动存储到影刀变量中
# ============================================================

if __name__ == "__main__":
helper = SeleniumHelper(headless=False)

# — 示例1: iframe嵌套操作 —
print("\\n=== 示例1: iframe嵌套 ===")
helper.driver.get("https://example.com/page-with-iframes")
time.sleep(3)

result = helper.operate_in_iframe(
iframe_selectors=["#outer-frame", "#inner-frame"],
action={"type": "click", "selector": "#confirm-btn"}
)
print(result)

# — 示例2: Shadow DOM —
# print("\\n=== 示例2: Shadow DOM ===")
# result = helper.operate_in_shadow_dom(
# host_selector="user-profile-card",
# inner_selectors=["#name", "#email", "#phone"]
# )
# print(result)

# — 示例3: 无限滚动 —
# print("\\n=== 示例3: 无限滚动 ===")
# result = helper.scroll_to_bottom_and_collect(
# scroll_selector=".feed-container",
# item_selector=".feed-item"
# )
# print(result)

helper.close()


实战对比:影刀纯RPA vs Selenium混合方案

在这里插入图片描述

场景影刀纯RPASelenium混合
普通表单填写 5分钟,拖拽即可 需要写代码,10分钟
3层iframe内操作 无法定位元素 operate_in_iframe() 一键搞定
Shadow DOM 不支持 execute_script 穿透
无限滚动列表 手动循环+碰运气 scroll_to_bottom() 精确到底
Canvas点击 只能用图像识别(不准) 坐标计算+ActionChains,像素级精准
文件上传 支持的格式有限 send_keys(文件路径) 全兼容

结论: 日常简单流程用影刀,遇到复杂交互时调用Selenium。二者互补,而不是替代。


常见问题速查

Q1: Selenium打开的新浏览器没有登录状态

现象: 影刀内置浏览器已登录,Selenium打开的还是未登录页面。

原因: Selenium默认使用临时用户目录,不共享影刀浏览器的登录态。

在这里插入图片描述

解决方式: 在ChromeOptions中设置 –user-data-dir 指向影刀浏览器的用户数据目录。在影刀中打开浏览器后,地址栏输入 chrome://version/ 查看 个人资料路径,将该路径传给Selenium的 user-data-dir 参数。注意:同一个用户数据目录不能同时被两个Chrome进程使用。

Q2: 调用Selenium后影刀流程卡住不继续

现象: Python脚本执行完了,但影刀的后续指令不执行。

原因: Python进程没有正确退出,或者print输出阻塞了。

解决方式: 确保Python脚本末尾有明确的 driver.quit() 关闭浏览器,并且主流程不要有 input() 这样的阻塞调用。如果问题依旧,在影刀中给「执行Python脚本」指令设置超时时间(建议30秒)。

Q3: 嵌套iframe中,第一层成功但第二层失败

现象: 切换到第一层iframe正常,进入第二层时报NoSuchElement。

在这里插入图片描述

原因: 第二层iframe可能是动态加载的(延迟出现在DOM中),frame_to_be_available_and_switch_to_it 的等待时间不够。

解决方式: 在切换每层iframe之间增加 time.sleep(1) 确保DOM更新。另外检查第二层iframe的CSS选择器是否正确——进入iframe后DOM上下文已经变了,选择器是基于iframe内DOM的,不是基于外层页面的。在Chrome DevTools中手动进入每层iframe后,确认选择器是否匹配。

Q4: Shadow DOM中的点击事件无效

现象: 元素找到了但点击没有反应。

原因: Shadow DOM内部的事件监听器可能使用了 event.composedPath() 或 event.target 校验,直接触发点击事件但事件目标不是原始元素。

解决方式: 通过JavaScript在Shadow DOM内部直接dispatch MouseEvent:

const host = document.querySelector('your-selector');
const inner = host.shadowRoot.querySelector('button');
inner.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
![在这里插入图片描述](https://i–blog.csdnimg.cn/direct/90c95e468bb841ea902e218741d2d517.png#pic_center)

Q5: headless模式截图是空白的

现象: 使用 headless=True 后,driver.save_screenshot() 得到一张白屏图片。

原因: 某些Chrome版本在headless模式下默认窗口大小为0x0,或者页面渲染依赖GPU加速。

解决方式: 在ChromeOptions中显式设置窗口大小 –window-size=1920,1080,并添加 –disable-gpu 参数。如果还不行,换成 –headless=new(Chrome 112+支持的新无头模式,渲染更接近正常模式)。

Q6: 如何让Selenium操作影刀当前正在看的页面?

现象: 不想新开浏览器,想在影刀当前的浏览器中执行Selenium操作。

原因: 影刀的浏览器是内部管理的,不能直接被外部WebDriver连接。

解决方式: 采用 –remote-debugging-port 模式:在影刀启动浏览器时附加调试端口参数(如果影刀支持),然后Selenium通过 debuggerAddress 连接到已有浏览器实例。如果影刀不支持自定义启动参数,则退而求其次:让Selenium新开浏览器,通过 –user-data-dir 共享Cookie,操作完成后影刀刷新页面即可看到结果。
在这里插入图片描述


推荐资源

  • Selenium官方文档:selenium-python.readthedocs.io,特别是 Waits 和 ActionChains 章节。
  • Chrome DevTools Protocol Viewer:了解 Runtime.evaluate 等CDP命令,可以在Selenium中用 execute_cdp_cmd() 直接调用。
  • SelectorsHub浏览器插件:快速生成正确的CSS选择器和XPath,对Shadow DOM也有支持。

作者:林焱
标签: #RPA #Selenium #混合开发 #iframe #ShadowDOM #Canvas #影刀进阶

赞(0)
未经允许不得转载:171主机测评 » 影刀RPA进阶教程:Selenium混合开发与复杂页面交互方案
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址