Selenium Python Selenium 源码阅读以及基础知识的补充

追风 · 2023年01月31日 · 最后由 追风 回复于 2023年02月21日 · 7620 次阅读

近期,会统一阅读 selenium 源码以及 pytest 的源码。在阅读过程中,重在理解 selenium 和 pytest 的工作原理,同时对薄弱部分的基础知识进行补充,在此进行记录。

python 基础知识

  • python 中的冒号和箭头

    • 冒号比方函数参数中或者就是对变量定义赋值的时候出现,如下
    GET: str="get"
    def Sum(num1: int, num2: int=200):
    

    这时的冒号代表类型建议符,更多的是软件工程中使用方便第一次使用这个方法的人快速知道这里的参数应该传递什么类型。但是这里不会做强制的类型检查,即使你传递的和建议的不符合也不会报错。

    • 箭头出现在函数后面,如下
    def Sum(num1: int, num2: int=200) -> int:
    

    这里的箭头是对返回值的类型建议符

  • isinstance(obj: object,class_or_tuple: _ClassInfo) 方法

    • 判断obj 是不是_class_or_tuple 类型,返回一个布尔类型
  • string.Template()/substitute()

    • string.Template() 用来定义一个字符串的模板,其中变量默认用 $ 符号标记,比方如下
    template_str = '$this is $what'
    s = Template(template_str)
    d = {'this': 'apple', 'what': 'red'}
    s.substitute(d)
    'apple is red'
    
    • 其中可以看到 template 用来根据给出的字符创造模板,substitute 负责将模板填充上实际的内容
  • 使用 f 修饰字符串

    • 最简单的用法就是源码中的用法 f{string1}{string2},这种打印出来就是 string1string2
    • f 的用法实际上的用法是 f{字符串/变量:格式},比如 f{3.1415926:2f},那么打印出来的就是 3.14
  • hasattr(): 用来判断 object 是否含有某个属性

    class Coordinate:
        x = 10
        y = -5
        z = 0
    
    point1 = Coordinate() 
    print(hasattr(point1, 'x')) # True
    print(hasattr(point1, 'y')) # True
    print(hasattr(point1, 'z')) # True
    print(hasattr(point1, 'no')) # False
    

Selenium 源码阅读/使用

  • 第一个需要考虑的问题就是 chromedriver.exe 的问题(其他浏览器也一样),我们需要选择和浏览器版本对应的 webdriver 否则是无法正常执行测试的。但是有一个现实的问题就是浏览器始终在升级,除非有特定的要求就是要在某个版本的浏览器进行测试,否则不建议一个个手动下载 webdriver.exe。这里可以选择第三方 library webdriver-manager,他会自动下载对应版本的 webdriver.exe

    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service
    from webdriver_manager.chrome import ChromeDriverManager
    
    service = Service(executable_path=ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service)
    
  • 我们运行这段代码就会发现会启动一个新的浏览器窗口,说明在上面这段代码中会将所有的初始化都做好,并且将我们需要的 session 建立好,主要得工作在下面这段代码中实现

    driver = webdriver.Chrome(service=service)
    
  • 第一个关键点在于其继承类的初始化函数中的如下代码:

    self.service.start()
    

    跟踪这段代码,发现它执行了如下代码:

    cmd = [self.path]
            cmd.extend(self.command_line_args())
            self.process = subprocess.Popen(cmd, env=self.env,
                                            close_fds=system() != 'Windows',
                                            stdout=self.log_file,
                                            stderr=self.log_file,
                                            stdin=PIPE,
                                            creationflags=self.creationflags)
    

    上面这段代码开启了一个进程,这个进程启动了 chromewebdriver.exe 文件,说明他在后台启动了 webdriver 的进程

  • Chrome() 这个方法位于 webdriver.py 文件,经过一系列的变量初始化和类的初始化函数调用(这其中其实也会有一些比较关键的点,比方设置 localhost 的端口号等等,暂时不展开),最终会进入 remote/webdriver.py 中的init() 函数中,在如下代码中进行 session 的建立

    self.start_session(capabilities, browser_profile)
    

    这一行会启动一个 session 用来执行接下来的测试,其中的 session_id 是贯穿始终的一个值,用来标注是是与这个 client 进行交互

  • 接下来我们进入到 start_session() 中看看发生了什么

  • 在 start_session() 中,最关键的是下面这一行,字面意思看他通过执行某些命令,启动了一个新的 session,那我们接下来看看,执行了什么命令,又是如何启动 session 的

    response = self.execute(Command.NEW_SESSION, parameters)
    
  • 进入这个 execute() 方法,这里面最关键的一行的代码是下面这一行,而这一行代码基本上也就是贯穿了 selenium 基本上所有的方法

    response = self.command_executor.execute(driver_command, params)
    
  • execute() 方法中,先是对 driver_command 和 params 参数进行了一些处理,最终以下面这段代码作为响应返回值返回

    return self._request(command_info[0], url, body=data)
    

这段代码中,command_info[0] 定义了 method(POST,GET...), url 为 RemotewebDriver 的 url,body 是需要传输给 webdriver 的数据,内容在上面被格式化了成 json 格式。所以通俗来讲就是,通过_request() 方法,将操作 (data) 发送给 webdriver 去执行并获取返回值。如果不继续看底层代码的话,这里就结束了 selenium 的工作原理。

  • 在_request() 方法中,首先处理了一下 url 和 header 信息,然后对 KeepAlive 参数进行了判断,由于大部分时间这个参数都是 True,所以我们也跟着 True 的情况去继续看

    response = self._conn.request(method, url, body=body, headers=headers)
    
  • 继续上面可以看到调用了一个 request() 方法,我们用跟进的方式跟进去是一个 pyi 的文件,只是提供了一些没有实现的接口,这个时候需要使用 F11 去跟进,发现 request 方法是在 urllib3/request.py 下的方法,这个方法其实也是一个过度方法,对参数变量进行了一些大小写之类的处理,然后调入相同文件的 request_encode_body() 方法

  • request_encode_body() 方法最关键的是调用 urlopen() 方法,这个 urlopen() 方法的实现处于 urllib3/poolmanager.py 中,这个 urlopen() 的方法是一个回调函数,他一直在确定是不是重定向的最后一层,如果不是就继续调用本方法去寻找,如果是的话,调用类似下列函数来获取响应

    response = conn.urlopen()
    
  • urlopen() 最终调用的是 urllib3/connectionpool.py 中的方法,这个方法是最底层的调用,所以细化了一个 httprequest 所需的所有细节的内容。这个方法中最终发送 request 的方法是如下代码:

    httplib_response = self._make_request(
                    conn,
                    method,
                    url,
                    timeout=timeout_obj,
                    body=body,
                    headers=headers,
                    chunked=chunked,
                )
    
  • 继续跟进,在_make_request() 中调用了 urllib3/connection.py 中的 request() 方法

  • request() 方法调用了 http/client.py 中的 request()->_send_request() 方法

  • 在 client.py 中经过一系列的 class 内部函数调用,最终将整个待发送的 data 整合好,在 send() 方法中依靠下列代码和 server 建立连接,同时发送:

    self.sock.sendall(data)
    
  • 至此结束,其实也没有结束。这个 sendall 最终走入了 ssl.py 的 write 方法中,奈何我无法继续跟进。如果有大佬看到麻烦指正,很想知道底层到底是如何将一个 request 发送出去。

其实,这里只看了一个建立 session 的过程,但是大部分的操作(比方说打开浏览器,get(url), find_element())都是使用的相同的方式,只不过是使用了维护的字典中不一样的信息,如下:

self._commands = {
            Command.NEW_SESSION: ('POST', '/session'),
            Command.QUIT: ('DELETE', '/session/$sessionId'),
            Command.W3C_GET_CURRENT_WINDOW_HANDLE:
                ('GET', '/session/$sessionId/window'),
            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.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.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.GET_ELEMENT_TEXT: ('GET', '/session/$sessionId/element/$id/text'),
            Command.SEND_KEYS_TO_ELEMENT:
                ('POST', '/session/$sessionId/element/$id/value'),
            Command.UPLOAD_FILE: ('POST', "/session/$sessionId/se/file"),
            Command.GET_ELEMENT_TAG_NAME:
                ('GET', '/session/$sessionId/element/$id/name'),
            Command.IS_ELEMENT_SELECTED:
                ('GET', '/session/$sessionId/element/$id/selected'),
            Command.IS_ELEMENT_ENABLED:
                ('GET', '/session/$sessionId/element/$id/enabled'),
            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_ELEMENT_ARIA_ROLE:
                ('GET', '/session/$sessionId/element/$id/computedrole'),
            Command.GET_ELEMENT_ARIA_LABEL:
                ('GET', '/session/$sessionId/element/$id/computedlabel'),
            Command.GET_SHADOW_ROOT:
                ('GET', '/session/$sessionId/element/$id/shadow'),
            Command.FIND_ELEMENT_FROM_SHADOW_ROOT:
                ('POST', '/session/$sessionId/shadow/$shadowId/element'),
            Command.FIND_ELEMENTS_FROM_SHADOW_ROOT:
                ('POST', '/session/$sessionId/shadow/$shadowId/elements'),
            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.NEW_WINDOW: ('POST', '/session/$sessionId/window/new'),
            Command.CLOSE: ('DELETE', '/session/$sessionId/window'),
            Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY:
                ('GET', '/session/$sessionId/element/$id/css/$propertyName'),
            Command.EXECUTE_ASYNC_SCRIPT: ('POST', '/session/$sessionId/execute_async'),
            Command.SET_TIMEOUTS:
                ('POST', '/session/$sessionId/timeouts'),
            Command.GET_TIMEOUTS:
                ('GET', '/session/$sessionId/timeouts'),
            Command.W3C_DISMISS_ALERT:
                ('POST', '/session/$sessionId/alert/dismiss'),
            Command.W3C_ACCEPT_ALERT:
                ('POST', '/session/$sessionId/alert/accept'),
            Command.W3C_SET_ALERT_VALUE:
                ('POST', '/session/$sessionId/alert/text'),
            Command.W3C_GET_ALERT_TEXT:
                ('GET', '/session/$sessionId/alert/text'),
            Command.W3C_ACTIONS:
                ('POST', '/session/$sessionId/actions'),
            Command.W3C_CLEAR_ACTIONS:
                ('DELETE', '/session/$sessionId/actions'),
            Command.SET_WINDOW_RECT:
                ('POST', '/session/$sessionId/window/rect'),
            Command.GET_WINDOW_RECT:
                ('GET', '/session/$sessionId/window/rect'),
            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.GET_NETWORK_CONNECTION:
                ('GET', '/session/$sessionId/network_connection'),
            Command.SET_NETWORK_CONNECTION:
                ('POST', '/session/$sessionId/network_connection'),
            Command.GET_LOG:
                ('POST', '/session/$sessionId/se/log'),
            Command.GET_AVAILABLE_LOG_TYPES:
                ('GET', '/session/$sessionId/se/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'),
            Command.PRINT_PAGE:
                ('POST', '/session/$sessionId/print'),
            Command.ADD_VIRTUAL_AUTHENTICATOR:
                ('POST', '/session/$sessionId/webauthn/authenticator'),
            Command.REMOVE_VIRTUAL_AUTHENTICATOR:
                ('DELETE', '/session/$sessionId/webauthn/authenticator/$authenticatorId'),
            Command.ADD_CREDENTIAL:
                ('POST', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credential'),
            Command.GET_CREDENTIALS:
                ('GET', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials'),
            Command.REMOVE_CREDENTIAL:
                ('DELETE', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials/$credentialId'),
            Command.REMOVE_ALL_CREDENTIALS:
                ('DELETE', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials'),
            Command.SET_USER_VERIFIED:
                ('POST', '/session/$sessionId/webauthn/authenticator/$authenticatorId/uv'),
        }

接下来还会继续去看源码,找一找有没有其他的用得是不一样的方式来触发一条请求的发送。

共收到 4 条回复 时间 点赞

get!!感谢分享

Dn__ 回复

😀 一起加油!

太猛了,大佬

TestNovice 回复

共同学习,兄弟!

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册