使用 docker 部署基于 selenium+chrome-headless的服务
使用 docker 部署基于 selenium+chrome-headless的服务
1、编写 docker-compose 文件
bs-whatweb-chrome:
image: selenium/standalone-chrome:latest # 使用官方镜像
ports: # 端口映射(后续可能没用)
- 9999:4444
shm_size: 2g # docker 默认的共享内存只有 64M,当启动多个 Chrome 实例的时候可能会导致 Chrome 崩溃,所以需要增大/dev/shm的内存
2、基础配置
selenium 容器的 hostname 是 Chrome,所以需要修改COMMAND_EXECUTOR的 IP地址,修改如下:
bs-whatweb-chrome 为项目中关于 selenium容器名字
# chromedriver_url
COMMAND_EXECUTOR=http://bs-whatweb-chrome:4444/wd/hub
3、使用示例
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
# 无界面浏览器获取最新的网址(网站可能存在重定向情况)
chrome_options = Options()
chrome_options.add_argument('--headless') # 使用无界面浏览器
driver = webdriver.Remote(
command_executor=current_app.config.get("COMMAND_EXECUTOR"),
desired_capabilities=DesiredCapabilities.CHROME
)
driver.get(target_url)
new_target_url = driver.current_url
最近在看一些底层的东西。
driver翻译过来是驱动,司机的意思。如果将webdriver比做成司机,竟然非常恰当。
我们可以把WebDriver驱动浏览器类比成出租车司机开出租车。
在开出租车时有三个角色:
乘客:他/她告诉出租车司机去哪里,大概怎么走
出租车司机:他按照乘客的要求来操控出租车
出租车:出租车按照司机的操控完成真正的行驶,把乘客送到目的地
在WebDriver中也有类似的三个角色:
自动化测试代码:自动化测试代码发送请求给浏览器的驱动(比如火狐驱动、谷歌驱动)
浏览器的驱动:它来解析这些自动化测试的代码,解析后把它们发送给浏览器
浏览器:执行浏览器驱动发来的指令,并最终完成工程师想要的操作。
所以在这个类比中:
工程师写的自动化测试代码就相当于是乘客
浏览器的驱动就相当于是出租车司机
浏览器就相当于是出租车
面再从技术上解释下WebDriver的工作原理:
从技术上讲,也同样是上面的三个角色:
WebDriver API(基于Java、Python、C#等语言)
对于java语言来说,就是下载下来的selenium的Jar包,比如selenium-java-3.8.1.zip包,代表Selenium3.8.1的版本
浏览器的驱动(browser driver)
每个浏览器都有自己的驱动,均以exe文件形式存在
比如谷歌的chromedriver.exe、火狐的geckodriver.exe、IE的IEDriverServer.exe
浏览器
浏览器当然就是我们很熟悉的常用的各种浏览器。
那在WebDriver脚本运行的时候,它们之间是如何通信的呢?为什么同一个browser driver即可以处理java语言的脚本,也可以处理python语言的脚本呢?让我们来看一下,一条Selenium脚本执行时后端都发生了哪些事情:
对于每一条Selenium脚本,一个http请求会被创建并且发送给浏览器的驱动
浏览器驱动中包含了一个HTTP Server,用来接收这些http请求
HTTP Server接收到请求后根据请求来具体操控对应的浏览器
浏览器执行具体的测试步骤
浏览器将步骤执行结果返回给HTTP Server
HTTP Server又将结果返回给Selenium的脚本,如果是错误的http代码我们就会在控制台看到对应的报错信息。
为什么使用HTTP协议呢?
因为HTTP协议是一个浏览器和Web服务器之间通信的标准协议,而几乎每一种编程语言都提供了丰富的http libraries,这样就可以方便的处理客户端Client和服务器Server之间的请求request及响应response,WebDriver的结构中就是典型的C/S结构,WebDriver API相当于是客户端,而小小的浏览器驱动才是服务器端。
WebDriver基于的协议:JSON Wire protocol。
JSON Wire protocol是在http协议基础上,对http请求及响应的body部分的数据的进一步规范。
我们知道在HTTP请求及响应中常常包括以下几个部分:http请求方法、http请求及响应内容body、http响应状态码等。
常见的http请求方法:
GET:用来从服务器获取信息。比如获取网页的标题信息
POST:向服务器发送操作请求。比如findElement,Click等
http响应状态码:
在WebDriver中为了给用户以更明确的反馈信息,提供了更细化的http响应状态码,比如:
7:NoSuchElement
11:ElementNotVisible
200:Everything OK
现在到了最关键的http请求及响应的body部分了:
body部分主要传送具体的数据,在WebDriver中这些数据都是以JSON的形式存在并进行传送的,这就是JSON Wire protocol。
Selenium 是将各个浏览器的API封装成" Selenium自己设计定义的协议,名字叫做The WebDriver Wire Protocol " 的webdriver API
操作层面:
1、测试人员编写UI自动化测试脚本(java,python等等),运行脚本后,程序会打开指定的webdriver浏览器
webdriver浏览器作为一个remote-server 接受脚本的命令,同时webservice会打开一个端口:http://localhost:9515 浏览器则会监听这个端口
2、webservice会将脚本语言翻译成json格式传递给浏览器执行操作命令
逻辑层面:
1、测试人员执行测试脚本后,就创建了一个session, 通过http 请求向webservice发送了restfull的请求。
2、webservice翻译restfull的请求为浏览器能懂的脚本,然后接受脚本执行结果。
3、webservice将结果进行封装--json 给到客户端client/测试脚本 ,然后client就知道操作是否成功,同时测试也可以进行校验了。
我们可以验证一下:
下载好chromedriver,放到环境变量里,注意要和chrome浏览器版本对上,然后执行chromedriver
可以看到,会启动一个server, 并开启端口9515:
andersons-iMac:~ anderson$ chromedriver
Starting ChromeDriver 2.39.562713 (dd642283e958a93ebf6891600db055f1f1b4f3b2) on port 9515
Only local connections are allowed.
GVA info: Successfully connected to the Intel plugin, offline Gen9
强调了只允许本地连接。
前面已经提过了,乘客向司机发一个请求,
行为是构造一个http请求
构造的请求是这样子的:
请求方式 :POST
请求地址 :http://localhost:9515/session
请求body :
capabilities = {
"capabilities": {
"alwaysMatch": {
"browserName": "chrome"
},
"firstMatch": [
{}
]
},
"desiredCapabilities": {
"platform": "ANY",
"browserName": "chrome",
"version": "",
"chromeOptions": {
"args": [],
"extensions": []
}
}
}
我们可以尝试使用python requests 向 ChromeDriver发送请求
import requests
import json
session_url = 'http://localhost:9515/session'
session_pars = {"capabilities": {"firstMatch": [{}], \
"alwaysMatch": {"browserName": "chrome",\
"platformName": "any", \
"goog:chromeOptions": {"extensions": [], "args": []}}}, \
"desiredCapabilities": {"browserName": "chrome", \
"version": "", "platform": "ANY", "goog:chromeOptions": {"extensions": [], "args": []}}}
r_session = requests.post(session_url,json=session_pars)
print(json.dumps(r_session.json(),indent=2))
结果:
{
"sessionId": "44fdb7b1b048a76c0f625545b0d2567b",
"status": 0,
"value": {
"acceptInsecureCerts": false,
"acceptSslCerts": false,
"applicationCacheEnabled": false,
"browserConnectionEnabled": false,
"browserName": "chrome",
"chrome": {
"chromedriverVersion": "2.40.565386 (45a059dc425e08165f9a10324bd1380cc13ca363)",
"userDataDir": "/var/folders/yd/dmwmz84x5rj354qkz9rwwzbc0000gn/T/.org.chromium.Chromium.RzlABs"
},
"cssSelectorsEnabled": true,
"databaseEnabled": false,
"handlesAlerts": true,
"hasTouchScreen": false,
"javascriptEnabled": true,
"locationContextEnabled": true,
"mobileEmulationEnabled": false,
"nativeEvents": true,
"networkConnectionEnabled": false,
"pageLoadStrategy": "normal",
"platform": "Mac OS X",
"rotatable": false,
"setWindowRect": true,
"takesHeapSnapshot": true,
"takesScreenshot": true,
"unexpectedAlertBehaviour": "",
"version": "71.0.3578.80",
"webStorageEnabled": true
}
}
如何打开一个网页,类似driver.get(url)
那么构造的请求是:
请求方式 :POST
请求地址 :http://localhost:9515/session/:sessionId/url
注意:上述地址中的 ":sessionId"
要用启动浏览器的请求返回结果中的sessionId的值
例如:我刚刚发送请求,启动浏览器,返回结果中"sessionId": "44fdb7b1b048a76c0f625545b0d2567b"
然后请求的URL地址
请求地址:http://localhost:9515/session/b2801b5dc58b15e76d0d3295b04d295c/url
请求body :{"url": "https://www.baidu.com", "sessionId": "44fdb7b1b048a76c0f625545b0d2567b"}
即:
import requests
url = 'http://localhost:9515/session/44fdb7b1b048a76c0f625545b0d2567b/url'
pars = {"url": "https://www.baidu.com", "sessionId": "44fdb7b1b048a76c0f625545b0d2567b"}
r = requests.post(url,json=pars)
print(r.json())
如何定位元素,类似driver.finde_element_by_xx:
请求方式 :POST
请求地址 :http://localhost:9515/session/:sessionId/element
注意:上述地址中的 ":sessionId"
要用启动浏览器的请求返回结果中的sessionId的值
例如:我刚刚发送请求,启动浏览器,返回结果中"sessionId": "b2801b5dc58b15e76d0d3295b04d295c"
然后我构造 查找页面元素的请求地址
请求地址:http://localhost:9515/session/b2801b5dc58b15e76d0d3295b04d295c/element
请求body :{"using": "css selector", "value": ".postTitle a", "sessionId": "b2801b5dc58b15e76d0d3295b04d295c"}
即:
import requests
url = 'http://localhost:9515/session/b2801b5dc58b15e76d0d3295b04d295c/element'
pars = {"using": "css selector", "value": ".postTitle a", "sessionId": "b2801b5dc58b15e76d0d3295b04d295c"}
r = requests.post(url,json=pars)
print(r.json())
如何操作元素:类似click()
请求方式 :POST
请求地址 :http://localhost:9515/session/:sessionId/element/:id/click
注意:上述地址中的 ":sessionId"
要用启动浏览器的请求返回结果中的sessionId的值
:id 要用元素定位请求后返回ELEMENT的值
例如:我刚刚发送请求,启动浏览器,返回结果中"sessionId": "b2801b5dc58b15e76d0d3295b04d295c"
元素定位,返回ELEMENT的值"0.11402119390850629-1"
然后我构造 点击页面元素的请求地址
请求地址:http://localhost:9515/session/b2801b5dc58b15e76d0d3295b04d295c/element/0.11402119390850629-1/click
请求body :{"id": "0.11402119390850629-1", "sessionId": "b2801b5dc58b15e76d0d3295b04d295c"}
即:
import requests
url = 'http://localhost:9515/session/b2801b5dc58b15e76d0d3295b04d295c/element/0.11402119390850629-1/click'
pars ={"id": "0.5930642995574296-1", "sessionId": "b2801b5dc58b15e76d0d3295b04d295c"}
r = requests.post(url,json=pars)
print(r.json())
从上面可以看出来,UI自动化,其实也可以写成API自动化。
只是,只是
好繁琐,没有封装好的wedriver指令好用,有点脱裤子放屁的感觉。
我们来写段代码感觉一下:
import requests
import time
capabilities = {
"capabilities": {
"alwaysMatch": {
"browserName": "chrome"
},
"firstMatch": [
{}
]
},
"desiredCapabilities": {
"platform": "ANY",
"browserName": "chrome",
"version": "",
"chromeOptions": {
"args": [],
"extensions": []
}
}
}
# 打开浏览器 http://127.0.0.1:9515/session
res = requests.post('http://127.0.0.1:9515/session', json=capabilities).json()
session_id = res['sessionId']
# 打开百度
requests.post('http://127.0.0.1:9515/session/%s/url' % session_id,
json={"url": "http://www.baidu.com", "sessionId": session_id})
time.sleep(3)
# 关闭浏览器,删除session
requests.delete('http://127.0.0.1:9515/session/%s' % session_id, json={"sessionId": session_id})
其实搞懂真正的原理,也就是为了方便解决问题,在debug的时候,更方便的查看和解决问题。
当然,如果在接口自动化里面也需要调用少量的UI自动化,可以考虑这种方式。