杰瑞科技汇

python selenium 源码

Selenium 并不是一个单一的项目,而是一个包含多个组件的生态系统,它的 Python 绑定 (selenium) 只是其中的一部分。

python selenium 源码-图1
(图片来源网络,侵删)

我们将从以下几个方面来解析其源码:

  1. Selenium 生态系统概览:了解各个组件如何协同工作。
  2. Python 绑定 (selenium) 的核心架构:分析 selenium 库的源码结构。
  3. 核心流程剖析:以 driver.get() 为例,看懂一次请求的完整生命周期。
  4. WebDriver 协议:Python 库与浏览器驱动沟通的“普通话”。
  5. 如何阅读和调试源码:提供实用的方法。

Selenium 生态系统概览

要理解 Python Selenium 的源码,首先必须理解它的架构,Selenium 采用了客户端-服务器模型。

  1. 你的测试脚本 (Client):

    • 这就是你用 Python (或 Java, C#, 等) 编写的自动化测试脚本。
    • 它通过 selenium 这个 Python 库与 Selenium 的 WebDriver 协议进行交互。
  2. WebDriver 绑定库:

    python selenium 源码-图2
    (图片来源网络,侵删)
    • 这就是 selenium Python 包本身,它不直接操作浏览器,而是: a. 将你的操作(如 find_element, click())转换成标准的 JSON 命令。 b. 通过 HTTP 请求将这些命令发送给浏览器驱动。 c. 接收来自浏览器驱动的响应(如元素信息、执行结果)。
  3. 浏览器驱动:

    • 这是真正的“翻译官”和“执行者”,每个浏览器都有自己的驱动(如 chromedriver, geckodriver)。
    • 它是一个独立的小程序,你的测试脚本启动它。
    • 它监听来自 selenium 库的 HTTP 请求。
    • 它将这些 JSON 命令转换成浏览器可以理解的、底层的自动化 API 调用(在 Chrome 中是使用 Chrome DevTools Protocol)。
    • 它执行命令,并将结果(如页面截图、元素属性)返回给 selenium 库。
  4. 浏览器:

    最终的目标,它接收来自驱动的指令,并执行相应的操作(导航、点击、输入等)。

关键点:你的 Python 脚本 从不直接与浏览器通信,它始终通过浏览器驱动这个中间层,这就是为什么你需要为每个浏览器下载并配置对应的驱动程序。

python selenium 源码-图3
(图片来源网络,侵删)

Python 绑定 (selenium) 的核心架构

selenium 库的源码在 GitHub 上是开源的:https://github.com/SeleniumHQ/selenium/tree/trunk/py

我们可以通过其目录结构来理解它的设计思想:

selenium/
├── __init__.py              # 包的入口点,导出最常用的类和函数
├── api/                     # 高级 API,我们日常使用的接口都在这里
│   ├── webelement.py        # WebElement 类,代表页面上的一个元素
│   └── webdriver.py         # WebDriver 类,代表整个浏览器会话
├── common/
│   ├── exceptions.py        # 定义了所有 Selenium 的异常类
│   ├── utils/               # 各种工具函数,如文件路径处理、等待等
│   └── ...                  # 其他通用组件
├── remote/
│   ├── webdriver.py         # **核心!** 远程 WebDriver 的实现
│   ├── connection.py        # **核心!** 处理与驱动的 HTTP 连接
│   └── webelement.py        # 远程 WebElement 的实现
└── ...

核心类解析

  1. selenium.webdriver.remote.webdriver.WebDriver:

    • 这是所有操作的起点,是我们最常接触的 driver 对象。
    • 它负责管理整个会话的生命周期(start_session, quit)。
    • 它提供了所有高级操作,如 get(), find_element(), execute_script() 等。
    • 它内部持有一个 RemoteConnection 对象,用于与驱动通信。
  2. selenium.webdriver.remote.connection.RemoteConnection:

    • 这是通信层的核心,它封装了与浏览器驱动的所有 HTTP 交互。
    • 它有一个 execute() 方法,这是将命令发送给驱动的底层方法。
    • 它处理了请求的序列化(将 Python 对象转为 JSON)和响应的反序列化(将 JSON 转为 Python 对象)。
  3. selenium.webdriver.remote.webelement.WebElement:

    • 代表页面上的一个 DOM 元素。
    • 它本身并不包含元素数据,而是通过 driver.execute() 向驱动发送命令,并附带一个 element ID(由驱动在 find_element 时返回)。
    • 当你调用 element.text 时,它内部会向驱动发送一个 getElementText 命令,并附上这个元素的 ID。
  4. selenium.webdriver.common.by.By:

    • 一个简单的枚举类,定义了定位元素的方式(ID, XPATH, CSS_SELECTOR 等)。

核心流程剖析:driver.get("https://www.google.com")

让我们以最常用的 driver.get() 方法为例,追踪其源码执行流程,感受整个“客户端-驱动-浏览器”的交互过程。

步骤 1:你的 Python 脚本调用

from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://www.google.com") # <-- 调用这里

步骤 2:WebDriver.get() 方法 (在 selenium/webdriver/remote/webdriver.py) 当你调用 driver.get(url) 时,执行的是 WebDriver 类中的 get 方法。

# 简化后的源码
class WebDriver:
    # ...
    def get(self, url: str) -> None:
        """
        Loads a web page in the current browser session.
        """
        self.execute(Command.GET, {'url': url}) # <-- 关键!

可以看到,get 方法非常简洁,它只是调用了 self.execute() 方法,并传入了两个参数:

  • Command.GET:这是一个预定义的命令常量,代表“导航”操作。
  • {'url': url}:一个包含 URL 的字典,作为命令的参数。

步骤 3:execute() 方法 (在 selenium/webdriver/remote/webdriver.pyconnection.py) execute() 方法是真正的核心,它负责与驱动通信。

# 简化后的源码
class WebDriver:
    # ...
    def execute(self, driver_command, params=None):
        """
        Sends a command to be executed by a command.CommandExecutor.
        """
        # self.command_executor 是一个 RemoteConnection 对象
        # 它负责实际的 HTTP 通信
        response = self.command_executor.execute(driver_command, params)
        # ... 处理响应,检查错误等 ...
        return response

这里,self.command_executor 通常就是 RemoteConnection 的一个实例,真正的执行被委托给了它。

步骤 4:RemoteConnection.execute() 方法 (在 selenium/webdriver/remote/connection.py) 这是将命令打包成 HTTP 请求并发送的地方。

# 简化后的源码
class RemoteConnection:
    # ...
    def execute(self, command, params=None):
        """
        Send a command to the remote server.
        """
        # 1. 准备请求体
        payload = {
            'url': self._url,  # 驱动的地址,如 'http://127.0.0.1:9515'
            'sessionId': self.session_id, # 当前会话的 ID
            'command': command, #  'get'
        }
        if params:
            payload['parameters'] = params #  {'url': 'https://www.google.com'}
        # 2. 序列化并发送 HTTP POST 请求
        response = self._request('POST', '/session/$sessionId/' + command_info[0], body=payload)
        # 3. 处理响应
        if response['status'] == 0:
            return response['value']
        else:
            # 抛出异常
            raise WebDriverException(response['value'], response['status'])

这个方法做了三件大事:

  1. 构建载荷:将命令名 (command)、参数 (params)、会话 ID (sessionId) 等信息打包成一个字典。
  2. 发送请求:通过 _request() 方法(内部使用 requests 库)向驱动发送一个 HTTP POST 请求,请求的 URL 类似于 http://127.0.0.1:9515/session/<session_id>/url
  3. 处理响应:接收驱动的 JSON 响应,如果响应状态码不为 0(表示成功),则抛出异常;否则,返回响应体中的 value 字段。

步骤 5:浏览器驱动执行 chromedriver 接收到了这个 HTTP POST 请求,它解析出 url 参数,然后使用 Chrome 的自动化接口(通常是 CDP)让 Chrome 浏览器打开 https://www.google.com,完成后,chromedriver 会返回一个成功的 JSON 响应。

步骤 6:响应返回 RemoteConnection 收到响应,WebDriver.get() 方法执行完毕,你的 Python 脚本继续往下运行。


WebDriver 协议

从上面的流程可以看出,Python 库和驱动之间通过一套标准化的协议(WebDriver Protocol)通信,这个协议是基于 HTTP 和 JSON 的。

  • 命令:如 get, findElement, click, executeScript 等。
  • 端点:如 /session, /session/{sessionId}/element, /session/{sessionId}/element/{elementId}/click
  • 请求/响应格式:统一的 JSON 结构。

Selenium 的 Python 库实现了一套JSON Wire Protocol(较旧)和一套更现代的W3C WebDriver Protocol,现在默认使用 W3C 协议,你可以通过阅读 selenium/webdriver/remote/common/command.py 来看到所有支持的命令定义。


如何阅读和调试源码

  1. 安装源码: 在你的虚拟环境中,使用 pip install -e . 的方式安装 selenium 的源码,这样你就可以在 IDE 中直接跳转到源码并进行调试。

    git clone https://github.com/SeleniumHQ/selenium.git
    cd selenium/trunk/py
    pip install -e .
  2. 使用 IDE 调试

    • 在 VS Code 或 PyCharm 中,设置断点。
    • 运行你的测试脚本。
    • 当执行到断点时,单步步入 (Step Into),你会清晰地看到调用栈如何从你的脚本一层层深入到 WebDriver -> execute -> RemoteConnection
  3. 打印日志: Selenium 提供了强大的日志功能,可以让你看到所有发送给驱动的命令和接收到的响应,这对于调试非常有用。

    from selenium import webdriver
    import logging
    # 配置日志
    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger('selenium.webdriver.remote.remote_connection')
    logger.setLevel(logging.DEBUG)
    # 创建驱动时传入服务日志
    service = webdriver.chrome.service.Service(log_path='chromedriver.log')
    driver = webdriver.Chrome(service=service)
    # 现在运行你的脚本,你会在控制台看到详细的命令/响应日志
    driver.get("https://www.example.com")
    driver.find_element("id", "some-id").click()
    driver.quit()

    这些日志会精确显示 POST /session/... 请求的内容和 200 OK 响应的内容,让你能直接看到底层协议的交互。

理解 Selenium Python 库的源码,关键在于把握其分层架构代理模式

  • 分层:高级 API (WebDriver, WebElement) -> 命令转换 -> HTTP 通信 (RemoteConnection)。
  • 代理selenium 库是用户的代理,而浏览器驱动是 selenium 库与浏览器之间的代理。

通过阅读源码,你会发现它设计得非常清晰和模块化。WebDriver 类关注“做什么”,而 RemoteConnection 类关注“怎么做”,这种解耦使得 Selenium 能够支持多种编程语言和多种浏览器。

分享:
扫描分享到社交APP
上一篇
下一篇