selenium是如何操作浏览器的?
我们使用编程语言来写selenium代码,目的是为了让浏览器按照我们的预期来自动化实现一些效果,比如:自动点击、自动输入字符、自动提交表单等。而我们操作网页的方式跟我们实际上网操作的那样,用鼠标定位到需要操作的地方,然后通过鼠标或键盘的一些动作来执行操作;但又有点不太一样,不一样的地方在于,我们定位到需要操作的地方是通过元素,也就是html中对应的标签去定位;而执行操作的方式,就是通过dom对象封装的函数来操作元素。所以,我们通过selenium来使浏览器自动化进行操作,实际上操作的是网页的html(见下图)
webdriver
python是一门编程语言,html是一门标记性语言,这两个语言是无法互通的。我们只能通过python语言去操作同属于python语言的代码,而不能操作java、php、JavaScript、html等非python语言的代码。所以,我们要想通过python操作html需要借助一些手段,除了要使用selenium模块里丰富的函数外,还需要借助第三方来把selenium写出来的代码解释给html听,然后让html去执行。
我们知道,两个不同的程序要想进行数据通信,就要借助接口。所以python要想跟html进行数据通信,就要通过接口进行数据传输。而这个接口由谁来充当呢?由webdriver(浏览器驱动)来充当。接口一般放在哪呢?放在服务器上。所以webdriver的其中一个作用就是为不同的编程语言与不同浏览器中的html进行数据交互时提供接口(见下图)
所以,有了webdriver就相当于在编程语言和浏览器之间搭建了一座桥梁,通过编程语言写的每一条selenium脚本,都会变成一个http请求并发送给浏览器驱动,而浏览器驱动中有一个http server负责接收客户端(编程语言写的selenium脚本)传过来的http请求。接收到请求之后,http server根据请求来操作具体的浏览器,如果是chromeDriver,就会取操作chrome浏览器;如果是firefoxDriver,就会去操作火狐浏览器。浏览器执行完相应的步骤之后,就会把执行的结果返回给webDriver中的http server,然后http server再将结果返回给客户端(编程语言),如果执行出错了就会在控制台中打印错误信息。
webdriver协议
上面所说selenium客户端与webdriver通信的协议是http协议,其实selenium把自己又封装了下,写了一大堆的接口,发给了w3c组织并获得许可,这些接口统一起来称为JsonWireProtocol协议。也就是说,selenium写的每一个脚本,背后都会去调用webdriver封装好的接口,通过JsonWireProtocol协议与webdriver进行通信。这也是为什么不同的编程语言能够操作浏览器的根本原因,不管你用什么语言写的selenium代码,都会调用webdriver的接口(也就是走JsonWireProtocol协议)。
以下是webdriver封装的api,详情可参考selenium-github的网址:
https://github.com/seleniumhq/selenium/wiki/jsonwireprotocol
webdriver与浏览器如何通信?
selenium客户端与webdriver是通过JsonWireProtocol协议进行通信的,那么webdriver又怎么与浏览器进行通信呢?其实webdriver与浏览器也是通过驱动(或者说接口)与浏览器进行通信的,而这个接口就是JavaScript API。
代码层面理解
我们导入webdriver包,然后进入webdriver这个模块看看:
可以看到这里导入了很多浏览器的驱动,而上面的webdriver.Chrome()其实就是去生成一个chrome下面的webdriver对象。
这里我们进入chrome.webdriver下面的webDriver:
进入chrome/webdriver.py后,在webdriver类中,调用了一个Service方法,然后又调用了start方法,这里我们进入start方法看下:
可以看到,这个方法其实是启动一个服务,这个服务就是webDriver,而cmd就是ChromeDriver.exe的所在路径。所以上面的service().start()就是启动ChromeDriver:
再返回刚刚的代码,上面调用的Service().start()是启动一个webdriver,那下面这段代码是干嘛的呢》这里我们进入ChromeRemoteConnection这个类看看:
接着再进入它继承的类:RemoteConnection:
class ChromeRemoteConnection(RemoteConnection):
def __init__(self, remote_server_addr, keep_alive=True):
RemoteConnection.__init__(self, remote_server_addr, keep_alive)
self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app')
self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions')
self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions')
self._commands['executeCdpCommand'] = ('POST', '/session/$sessionId/goog/cdp/execute')
这里代码分为三段。第一段封装了一个commands的列表,这里放入了很多Command类的变量,其实这里就是给每一个selenium的指令指引去访问哪一个接口;第二段封装了一个execute的方法,方法中传入刚刚封装的commands,最后调用了_request方法;最后一段就是_request方法,这里调用了urllib3的PoolManager。这里的urllib3跟requests很类似,都是发起一次http的请求,只不过requests是第三方模块,而urllib3是python的内置模块。最后调用request方法,发起http请求。
self._commands = {
Command.STATUS: ('GET', '/status'),
Command.NEW_SESSION: ('POST', '/session'),
Command.GET_ALL_SESSIONS: ('GET', '/sessions'),
Command.QUIT: ('DELETE', '/session/$sessionId'),
"""省略下面的代码"""
}
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)
"""省略后面的代码"""
再返回来看下这段很重要的代码。前面Service().start()是创建一个webdriver服务,打开对应的驱动:ChromeDriver.exe;而下面的RemoteWebDriver其实就是初始化一个selenium的客户端,然后调用webdriver中的接口,向webdriver发起请求。
生活的例子来理解
拿乘客、司机、汽车举个例子。乘客上车后,告诉司机去北京路,然后告诉司机怎么走;接着司机操纵汽车,按照乘客的指示驾驶;汽车则受司机方向盘的控制,行驶到具体的地点。
这里乘客就相当于selenium client,也就是各种编程语言,如:java、php、python;selenium则相当于跟司机交谈时的语言,这种语言乘客、司机都能听得懂;司机相当于webdriver,司机也分很多种,有只会驾驶汽车的司机,相当于chromeDriver之于chrome浏览器,也有只会驾驶大巴的司机,相当于firefoxDriver之于火狐浏览器;司机的大脑、手组合则相当于webDriver中的http server;汽车则相当于浏览器。不同国家的乘客(不同的编程语言)都能通过司机(webdriver)来操作汽车(浏览器)来到达他们的目的地(达到他们想要的操作,如点击按钮、提交表单等)。乘客上车后,脑海里想好要去什么地方,怎么去,相当于在写selenium脚本。想好目的地及怎么去之后,就会告诉司机,相当于selenium脚本转化成http请求发送给webDriver。司机的头脑接收到乘客说的话,理解意思并在脑海里规划路线之后,就会用手操作方向盘,驾驶汽车到达乘客指定地点,相当于webDriver中的http server接收到api(编程语言)的http 请求,进行相应的处理之后,就通过json这种通信的数据格式与浏览器进行交互,而浏览器接收到webDriver发过来的json数据后,就会转化成javaScript代码,从而对dom对象进行操作,以达到客户端的目的。司机看到目的地的一个路标之后,就知道到了乘客所说的目的地,然后告诉乘客,目的地到了。这就相当于浏览器在进行完操作之后,就会返回结果给webDriver的http server,接着server再将结果返回给客户端。