WebDriver

WebDriver提供了另外一种方式与浏览器进行交互。那就是利用浏览器原生的API,封装成一套更加面向对象的Selenium WebDriver API,直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的)。由于使用的是浏览器的原生API,速度大大提高,而且调用的稳定性交给了浏览器厂商本身,显然是更加科学。然而带来的一些副作用就是,不同的浏览器厂商,对Web元素的操作和呈现存在不同程度的差异,这就要求Selenium WebDriver要分浏览器厂商的不同,提供不同的实现,例如Chrome有专门的ChromeDriver,Firefox有FirefoxDriver等等。

WebDriver Wire协议是通用的,也就是说不管是FirefoxDriver还是ChromeDriver,启动之后都会在某一个端口启动基于这套协议的Web Service。例如ChromeDriver初始化成功之后,默认会从http://localhost:46350开始,而FirefoxDriver从http://localhost:7055开始。后续我们调用WebDriver的任何API,都需要借助一个ComandExecutor发送一个命令,实际上是一个HTTP request给监听端口上的Web Service。在我们的HTTP request的body中,会以WebDriver Wire协议规定的JSON格式的字符串来告诉Selenium我们希望浏览器接下来做什么事情。
v2ff124852096fe8c03c6472f205dd0065_r.jpg

工作流程如下图所示:

我们使用Selenium实现自动化测试,主要需要3个东西

1.测试脚本,可以是python,java编写的脚本程序(也可以叫做client端)

2.浏览器驱动, 这个驱动是根据不同的浏览器开发的,不同的浏览器使用不同的webdriver驱动程序且需要对应相应的浏览器版本,比如:chromedriver.exe(chrome)

3.浏览器,目前selenium支持市面上大多数浏览器,如:火狐,谷歌,IE等

selenium脚本

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/8 10:40
# @Author  : shisuiyi
# @File    : select1.py
# @Software: win10 Tensorflow1.13.1 python3.9
from selenium import webdriver

driver = webdriver.Chrome()

执行上述代码,我们会发现程序打开了Chrome浏览器(前提:你已经正确配置了chrome的驱动和对应版本)

那么selenium是如何实现这个过程的呢?ok,我们今天就通过分析源码的方式来理解selenium的工作原理

源码分析

查看weddriver源码(按住Ctrl键,鼠标点击Chrome)

webdriver.py

import warnings

from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
from .remote_connection import ChromeRemoteConnection
from .service import Service
from .options import Options


class WebDriver(RemoteWebDriver):
    """
    Controls the ChromeDriver and allows you to drive the browser.

    You will need to download the ChromeDriver executable from
    http://chromedriver.storage.googleapis.com/index.html
    """

    def __init__(self, executable_path="chromedriver", port=0,
                 options=None, service_args=None,
                 desired_capabilities=None, service_log_path=None,
                 chrome_options=None, keep_alive=True):
        """
        Creates a new instance of the chrome driver.

        Starts the service and then creates new instance of chrome driver.

        :Args:
         - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH
         - port - port you would like the service to run, if left as 0, a free port will be found.
         - options - this takes an instance of ChromeOptions
         - service_args - List of args to pass to the driver service
         - desired_capabilities - Dictionary object with non-browser specific
           capabilities only, such as "proxy" or "loggingPref".
         - service_log_path - Where to log information from the driver.
         - chrome_options - Deprecated argument for options
         - keep_alive - Whether to configure ChromeRemoteConnection to use HTTP keep-alive.
        """
        if chrome_options:
            warnings.warn('use options instead of chrome_options',
                          DeprecationWarning, stacklevel=2)
            options = chrome_options

        if options is None:
            # desired_capabilities stays as passed in
            if desired_capabilities is None:
                desired_capabilities = self.create_options().to_capabilities()
        else:
            if desired_capabilities is None:
                desired_capabilities = options.to_capabilities()
            else:
                desired_capabilities.update(options.to_capabilities())

        self.service = Service(
            executable_path,
            port=port,
            service_args=service_args,
            log_path=service_log_path)
        self.service.start()
        
        try:
            RemoteWebDriver.__init__(
                self,
                command_executor=ChromeRemoteConnection(
                    remote_server_addr=self.service.service_url,
                    keep_alive=keep_alive),
                desired_capabilities=desired_capabilities)
        except Exception:
            self.quit()
            raise
        self._is_remote = False
  # ......

self.service = Service

self.service.start()

通过源码中发现,初始化了一个service对象,然后调用了start()方法

# .....
def start(self):
    """
    Starts the Service.

    :Exceptions:
     - WebDriverException : Raised either when it can't start the service
       or when it can't connect to the service
    """
    try:
        cmd = [self.path]
        cmd.extend(self.command_line_args())
        self.process = subprocess.Popen(cmd, env=self.env,
                                        close_fds=platform.system() != 'Windows',
                                        stdout=self.log_file,
                                        stderr=self.log_file,
                                        stdin=PIPE)
 # .....

我们发现其中其实就是执行了一个cmd命令,命令的作用就是启动了chromedriver.exe Chrome浏览器的驱动程序

这个过程和我们手动启动浏览器驱动是一样的效果,类似下面的结果

Starting ChromeDriver 98.0.4758.80 (7f0488e8ba0d8e019187c6325a16c29d9b7f4989-refs/branch-heads/4758@{#972}) on port 9515
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.

启动驱动程序后,绑定端口号9515,且只允许本地访问这个服务

这里我们已经知道了执行脚本webdriver.Chrome()会自动执行 chromedirver.exe驱动程序,然后开启一个进程

如何打开浏览器

我们继续看webdriver.py源码,调用了父类RemoteWebDriver 的初始化方法,我们看这个方法做了什么事?

这里有一行最重要的代码,self.start_session(capabilities, browser_profile) 这个方法,继续看一下这个方法的源码做了什么工作

#...
if browser_profile is not None:
    warnings.warn("Please use FirefoxOptions to set browser profile",
                  DeprecationWarning, stacklevel=2)
self.start_session(capabilities, browser_profile)
self._switch_to = SwitchTo(self)
self._mobile = Mobile(self)
self.file_detector = file_detector or LocalFileDetector()
#...
def start_session(self, capabilities, browser_profile=None):
    """
    Creates a new session with the desired capabilities.

    :Args:
     - browser_name - The name of the browser to request.
     - version - Which browser version to request.
     - platform - Which platform to request the browser on.
     - javascript_enabled - Whether the new session should support JavaScript.
     - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object. Only used if Firefox is requested.
    """
    if not isinstance(capabilities, dict):
        raise InvalidArgumentException("Capabilities must be a dictionary")
    if browser_profile:
        if "moz:firefoxOptions" in capabilities:
            capabilities["moz:firefoxOptions"]["profile"] = browser_profile.encoded
        else:
            capabilities.update({'firefox_profile': browser_profile.encoded})
    w3c_caps = _make_w3c_caps(capabilities)
    parameters = {"capabilities": w3c_caps,
                  "desiredCapabilities": capabilities}
    response = self.execute(Command.NEW_SESSION, parameters)
    if 'sessionId' not in response:
        response = response['value']
    self.session_id = response['sessionId']
    self.capabilities = response.get('value')

    # if capabilities is none we are probably speaking to
    # a W3C endpoint
    if self.capabilities is None:
        self.capabilities = response.get('capabilities')

    # Double check to see if we have a W3C Compliant browser
    self.w3c = response.get('status') is None
    self.command_executor.w3c = self.w3c

分析这部分源码 response = self.execute(Command.NEW_SESSION, parameters)可以发现是向地址localhost:9515/session发送了一个post请求,参数是json格式的,然后返回特定的响应信息给程序(这里主要就是新建了一个sessionid),最终打开了浏览器

打开浏览器的操作完成了。

如何执行对应操作

查看webdriver.py源码

51         try:
52             RemoteWebDriver.__init__(
53                 self,
54                 command_executor=ChromeRemoteConnection(
55                     remote_server_addr=self.service.service_url,
56                     keep_alive=keep_alive),
57                 desired_capabilities=desired_capabilities)

点击ChromeRemoteConnection查看一下源码

 1 from selenium.webdriver.remote.remote_connection import RemoteConnection
 2 
 3 
 4 class ChromeRemoteConnection(RemoteConnection):
 5 
 6     def __init__(self, remote_server_addr, keep_alive=True):
 7         RemoteConnection.__init__(self, remote_server_addr, keep_alive)
 8         self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app')
 9         self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions')
10         self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions')
11         self._commands['executeCdpCommand'] = ('POST', '/session/$sessionId/goog/cdp/execute')

第7行访问的是localhost:9515/session地址,第8-11行,定义了一些和我们使用的浏览器(chrome)特有的接口地址,我们再看一下父类RemoteConnection里面源码

self._commands = {
    Command.STATUS: ('GET', '/status'),
    Command.NEW_SESSION: ('POST', '/session'),
    Command.GET_ALL_SESSIONS: ('GET', '/sessions'),
    Command.QUIT: ('DELETE', '/session/$sessionId'),
    Command.GET_CURRENT_WINDOW_HANDLE:
        ('GET', '/session/$sessionId/window_handle'),
    Command.W3C_GET_CURRENT_WINDOW_HANDLE:
        ('GET', '/session/$sessionId/window'),
    Command.GET_WINDOW_HANDLES:
        ('GET', '/session/$sessionId/window_handles'),
    Command.W3C_GET_WINDOW_HANDLES:
        ('GET', '/session/$sessionId/window/handles'),
    Command.GET: ('POST', '/session/$sessionId/url'),
    Command.GO_FORWARD: ('POST', '/session/$sessionId/forward'),
    Command.GO_BACK: ('POST', '/session/$sessionId/back'),
    Command.REFRESH: ('POST', '/session/$sessionId/refresh'),
    Command.EXECUTE_SCRIPT: ('POST', '/session/$sessionId/execute'),
    Command.W3C_EXECUTE_SCRIPT:
        ('POST', '/session/$sessionId/execute/sync'),
    Command.W3C_EXECUTE_SCRIPT_ASYNC:
        ('POST', '/session/$sessionId/execute/async'),
    Command.GET_CURRENT_URL: ('GET', '/session/$sessionId/url'),
    Command.GET_TITLE: ('GET', '/session/$sessionId/title'),
    Command.GET_PAGE_SOURCE: ('GET', '/session/$sessionId/source'),
    Command.SCREENSHOT: ('GET', '/session/$sessionId/screenshot'),
    Command.ELEMENT_SCREENSHOT: ('GET', '/session/$sessionId/element/$id/screenshot'),
    Command.FIND_ELEMENT: ('POST', '/session/$sessionId/element'),
    Command.FIND_ELEMENTS: ('POST', '/session/$sessionId/elements'),
    Command.W3C_GET_ACTIVE_ELEMENT: ('GET', '/session/$sessionId/element/active'),
    Command.GET_ACTIVE_ELEMENT:
        ('POST', '/session/$sessionId/element/active'),
    Command.FIND_CHILD_ELEMENT:
        ('POST', '/session/$sessionId/element/$id/element'),
    Command.FIND_CHILD_ELEMENTS:
        ('POST', '/session/$sessionId/element/$id/elements'),
    Command.CLICK_ELEMENT: ('POST', '/session/$sessionId/element/$id/click'),
    Command.CLEAR_ELEMENT: ('POST', '/session/$sessionId/element/$id/clear'),
    Command.SUBMIT_ELEMENT: ('POST', '/session/$sessionId/element/$id/submit'),
    Command.GET_ELEMENT_TEXT: ('GET', '/session/$sessionId/element/$id/text'),
    Command.SEND_KEYS_TO_ELEMENT:
        ('POST', '/session/$sessionId/element/$id/value'),
    Command.SEND_KEYS_TO_ACTIVE_ELEMENT:
        ('POST', '/session/$sessionId/keys'),
    Command.UPLOAD_FILE: ('POST', "/session/$sessionId/file"),
    Command.GET_ELEMENT_VALUE:
        ('GET', '/session/$sessionId/element/$id/value'),
    Command.GET_ELEMENT_TAG_NAME:
        ('GET', '/session/$sessionId/element/$id/name'),
    Command.IS_ELEMENT_SELECTED:
        ('GET', '/session/$sessionId/element/$id/selected'),
    Command.SET_ELEMENT_SELECTED:
        ('POST', '/session/$sessionId/element/$id/selected'),
    Command.IS_ELEMENT_ENABLED:
        ('GET', '/session/$sessionId/element/$id/enabled'),
    Command.IS_ELEMENT_DISPLAYED:
        ('GET', '/session/$sessionId/element/$id/displayed'),
    Command.GET_ELEMENT_LOCATION:
        ('GET', '/session/$sessionId/element/$id/location'),
    Command.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW:
        ('GET', '/session/$sessionId/element/$id/location_in_view'),
    Command.GET_ELEMENT_SIZE:
        ('GET', '/session/$sessionId/element/$id/size'),
    Command.GET_ELEMENT_RECT:
        ('GET', '/session/$sessionId/element/$id/rect'),
    Command.GET_ELEMENT_ATTRIBUTE:
        ('GET', '/session/$sessionId/element/$id/attribute/$name'),
    Command.GET_ELEMENT_PROPERTY:
        ('GET', '/session/$sessionId/element/$id/property/$name'),
    Command.GET_ALL_COOKIES: ('GET', '/session/$sessionId/cookie'),
    Command.ADD_COOKIE: ('POST', '/session/$sessionId/cookie'),
    Command.GET_COOKIE: ('GET', '/session/$sessionId/cookie/$name'),
    Command.DELETE_ALL_COOKIES:
        ('DELETE', '/session/$sessionId/cookie'),
    Command.DELETE_COOKIE:
        ('DELETE', '/session/$sessionId/cookie/$name'),
    Command.SWITCH_TO_FRAME: ('POST', '/session/$sessionId/frame'),
    Command.SWITCH_TO_PARENT_FRAME: ('POST', '/session/$sessionId/frame/parent'),
    Command.SWITCH_TO_WINDOW: ('POST', '/session/$sessionId/window'),
    Command.CLOSE: ('DELETE', '/session/$sessionId/window'),
    Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY:
        ('GET', '/session/$sessionId/element/$id/css/$propertyName'),
    Command.IMPLICIT_WAIT:
        ('POST', '/session/$sessionId/timeouts/implicit_wait'),
    Command.EXECUTE_ASYNC_SCRIPT: ('POST', '/session/$sessionId/execute_async'),
    Command.SET_SCRIPT_TIMEOUT:
        ('POST', '/session/$sessionId/timeouts/async_script'),
    Command.SET_TIMEOUTS:
        ('POST', '/session/$sessionId/timeouts'),
    Command.DISMISS_ALERT:
        ('POST', '/session/$sessionId/dismiss_alert'),
    Command.W3C_DISMISS_ALERT:
        ('POST', '/session/$sessionId/alert/dismiss'),
    Command.ACCEPT_ALERT:
        ('POST', '/session/$sessionId/accept_alert'),
    Command.W3C_ACCEPT_ALERT:
        ('POST', '/session/$sessionId/alert/accept'),
    Command.SET_ALERT_VALUE:
        ('POST', '/session/$sessionId/alert_text'),
    Command.W3C_SET_ALERT_VALUE:
        ('POST', '/session/$sessionId/alert/text'),
    Command.GET_ALERT_TEXT:
        ('GET', '/session/$sessionId/alert_text'),
    Command.W3C_GET_ALERT_TEXT:
        ('GET', '/session/$sessionId/alert/text'),
    Command.SET_ALERT_CREDENTIALS:
        ('POST', '/session/$sessionId/alert/credentials'),
    Command.CLICK:
        ('POST', '/session/$sessionId/click'),
    Command.W3C_ACTIONS:
        ('POST', '/session/$sessionId/actions'),
    Command.W3C_CLEAR_ACTIONS:
        ('DELETE', '/session/$sessionId/actions'),
    Command.DOUBLE_CLICK:
        ('POST', '/session/$sessionId/doubleclick'),
    Command.MOUSE_DOWN:
        ('POST', '/session/$sessionId/buttondown'),
    Command.MOUSE_UP:
        ('POST', '/session/$sessionId/buttonup'),
    Command.MOVE_TO:
        ('POST', '/session/$sessionId/moveto'),
    Command.GET_WINDOW_SIZE:
        ('GET', '/session/$sessionId/window/$windowHandle/size'),
    Command.SET_WINDOW_SIZE:
        ('POST', '/session/$sessionId/window/$windowHandle/size'),
    Command.GET_WINDOW_POSITION:
        ('GET', '/session/$sessionId/window/$windowHandle/position'),
    Command.SET_WINDOW_POSITION:
        ('POST', '/session/$sessionId/window/$windowHandle/position'),
    Command.SET_WINDOW_RECT:
        ('POST', '/session/$sessionId/window/rect'),
    Command.GET_WINDOW_RECT:
        ('GET', '/session/$sessionId/window/rect'),
    Command.MAXIMIZE_WINDOW:
        ('POST', '/session/$sessionId/window/$windowHandle/maximize'),
    Command.W3C_MAXIMIZE_WINDOW:
        ('POST', '/session/$sessionId/window/maximize'),
    Command.SET_SCREEN_ORIENTATION:
        ('POST', '/session/$sessionId/orientation'),
    Command.GET_SCREEN_ORIENTATION:
        ('GET', '/session/$sessionId/orientation'),
    Command.SINGLE_TAP:
        ('POST', '/session/$sessionId/touch/click'),
    Command.TOUCH_DOWN:
        ('POST', '/session/$sessionId/touch/down'),
    Command.TOUCH_UP:
        ('POST', '/session/$sessionId/touch/up'),
    Command.TOUCH_MOVE:
        ('POST', '/session/$sessionId/touch/move'),
    Command.TOUCH_SCROLL:
        ('POST', '/session/$sessionId/touch/scroll'),
    Command.DOUBLE_TAP:
        ('POST', '/session/$sessionId/touch/doubleclick'),
    Command.LONG_PRESS:
        ('POST', '/session/$sessionId/touch/longclick'),
    Command.FLICK:
        ('POST', '/session/$sessionId/touch/flick'),
    Command.EXECUTE_SQL:
        ('POST', '/session/$sessionId/execute_sql'),
    Command.GET_LOCATION:
        ('GET', '/session/$sessionId/location'),
    Command.SET_LOCATION:
        ('POST', '/session/$sessionId/location'),
    Command.GET_APP_CACHE:
        ('GET', '/session/$sessionId/application_cache'),
    Command.GET_APP_CACHE_STATUS:
        ('GET', '/session/$sessionId/application_cache/status'),
    Command.CLEAR_APP_CACHE:
        ('DELETE', '/session/$sessionId/application_cache/clear'),
    Command.GET_NETWORK_CONNECTION:
        ('GET', '/session/$sessionId/network_connection'),
    Command.SET_NETWORK_CONNECTION:
        ('POST', '/session/$sessionId/network_connection'),
    Command.GET_LOCAL_STORAGE_ITEM:
        ('GET', '/session/$sessionId/local_storage/key/$key'),
    Command.REMOVE_LOCAL_STORAGE_ITEM:
        ('DELETE', '/session/$sessionId/local_storage/key/$key'),
    Command.GET_LOCAL_STORAGE_KEYS:
        ('GET', '/session/$sessionId/local_storage'),
    Command.SET_LOCAL_STORAGE_ITEM:
        ('POST', '/session/$sessionId/local_storage'),
    Command.CLEAR_LOCAL_STORAGE:
        ('DELETE', '/session/$sessionId/local_storage'),
    Command.GET_LOCAL_STORAGE_SIZE:
        ('GET', '/session/$sessionId/local_storage/size'),
    Command.GET_SESSION_STORAGE_ITEM:
        ('GET', '/session/$sessionId/session_storage/key/$key'),
    Command.REMOVE_SESSION_STORAGE_ITEM:
        ('DELETE', '/session/$sessionId/session_storage/key/$key'),
    Command.GET_SESSION_STORAGE_KEYS:
        ('GET', '/session/$sessionId/session_storage'),
    Command.SET_SESSION_STORAGE_ITEM:
        ('POST', '/session/$sessionId/session_storage'),
    Command.CLEAR_SESSION_STORAGE:
        ('DELETE', '/session/$sessionId/session_storage'),
    Command.GET_SESSION_STORAGE_SIZE:
        ('GET', '/session/$sessionId/session_storage/size'),
    Command.GET_LOG:
        ('POST', '/session/$sessionId/log'),
    Command.GET_AVAILABLE_LOG_TYPES:
        ('GET', '/session/$sessionId/log/types'),
    Command.CURRENT_CONTEXT_HANDLE:
        ('GET', '/session/$sessionId/context'),
    Command.CONTEXT_HANDLES:
        ('GET', '/session/$sessionId/contexts'),
    Command.SWITCH_TO_CONTEXT:
        ('POST', '/session/$sessionId/context'),
    Command.FULLSCREEN_WINDOW:
        ('POST', '/session/$sessionId/window/fullscreen'),
    Command.MINIMIZE_WINDOW:
        ('POST', '/session/$sessionId/window/minimize')
}

这个类里面定义了所有的selenium操作需要的接口地址(这些接口地址全部封装在浏览器驱动程序中),那么所有的浏览器操作就是通过访问这些接口来实现的

其中 Command.GET: (‘POST’, ‘/session/$sessionId/url’) 这个地址就是实现访问一个网址的url ,我们先记录一下后面有用

ok,所有的操作对应接口地址我们知道了,那么又怎样执行这些接口来达到在浏览器上实现各种操作呢?继续看紧接着接口地址定义下面的源码

def execute(self, command, params):
    """
    Send a command to the remote server.

    Any path subtitutions required for the URL mapped to the command should be
    included in the command parameters.

    :Args:
     - command - A string specifying the command to execute.
     - params - A dictionary of named parameters to send with the command as
       its JSON payload.
    """
    command_info = self._commands[command]
    assert command_info is not None, 'Unrecognised command %s' % command
    path = string.Template(command_info[1]).substitute(params)
    if hasattr(self, 'w3c') and self.w3c and isinstance(params, dict) and 'sessionId' in params:
        del params['sessionId']
    data = utils.dump_json(params)
    url = '%s%s' % (self._url, path)
    return self._request(command_info[0], url, body=data)

    def _request(self, method, url, body=None):
        """
        Send an HTTP request to the remote server.

        :Args:
         - method - A string for the HTTP method to send the request with.
         - url - A string for the URL to send the request to.
         - body - A string for request body. Ignored unless method is POST or PUT.

        :Returns:
          A dictionary with the server's parsed JSON response.
        """
        LOGGER.debug('%s %s %s' % (method, url, body))

        parsed_url = parse.urlparse(url)
        headers = self.get_remote_connection_headers(parsed_url, self.keep_alive)
        resp = None
        if body and method != 'POST' and method != 'PUT':
            body = None

        if self.keep_alive:
            resp = self._conn.request(method, url, body=body, headers=headers)

            statuscode = resp.status
        else:
            http = urllib3.PoolManager(timeout=self._timeout)
            resp = http.request(method, url, body=body, headers=headers)

            statuscode = resp.status
            if not hasattr(resp, 'getheader'):
                if hasattr(resp.headers, 'getheader'):
                    resp.getheader = lambda x: resp.headers.getheader(x)
                elif hasattr(resp.headers, 'get'):
                    resp.getheader = lambda x: resp.headers.get(x)

        data = resp.data.decode('UTF-8')
        try:
            if 300 <= statuscode < 304:
                return self._request('GET', resp.getheader('location'))
            if 399 < statuscode <= 500:
                return {'status': statuscode, 'value': data}
            content_type = []
            if resp.getheader('Content-Type') is not None:
                content_type = resp.getheader('Content-Type').split(';')
            if not any([x.startswith('image/png') for x in content_type]):

                try:
                    data = utils.load_json(data.strip())
                except ValueError:
                    if 199 < statuscode < 300:
                        status = ErrorCode.SUCCESS
                    else:
                        status = ErrorCode.UNKNOWN_ERROR
                    return {'status': status, 'value': data.strip()}

                # Some of the drivers incorrectly return a response
                # with no 'value' field when they should return null.
                if 'value' not in data:
                    data['value'] = None
                return data
            else:
                data = {'status': 0, 'value': data}
                return data
        finally:
            LOGGER.debug("Finished Request")
            resp.close()

可以看到主要是通过execute方法调用_request方法通过urilib3标准库向服务器发送对应操作请求地址,进而实现了浏览器各种操作

有人会问打开浏览器和操作浏览器实现各种动作是怎么关联的呢?

其实,打开浏览器也是发送请求,请求会返回一个sessionid,后面操作的各种接口地址,你也会发现接口地址中存在一个变量$sessionid,那么不难猜测打开浏览器和操作浏览器就是用过sessionid关联到一起,达到在同一个浏览器中做操作

第二步在浏览其上实现各种操作原理也完成了

总结

  1. selenium client(python等语言编写的自动化测试脚本)初始化一个service服务,通过Webdriver启动浏览器驱动程序chromedriver.exe

  2. 通过RemoteWebDriver向浏览器驱动程序发送HTTP请求,浏览器驱动程序解析请求,打开浏览器,并获得sessionid,如果再次对浏览器操作需携带此id

  3. 打开浏览器,绑定特定的端口,把启动后的浏览器作为webdriver的remote server

  4. 打开浏览器后,所有的selenium的操作(访问地址,查找元素等)均通过RemoteConnection链接到remote server,然后使用execute方法调用_request方法通过urlib3向remote server发送请求

  5. 浏览器通过请求的内容执行对应动作

  6. 浏览器再把执行的动作结果通过浏览器驱动程序返回给测试脚本