python自動化測試工具selenium使用指南

悠悠i發表於2022-04-27

概述

selenium是網頁應用中最流行的自動化測試工具,可以用來做自動化測試或者瀏覽器爬蟲等。官網地址為:https://www.selenium.dev/。相對於另外一款web自動化測試工具QTP來說有如下優點:

  • 免費開源輕量級,不同語言只需要一個體積很小的依賴包
  • 支援多系統環境,包括Windows,Mac,Linux
  • 支援多種瀏覽器,包括Chrome,FireFox,IE,safari,opera等
  • 支援多語言,包括Java,C,python,c#等主流語言
  • 支援分散式測試用例執行

python+selenium環境安裝

首先需要安裝python(推薦3.7+)環境,然後直接用pip install selenium安裝依賴包即可。

另外還需要下載瀏覽器相應的webdriver驅動程式,注意下載的驅動版本一定要匹配瀏覽器版本

下載以後可以把驅動程式加到環境變數,這樣使用時就不用手動指定驅動程式路徑。

使用selenium啟動瀏覽器

可以在python中使用下面的程式碼啟動一個Chrome瀏覽器,然後控制這個瀏覽器的行為或者讀取資料。

from selenium import webdriver

# 啟動Chrome瀏覽器,要求chromedriver驅動程式已經配置到環境變數
# 將驅動程式和當前指令碼放在同一個資料夾也可以
driver = webdriver.Chrome()

# 手動指定驅動程式路徑
driver = webdriver.Chrome(r'D:/uusama/tools/chromedriver.exe')

driver = webdriver.Ie()        # Internet Explorer瀏覽器
driver = webdriver.Edge()      # Edge瀏覽器
driver = webdriver.Opera()     # Opera瀏覽器
driver = webdriver.PhantomJS()   # PhantomJS

driver.get('http://uusama.com')  # 開啟指定路徑的頁面

啟動的時候還可以設定啟動引數,比如下面的程式碼實現啟動時新增代理,並且忽略https證照校驗。

from selenium import webdriver

# 建立chrome啟動選項物件
options = webdriver.ChromeOptions()


options.add_argument("--proxy-server=127.0.0.1:16666")  # 設定代理
options.add_argument("---ignore-certificate-errors")  # 設定忽略https證照校驗
options.add_experimental_option("excludeSwitches", ["enable-logging"])  # 啟用日誌

# 設定瀏覽器下載檔案時儲存的預設路徑
prefs = {"download.default_directory": get_download_dir()}
options.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=options)

一些非常有用的啟動選項,下面使用的options = webdriver.ChromeOptions():

  • options.add_argument("--proxy-server=127.0.0.1:16666"): 設定代理,可以結合mitmproxy進行抓包等
  • option.add_experimental_option('excludeSwitches', ['enable-automation']): 設定繞過selenium檢測
  • options.add_argument("---ignore-certificate-errors"): 設定忽略https證照校驗
  • options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2}): 設定不請求圖片模式加快頁面載入速度
  • chrome_options.add_argument('--headless'): 設定無頭瀏覽器

selenium頁面載入等待和檢測

使用selenium開啟頁面以後,還不能立刻操作,需要等到待處理頁面元素載入完成,這時就需要檢測和等待頁面載入完成。

使用time.sleep()等待

最簡單的方法就是開啟頁面以後,使用time.sleep()強制等待一定時間,該方法只能設定一個固定時間等待,如果頁面提前載入完成,則會空等阻塞。

from time import sleep
from selenium import webdriver

driver = webdriver.Chrome()
driver.get('http://uusama.con')
time.sleep(10)
print('load finish')

使用implicitly_wait設定最長等待時間

另外還可以使用implicitly_wait設定最長等待時間,如果在給定時間內頁面載入完成或者已經超時,才會執行下一步。該方法會等到所有資源全部載入完成,也就是瀏覽器標籤欄的loading小圈不再轉才會執行下一步。有可能頁面元素已經載入完成,但是js或者圖片等資源還未載入完成,此時還需要等待。

另需注意使用implicitly_wait只需設定一次,並且對整個driver生命週期都起作用,凡是遇到頁面正在載入都會阻塞。

示例如下:

from selenium import webdriver

driver = webdriver.Chrome()
driver.implicitly_wait(30)   # 設定最長等30秒
driver.get('http://uusama.com')
print(driver.current_url)

driver.get('http://baidu.com')
print(driver.current_url)

使用WebDriverWait設定等待條件

使用WebDriverWait(selenium.webdriver.support.wait.WebDriverWait)能夠更加精確靈活地設定等待時間,WebDriverWait可在設定時間內每隔一段時間檢測、是否滿足某個條件,如果滿足條件則進行下一步操作,如果超過設定時間還不滿足,則丟擲異常TimeoutException,其方法宣告如下:

WebDriverWait(driver, timeout, poll_frequency=0.5, ignored_exceptions=None)

其中各引數含義如下

  • driver:瀏覽器驅動
  • timeout:最長超時時間,預設以秒為單位
  • poll_frequency:檢測的間隔(步長)時間,預設為0.5秒
  • ignored_exceptions:忽略的異常,即使在呼叫until()until_not()的過程中丟擲給定異常也不中斷

WebDriverWait()一般配合until()until_not()方法使用,表示等待阻塞直到返回值為True或者False,需要注意這兩個方法的引數都需是可呼叫物件,即方法名稱,可以使用expected_conditions模組中的方法或者自己封裝的方法。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions

driver = webdriver.Chrome()
driver.get("http://baidu.com")

# 判斷id為`input`的元素是否被加到了dom樹裡,並不代表該元素一定可見,如果定位到就返回WebElement
element = WebDriverWait(driver, 5, 0.5).until(expected_conditions.presence_of_element_located((By.ID, "s_btn_wr")))

# implicitly_wait和WebDriverWait都設定時,取二者中最大的等待時間
driver.implicitly_wait(5)

# 判斷某個元素是否被新增到了dom裡並且可見,可見代表元素可顯示且寬和高都大於0
WebDriverWait(driver,10).until(EC.visibility_of_element_located((By.ID, 'su')))

# 判斷元素是否可見,如果可見就返回這個元素
WebDriverWait(driver,10).until(EC.visibility_of(driver.find_element(by=By.ID, value='kw')))

下面列出expected_conditions常用的一些方法:

  • title_is: 判斷當前頁面title是否精確等於預期
  • title_contains: 判斷當前頁面title是否包含預期字串
  • presence_of_element_located: 判斷某個元素是否被加到了dom樹裡,並不代表該元素一定可見
  • visibility_of_element_located: 判斷某個元素是否可見(元素非隱藏,並且元素的寬和高都不等於0)
  • visibility_of: 跟上面的方法做一樣的事情,只是上面的方法要傳入locator,這個方法直接傳定位到的element就好了
  • presence_of_all_elements_located: 判斷是否至少有1個元素存在於dom樹中。舉個例子,如果頁面上有n個元素的class都是'column-md-3',那麼只要有1個元素存在,這個方法就返回True
  • text_to_be_present_in_element: 判斷某個元素中的text是否包含了預期的字串
  • text_to_be_present_in_element_value: 判斷某個元素中的value屬性是否包含了預期的字串
  • frame_to_be_available_and_switch_to_it: 判斷該frame是否可以switch進去,如果可以的話,返回True並且switch進去,否則返回False
  • invisibility_of_element_located: 判斷某個元素中是否不存在於dom樹或不可見
  • element_to_be_clickable: 判斷某個元素中是否可見並且是enable的,這樣的話才叫clickable
  • staleness_of: 等某個元素從dom樹中移除,注意,這個方法也是返回True或False
  • element_to_be_selected: 判斷某個元素是否被選中了,一般用在下拉選單
  • element_selection_state_to_be: 判斷某個元素的選中狀態是否符合預期
  • element_located_selection_state_to_be: 跟上面的方法作用一樣,只是上面的方法傳入定位到的element,而這個方法傳入locator

檢測document是否載入完成

另外還可以使用driver.execute_script('return document.readyState;') == 'complete'來檢測document是否載入完成。

注意document載入完成,是不包括那種非同步載入ajax請求動態渲染的dom的,這種需要使用上面的方法檢測某個元素是否渲染完成。

selenium元素定位和讀取

查詢元素

selenium提供了一系列api方便獲取chrome中的元素,這些API都返回WebElement物件或其列表,如:

  • find_element_by_id(id): 查詢匹配id的第一個元素
  • find_element_by_class_name(): 查詢匹配class的第一個元素
  • find_elements_by_xpath(): 查詢匹配xpath的所有元素
  • find_elements_by_css_selector(): 查詢匹配css選擇器的所有元素

其實可以看WebDriver類裡面的實現原始碼,其核心實現都是呼叫兩個基本函式:

  • find_element(self, by=By.ID, value=None): 查詢匹配策略的第一個元素
  • find_elements(self, by=By.ID, value=None): 查詢匹配策略的所有元素

其中by引數可以是ID, CSS_SELECTOR, CLASS_NAME, XPATH等。下面舉幾個簡單的例子:

  • 通過xpath查詢包含文字登入的第一個元素: find_element_by_xpath("//*[contains(text(),'登入')]")
  • 查詢包含類名refresh的所有元素: find_elements_by_class_name('refresh')
  • 查詢table表格的第二行: find_element_by_css_selector('table tbody > tr:nth(2)')

dom元素互動

上面介紹的元素查詢方法基本返回WebElement物件或者該物件的列表,該物件常用的有如下api:

  • element.text: 返回元素的文字內容(包括後臺節點所有內容),注意如果元素display=none則返回為空字串
  • element.screenshot_as_png: 元素截圖
  • element.send_keys("input"): 元素輸入框輸入input字串
  • element.get_attribute('data-v'): 獲取data-v名稱屬性值,除了自定義節點屬性,還可以獲取如textContent等屬性
  • element.is_displayed(): 元素是否使用者可見
  • element.clear(): 清除元素文字
  • element.click(): 點選元素,如果元素不可點選會丟擲ElementNotInteractableException異常
  • element.submit(): 模擬表單提交

查詢元素失敗處理

如果找不到指定元素,則會丟擲NoSuchElementException異常,而且需要注意,即使是display=none的元素也會獲取到,凡是在dom節點中的元素都可以獲取到。

而且實際使用的時候要注意一些js程式碼動態建立的元素,可能需要輪詢獲取或者監控。

一個檢查是否存在指定元素的方法如下:

def check_element_exists(xpath):
    try:
        driver.find_element_by_xpath(xpath)
    except NoSuchElementException:
        return False
    return True

selenium互動控制

ActionChains動作鏈

webdriver通過ActionChains物件來模擬使用者操作,該物件表示一個動作鏈路佇列,所有操作會依次進入佇列並不會立即執行,需要呼叫perform()方法時才會執行。其常用方法如下:

  • click(on_element=None): 單擊滑鼠左鍵
  • click_and_hold(on_element=None): 點選滑鼠左鍵,不鬆開
  • context_click(on_element=None): 點選滑鼠右鍵
  • double_click(on_element=None): 雙擊滑鼠左鍵
  • send_keys(*keys_to_send): 傳送某個鍵到當前焦點的元素
  • send_keys_to_element(element, *keys_to_send): 傳送某個鍵到指定元素
  • key_down(value, element=None): 按下某個鍵盤上的鍵
  • key_up(value, element=None): 鬆開某個鍵
  • drag_and_drop(source, target): 拖拽到某個元素然後鬆開
  • drag_and_drop_by_offset(source, xoffset, yoffset): 拖拽到某個座標然後鬆開
  • move_by_offset(xoffset, yoffset): 滑鼠從當前位置移動到某個座標
  • move_to_element(to_element): 滑鼠移動到某個元素
  • move_to_element_with_offset(to_element, xoffset, yoffset): 移動到距某個元素(左上角座標)多少距離的位置
  • perform(): 執行鏈中的所有動作
  • release(on_element=None): 在某個元素位置鬆開滑鼠左鍵

模擬滑鼠事件

下面程式碼模擬滑鼠移動,點選,拖拽等操作,注意操作時需要等待一定時間,否則頁面還來不及渲染。

from time import sleep
from selenium import webdriver
# 引入 ActionChains 類
from selenium.webdriver.common.action_chains import ActionChains

driver = webdriver.Chrome()
driver.get("https://www.baidu.cn")
action_chains = ActionChains(driver)

target = driver.find_element_by_link_text("搜尋")
# 移動滑鼠到指定元素然後點選
action_chains.move_to_element(target).click(target).perform()
time.sleep(2)

# 也可以直接呼叫元素的點選方法
target.click()
time.sleep(2)

# 滑鼠移動到(10, 50)座標處
action_chains.move_by_offset(10, 50).perform()
time.sleep(2)

# 滑鼠移動到距離元素target(10, 50)處
action_chains.move_to_element_with_offset(target, 10, 50).perform()
time.sleep(2)

# 滑鼠拖拽,將一個元素拖動到另一個元素
dragger = driver.find_element_by_id('dragger')
action.drag_and_drop(dragger, target).perform()
time.sleep(2)

# 也可以使用點選 -> 移動來實現拖拽
action.click_and_hold(dragger).release(target).perform()
time.sleep(2)
action.click_and_hold(dragger).move_to_element(target).release().perform()

模擬鍵盤輸入事件

通過send_keys模擬鍵盤事件,常用有:

  • send_keys(Keys.BACK_SPACE): 刪除鍵(BackSpace)
  • send_keys(Keys.SPACE): 空格鍵(Space)
  • send_keys(Keys.TAB): 製表鍵(Tab)
  • send_keys(Keys.ESCAPE): 回退鍵(Esc)
  • send_keys(Keys.ENTER): Enter鍵(Enter)
  • send_keys(Keys.F1): 鍵盤 F1
  • send_keys(Keys.CONTROL,'a'): 全選(Ctrl+A)
  • send_keys(Keys.CONTROL,'c'): 複製(Ctrl+C)
  • send_keys(Keys.CONTROL,'x'): 剪下(Ctrl+X)
  • send_keys(Keys.CONTROL,'v'): 貼上(Ctrl+V)

示例:定位到輸入框,然後輸入內容

# 輸入框輸入內容
driver.find_element_by_id("kw").send_keys("seleniumm")

# 刪除多輸入的一個 m
driver.find_element_by_id("kw").send_keys(Keys.BACK_SPACE)

警告框處理

用於處理呼叫alert彈出的對話方塊。

  • driver.switch_to_alert(): 切換到對話方塊
  • text:返回 alert/confirm/prompt 中的文字資訊
  • accept():接受現有警告框
  • dismiss():關閉現有警告框
  • send_keys(keysToSend):將文字傳送至警告框

selenium瀏覽器控制

基本常用api

下面列出一些非常實用的瀏覽器控制api:

  • driver.current_url: 獲取當前活動視窗的url
  • driver.switch_to_window("windowName"): 移動到指定的標籤視窗
  • driver.switch_to_frame("frameName"): 移動到指定名稱的iframe
  • driver.switch_to_default_content(): 移動到預設文字內容區
  • driver.maximize_window(): 將瀏覽器最大化顯示
  • driver.set_window_size(480, 800): 設定瀏覽器寬480、高800顯示
  • driver.forword(), driver.back(): 瀏覽器前進和後退
  • driver.refresh(): 重新整理頁面
  • driver.close(): 關閉當前標籤頁
  • driver.quiit(): 關閉整個瀏覽器
  • driver.save_screenshot('screen.png'): 儲存頁面截圖
  • driver.maximize_window(): 將瀏覽器最大化顯示
  • browser.execute_script('return document.readyState;'): 執行js指令碼

selenium讀取和載入cookie

使用get_cookiesadd_cookie可以實現將cookie快取到本地,然後啟動時載入,這樣可以保留登入態。實現如下

import os
import json
from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://www.baidu.cn")

# 讀取所有cookie並儲存到檔案
cookies = driver.get_cookies()
cookie_save_path = 'cookie.json'
with open(cookie_save_path, 'w', encoding='utf-8') as file_handle:
    json.dump(cookies, file_handle, ensure_ascii=False, indent=4)

# 從檔案讀取cookie並載入到瀏覽器
with open(cookie_save_path, 'r', encoding='utf-8') as file_handle:
    cookies = json.load(file_handle)
    for cookie in cookies:
        driver.add_cookie(cookie)

selenium開啟新的標籤頁視窗

使用driver.get(url)會預設在第一個標籤視窗開啟指定連線,點選頁面中的_blank的連結時也會開啟一個新的標籤視窗。

還可以用下面的方式手動開啟一個指定頁面的標籤視窗,需要注意開啟新視窗或者關閉以後,還需要手動呼叫switch_to.window切換當前活動的標籤視窗,否則會丟擲NoSuchWindowException異常。

from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://www.baidu.cn")

new_tab_url = 'http://uusama.com'
driver.execute_script(f'window.open("{new_tab_url}", "_blank");')
time.sleep(1)

# 注意:必須呼叫switch_to.window手動切換window,否則會找不到tab view
# 聚焦到新開啟的tab頁面,然後關閉
driver.switch_to.window(driver.window_handles[1])
time.sleep(2)
driver.close()   # 關閉當前視窗

# 手動回到原來的tab頁面
driver.switch_to.window(driver.window_handles[0])
time.sleep(1)

除了使用execute_script外,還可以使用模擬開啟新tab頁按鍵的方式新建一個標籤頁視窗:

  • driver.find_element_by_tag_name('body').send_keys(Keys.CONTROL + 't')
  • ActionChains(driver).key_down(Keys.CONTROL).send_keys('t').key_up(Keys.CONTROL).perform()

selenium一些問題記錄

獲取隱藏元素的文字內容

如果一個元素是隱藏的,即display=none,雖然可以通過find_element查詢到該元素,但是用element.text屬性是獲取不到該元素的文字內容的,起值是空字串,這時可以用下面的方式獲取:

element = driver.find_element_by_id('uusama')
driver.execute_script("return arguments[0].textContent", element)
driver.execute_script("return arguments[0].innerHTML", element)

# 相應的也可以把隱藏的元素設定為非隱藏
driver.execute_script("arguments[0].style.display = 'block';", element)

瀏覽器崩潰WebDriverException異常處理

比如在Chrome中長時間執行一個頁面會出現Out Of Memory記憶體不足的錯誤,此時WebDriver會丟擲WebDriverException異常,基本所有api都會丟擲這個異常,這個時候需要捕獲並進行特殊處理。

我的處理方式是記錄頁面的一些基本資訊,比如url,cookie等,然後定期寫入到檔案中,如果檢測到該異常,則重啟瀏覽器並且載入url和cookie等資料。

selenium抓取頁面請求資料

網上有通過driver.requests或者通過解析日誌來獲取頁面請求的方式,但是我感覺都不是很好使。最後使用mitmproxy代理進行抓包處理,然後啟動selenium時填入代理來實現。

proxy.py為在mitmproxy基礎上封裝的自定義代理請求處理,其程式碼如下:

import os
import gzip
from mitmproxy.options import Options
from mitmproxy.proxy.config import ProxyConfig
from mitmproxy.proxy.server import ProxyServer
from mitmproxy.tools.dump import DumpMaster
from mitmproxy.http import HTTPFlow
from mitmproxy.websocket import WebSocketFlow


class ProxyMaster(DumpMaster):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def run(self, func=None):
        try:
            DumpMaster.run(self, func)
        except KeyboardInterrupt:
            self.shutdown()


def process(url: str, request_body: str, response_content: str):
    # 抓包請求處理,可以在這兒轉存和解析資料
    pass


class Addon(object):
    def websocket_message(self, flow: WebSocketFlow):
        # 監聽websockt請求
        pass

    def response(self, flow: HTTPFlow):
        # 避免一直儲存flow流,導致記憶體佔用飆升
        # flow.request.headers["Connection"] = "close"
        # 監聽http請求響應,並獲取請求體和響應內容
        url = flow.request.url
        request_body = flow.request
        response_content = flow.response

        # 如果返回值是壓縮的內容需要進行解壓縮
        if response_content.data.content.startswith(b'\x1f\x8b\x08'):
            response_content = gzip.decompress(response_content.data.content).decode('utf-8')
        Addon.EXECUTOR.submit(process, url, request_body, response_content)


def run_proxy_server():
    options = Options(listen_host='0.0.0.0', listen_port=16666)
    config = ProxyConfig(options)
    master = ProxyMaster(options, with_termlog=False, with_dumper=False)
    master.server = ProxyServer(config)
    master.addons.add(Addon())
    master.run()


if __name__ == '__main__':
    with open('proxy.pid', mode='w') as fin:
        fin.write(os.getpid().__str__())
    run_proxy_server()

在使用mitmproxy過程中,隨著時間推移proxy.py會出現佔用記憶體飆升的問題,在github的issue區有人也遇到過,有說是因為http連線keep-alive=true請求會一直儲存不會釋放,導致請求越多越佔用記憶體,然後通過新增flow.request.headers["Connection"] = "close"來手動關閉連線,我加了以後有一定緩解,但還是不能從根本上解決。

最後通過寫入proxy.pid記錄代理程式程式,然後用另外一個程式定時重啟proxy.py來解決記憶體洩漏的問題。

相關文章