FindElements Mixin 架构
FindElementsMixin 代表了 Pydoll 中的一个关键架构决策:使用组合优于继承在 Tab 和 WebElement 之间共享元素查找能力,而不通过公共基类耦合它们。本文档探讨 mixin 模式、其实现以及元素定位的内部机制。
Mixin 模式:设计理念
什么是 Mixin?
Mixin 是一个旨在向其他类提供方法的类,而不是传统继承层次结构中的基类。与标准继承(建模"is-a"关系)不同,mixin 建模"can-do"能力。
# 传统继承:"is-a"
class Animal:
def breathe(self): ...
class Dog(Animal): # Dog IS-A Animal(狗是一种动物)
def bark(self): ...
# Mixin 模式:"can-do"
class FlyableMixin:
def fly(self): ...
class Bird(Animal, FlyableMixin): # Bird IS-A Animal, CAN fly(鸟是动物,能飞)
pass
为什么使用 Mixin 而不是继承?
Pydoll 面临特定的架构挑战:
Tab需要在文档上下文中查找元素WebElement需要相对于自身查找元素(子元素)- 两者都需要相同的选择器逻辑(CSS、XPath、属性构建)
选项 1:共享基类
class ElementLocator:
def find(...): ...
class Tab(ElementLocator):
pass
class WebElement(ElementLocator):
pass
问题:
- 紧耦合:Tab 和 WebElement 现在共享继承层次结构
- 违反单一职责:Tab 不应该从与 WebElement 相同的类继承
- 难以扩展:添加新功能需要修改基类
选项 2:Mixin 模式(选定方法)
class FindElementsMixin:
def find(...): ...
def query(...): ...
class Tab(FindElementsMixin):
# Tab 特定逻辑
pass
class WebElement(FindElementsMixin):
# WebElement 特定逻辑
pass
优点:
- 解耦:
Tab和WebElement保持独立 - 可重用性:两个类中使用相同的元素查找逻辑
- 可组合性:可以添加其他 mixin 而不会冲突
- 可测试性:Mixin 可以单独测试
Mixin 特性
- 无状态:Mixin 不维护自己的状态(没有
__init__) - 依赖注入:假定使用类提供依赖项(例如
_connection_handler) - 单一目的:每个 mixin 提供一个内聚的能力
- 不可实例化:永远不要直接创建
FindElementsMixin()
Pydoll 中的 Mixin 实现
类结构
FindElementsMixin 使用依赖注入与提供 _connection_handler 的任何类一起工作:
class FindElementsMixin:
"""
提供元素查找能力的 Mixin。
假定使用类具有:
- _connection_handler: 用于 CDP 命令的 ConnectionHandler 实例
- _object_id: 用于上下文相对搜索的 Optional[str](仅 WebElement)
"""
if TYPE_CHECKING:
_connection_handler: ConnectionHandler # 类型提示,不是实际属性
async def find(self, ...):
# 实现使用 self._connection_handler
# 检查 self._object_id 以确定上下文
关键见解: Mixin 不定义 _connection_handler 或 _object_id。它通过鸭子类型假定它们存在。
Tab 和 WebElement 如何使用 Mixin
# Tab:文档级搜索
class Tab(FindElementsMixin):
def __init__(self, browser, target_id, connection_port):
self._connection_handler = ConnectionHandler(connection_port)
# 没有 _object_id → 从文档根开始搜索
# WebElement:元素相对搜索
class WebElement(FindElementsMixin):
def __init__(self, object_id, connection_handler, ...):
self._object_id = object_id # CDP 对象 ID
self._connection_handler = connection_handler
# 有 _object_id → 相对于此元素搜索
关键区别:
- Tab:
hasattr(self, '_object_id')→False→ 使用RuntimeCommands.evaluate()(文档上下文) - WebElement:
hasattr(self, '_object_id')→True→ 使用RuntimeCommands.call_function_on()(元素上下文)
上下文检测
Mixin 动态检测搜索上下文:
async def _find_element(self, by, value, raise_exc=True):
if hasattr(self, '_object_id'):
# 相对搜索:在此元素上调用 JavaScript 函数
command = self._get_find_element_command(by, value, self._object_id)
else:
# 文档搜索:在全局上下文中评估 JavaScript
command = self._get_find_element_command(by, value)
response = await self._execute_command(command)
# ...
这个单一实现处理两者:
tab.find(id='submit')→ 搜索整个文档form_element.find(id='submit')→ 在form_element内搜索
Mixin 依赖耦合
Mixin 紧密耦合到 CDP 的对象模型。它假定:
- 元素由
objectId字符串表示 - 文档搜索使用
Runtime.evaluate() - 元素相对搜索使用
Runtime.callFunctionOn()
这是可以接受的,因为 Pydoll 是 CDP 特定的。更通用的设计需要抽象层。
公共 API 设计
Mixin 暴露两个具有不同设计理念的高级方法:
find():基于属性的选择
@overload
async def find(self, find_all: Literal[False], ...) -> WebElement: ...
@overload
async def find(self, find_all: Literal[True], ...) -> list[WebElement]: ...
async def find(
self,
id: Optional[str] = None,
class_name: Optional[str] = None,
name: Optional[str] = None,
tag_name: Optional[str] = None,
text: Optional[str] = None,
timeout: int = 0,
find_all: bool = False,
raise_exc: bool = True,
**attributes,
) -> Union[WebElement, list[WebElement], None]:
设计决策:
-
Kwargs 优于位置 By 枚举:
-
自动解析为最佳选择器:
- 单个属性 → 使用
By.ID、By.CLASS_NAME等(最快) -
多个属性 → 构建 XPath(灵活但较慢)
-
**attributes用于扩展性:
query():基于表达式的选择
@overload
async def query(self, expression, find_all: Literal[False], ...) -> WebElement: ...
@overload
async def query(self, expression, find_all: Literal[True], ...) -> list[WebElement]: ...
async def query(
self,
expression: str,
timeout: int = 0,
find_all: bool = False,
raise_exc: bool = True
) -> Union[WebElement, list[WebElement], None]:
设计决策:
-
自动检测 CSS vs XPath:
-
单个表达式参数(与
find()不同): - 假定用户知道选择器语法
-
没有抽象开销
-
直接传递到浏览器:
- CSS 使用
querySelector()/querySelectorAll() - XPath 使用
document.evaluate()
类型安全的重载模式
两种方法都使用 @overload 提供精确的返回类型:
# IDE 知道返回类型是 WebElement
element = await tab.find(id='submit')
# IDE 知道返回类型是 list[WebElement]
elements = await tab.find(class_name='item', find_all=True)
# IDE 知道返回类型是 Optional[WebElement]
maybe_element = await tab.find(id='optional', raise_exc=False)
这对于 IDE 自动完成和类型检查至关重要。有关详细信息,请参阅类型系统深入了解。
选择器解析架构
Mixin 通过解析管道将用户输入转换为 CDP 命令:
| 阶段 | 输入 | 输出 | 关键决策 |
|---|---|---|---|
| 1. 方法选择 | find() kwargs 或 query() 表达式 |
选择器策略 | 基于属性 vs 基于表达式 |
| 2. 策略解析 | 属性或表达式 | By 枚举 + 值 |
单个属性 → 原生方法,多个 → XPath |
| 3. 上下文检测 | By + 值 + hasattr(_object_id) |
CDP 命令类型 | 文档 vs 元素相对搜索 |
| 4. 命令生成 | CDP 命令类型 + 选择器 | JavaScript + CDP 方法 | evaluate() vs callFunctionOn() |
| 5. 执行 | CDP 命令 | objectId 或 objectId 数组 |
通过 ConnectionHandler |
| 6. WebElement 创建 | objectId + 属性 |
WebElement 实例 |
工厂函数避免循环导入 |
关键架构决策
1. 单个 vs 多个属性
# 单个属性 → 直接选择器(快速)
await tab.find(id='username') # 使用 By.ID → getElementById()
# 多个属性 → XPath(灵活)
await tab.find(tag_name='input', type='password', name='pwd')
# → //input[@type='password' and @name='pwd']
为什么这很重要:
- 原生方法(getElementById、getElementsByClassName)比 XPath 快 10-50%
- 组合属性时 XPath 开销可接受(无替代方案)
2. 选择器类型的自动检测
实现:
启发式是明确的 - CSS 选择器不能以 / 开头。
3. XPath 相对路径调整
对于元素相对搜索,绝对 XPath 必须转换:
# 用户提供://div
# 对于 WebElement:.//div(相对于元素,而不是文档)
def _ensure_relative_xpath(xpath):
return f'.{xpath}' if not xpath.startswith('.') else xpath
没有这个,element.find() 将从文档根开始搜索。
CDP 命令生成
Mixin 根据搜索上下文路由到不同的 CDP 方法:
| 上下文 | 选择器类型 | CDP 方法 | JavaScript 等价 |
|---|---|---|---|
| 文档 | CSS | Runtime.evaluate |
document.querySelector() |
| 文档 | XPath | Runtime.evaluate |
document.evaluate() |
| 元素 | CSS | Runtime.callFunctionOn |
this.querySelector() |
| 元素 | XPath | Runtime.callFunctionOn |
document.evaluate(..., this) |
关键见解: Runtime.callFunctionOn 需要一个 objectId(要调用的元素),而 Runtime.evaluate 在全局范围内执行。
JavaScript 模板
Pydoll 使用预定义的模板以保持一致性和性能:
# CSS 选择器
Scripts.QUERY_SELECTOR = 'document.querySelector("{selector}")'
Scripts.RELATIVE_QUERY_SELECTOR = 'this.querySelector("{selector}")'
# XPath 表达式
Scripts.FIND_XPATH_ELEMENT = '''
document.evaluate("{escaped_value}", document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
'''
模板避免运行时字符串连接并集中 JavaScript 代码。
对象 ID 解析和 WebElement 创建
CDP 将 DOM 节点表示为 objectId 字符串。Mixin 抽象了这一点:
单个元素流程:
1. 执行 CDP 命令 → 从响应中提取 objectId
2. 调用 DOM.describeNode(objectId) → 获取属性、标签名
3. 创建 WebElement(objectId, connection_handler, attributes)
多个元素流程:
1. 执行 CDP 命令 → 返回作为单个远程对象的数组
2. 调用 Runtime.getProperties(array_objectId) → 枚举数组索引
3. 为每个元素提取单独的 objectId
4. 描述并为每个创建 WebElement
为什么使用 Runtime.getProperties? CDP 不直接返回数组 - 它返回对数组对象的引用。我们必须枚举其属性以提取单个元素。
架构见解和设计权衡
为什么使用 Kwargs 而不是 By 枚举?
Pydoll 的选择:
Selenium 的方法:
理由:
- 可发现性:IDE 自动完成显示所有可用参数
- 可组合性:可以在一次调用中组合多个属性
- 可读性:
id='submit'比(By.ID, 'submit')更直观
权衡: Kwargs 对选择器策略不够明确。通过文档和日志记录解决。
为什么自动检测 CSS vs XPath?
_get_expression_type() 启发式消除了用户负担:
await tab.query("//div") # 自动:XPath
await tab.query("#login") # 自动:CSS
await tab.query("div > p") # 自动:CSS
优点:
- 人体工程学:用户不需要指定选择器类型
- 正确性:不可能误用(使用 CSS 方法的 XPath,反之亦然)
限制: 无法强制对模糊选择器进行 CSS 解释(罕见的边缘情况)。
防止循环导入:create_web_element()
Mixin 使用工厂函数来避免循环导入:
def create_web_element(*args, **kwargs):
"""在运行时动态导入 WebElement。"""
from pydoll.elements.web_element import WebElement # 延迟导入
return WebElement(*args, **kwargs)
为什么需要?
FindElementsMixin→ 需要创建WebElementWebElement→ 从FindElementsMixin继承- 循环依赖!
解决方案: 工厂函数内的延迟导入。导入仅在调用函数时执行,打破循环。
hasattr() 进行上下文检测:优雅还是 Hacky?
Mixin 使用 hasattr(self, '_object_id') 检测 Tab vs WebElement:
这是"hacky"吗?
- 不:这是鸭子类型(Pythonic 习语)
- Mixin 不需要知道类层次结构
- Tab 和 WebElement 都提供
_connection_handler - WebElement 另外提供
_object_id
替代方法:
- 类型检查:
if isinstance(self, WebElement)→ 将 mixin 耦合到 WebElement - 抽象方法:要求 Tab/WebElement 实现
get_search_context()→ 更多样板代码 - 依赖注入:将上下文作为参数传递 → 破坏 API 人体工程学
结论: hasattr() 是此用例的最佳解决方案。
关键要点
- Mixin 实现代码共享,而不通过继承耦合
Tab和WebElement - 通过鸭子类型进行上下文检测(
hasattr)使 mixin 与类层次结构解耦 - 自动解析优化性能,通过对单个属性使用原生方法
- XPath 构建提供可组合性用于多属性查询
- 基于轮询的等待很简单,但以 CPU 周期换取实现简单性
- CDP 对象模型复杂性隐藏在 WebElement 抽象后面
- 通过重载实现类型安全为 IDE 支持提供精确的返回类型
相关文档
要更深入地了解相关架构组件:
- 类型系统:重载模式、TypedDict、泛型类型
- WebElement 域:WebElement 架构和交互方法
- 选择器指南:CSS vs XPath 语法和最佳实践
- Tab 域:Tab 级操作和上下文管理
有关实际使用模式: