使用 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自動化,可以考慮這種方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM