跳转至

Selenium

pip install selenium

核心组成及原理

  • Selenium IDE 浏览器插件,用于录制简单的脚本
  • Selenium Webdriver 底层依赖
  • Selenium Grid 支持远程和分布式运行测试用例

20240721124736

Webdriver

Selenium 和 Appium 等大部分框架都是基于 W3C Webdriver

WebDriver 采用 C/S 架构的通信模式,客户端(测试脚本)通过发送命令到 Server 端

Server 通过基于 HTTP 的 JSON Wire Protocol 与 ChromeDriver、GeckoDriver 等驱动通信,继而操控浏览器

缺点就是速度较慢,但优点就是支持多语言,多种浏览器,Server 端可以分布式部署

浏览器驱动

ChromeDriver 实现了 WebDriver 协议,负责解释 WebDriver 命令,并将其转化为浏览器能理解的操作,通过 CDP 在进程外控制 Chrome。

手动管理

第一步:下载与浏览器版本相近的驱动

  • chromedriver
  • geckodriver
  • safaridriver 无需下载,已内置在 usr/bin/ 中,But You must enable the 'Allow Remote Automation' option in Safari's Develop menu to control Safari via WebDriver.

第二步:将驱动放入 PATH 环境变量

Mac

  1. mv chromedriver /usr/local/bin,或者打开 Finder,使用快捷键 Command+Shift+G,前往文件夹 /usr/local/bin,将下载的驱动文件拖入。
  2. 打开终端,执行 vim ~/.bash_profile,添加环境变量 export PATH=$PATH:/usr/local/bin/chromedriver,保存后执行 source ~/.bash_profile 生效。(如果不是自定义路径,这步可省略)
  3. 执行 chromedriver --version,如果返回版本号则说明配置正确,如果提示无法验证开发者,需要到 系统设置 隐私与安全性 界面准许权限。

Windows

  1. 下载的驱动文件可以集中放在一个文件夹内,比如 selenium-driver
  2. 然后将该文件路径添加到 环境变量\系统变量\Path
  3. 打开终端,执行 chromedriver --version,如果返回版本号则说明配置正确

自动管理

可依靠第三方驱动程序管理器自动下载维护浏览器驱动,比如 https://pypi.org/project/webdriver-manager/

pip install webdriver-manager

from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))

从 v4.11 版本开始,官方提供了 Selenium Manager 来自动发现、下载并缓存这些驱动程序

启动

Chrome

from selenium import webdriver

driver = webdriver.Chrome()
# 如果没有配置环境变量,需要指定路径
# driver = webdriver.Chrome(executable_path='Xxx/chromedriver')

# url必须带 scheme://
driver.get("http://selenium.dev")
driver.quit()

debug 模式启动 Chrome

通过 debug 模式可以让 Selenium 连接到手动打开的浏览器窗口,而不是每次启动一个新的的窗口,通常用于调试定位,而不需要依赖其它页面的操作

首先需要关闭所有Chrome浏览器窗口,并保证进程已退出,命令行以调试模式启动 Chrome

  • Mac: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222(空格需要用\转义)
  • Windows: .\chrome --remote-debugging-port=9222(先切换到 chrome.exe 所在目录)

访问 http://127.0.0.1:9222,如果不报错,而是空白页面,这说明调试模式启动成功

手动打开想要调试的页面

然后脚本中即可使用 debug 模式启动,便可连接到已经打开的页面,进行调试

chrome_options = webdriver.ChromeOptions()
chrome_options.debugger_address = "localhost:9222"
# 或者
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "localhost:9222")  # add_experimental_option 方法用于添加一些不是常规Chrome选项的实验性或非标准选项

# 设置参数
driver = webdriver.Chrome(options=chrome_options)

其它浏览器

  • Safari

需要先在 Safari浏览器-偏好设置-高级,勾选 在菜单中显示开发,然后在 开发者设置 中,勾选 允许远程自动化

driver = webdriver.Safari()
  • FireFox
driver = webdriver.Firefox()
  • PhantomJS
driver = webdriver.PhantomJS()

Browser Options

通用选项

  • 页面加载策略
from selenium import webdriver
options = webdriver.ChromeOptions()
# 或
from selenium.webdriver.chrome.options import Options
options = Options()

options.page_load_strategy = 'eager'
driver = webdriver.Chrome(options=options)
"""
normal 默认值,等待所有资源下载后才开始执行自动化操作
eager DOM加载完即开始,不等待图像、css、js等资源加载完成,通常这些需要较长时间,但对自动化并不是很重要
none 仅等待初始页面加载,不会阻塞 WebDriver
"""

ChromeOptions

options = webdriver.ChromeOptions()

options.xxx

driver = webdriver.Chrome(options=options)
  • options.add_argument

https://www.selenium.dev/documentation/webdriver/browsers/chrome/#arguments

# 以最大化方式启动浏览器
options.add_argument("--start-maximized")
# 指定浏览器存储用户配置文件的目录,会话将不再是隐身模式
options.add_argument("--user-data-dir=...")
# 以无头模式运行
options.add_argument("--headless=new")
  • options.add_experimental_option
# 只要不发送退出命令,进程结束后保持打开状态,即不自动关闭窗口
options.add_experimental_option("detach", True)

# 去除提示:Chrome正在受到自动软件的控制
options.add_experimental_option('useAutomationExtension', False)
options.add_experimental_option('excludeSwitches', ['enable-automation'])

# 关闭:是否保存密码弹框
prefs = {
    "credentials_enable_service": False, 
    "profile.password_manager_enabled": False
}
options.add_experimental_option("prefs", prefs)

Driver Service Class

https://www.selenium.dev/documentation/webdriver/drivers/service/

# 日志输出到文件
service = webdriver.ChromeService(log_output=log_path)
driver = webdriver.Chrome(service=service)

操控浏览器

  • 属性
print(driver.name)  #浏览器名称
print(driver.curent_url)  # 当前url
print(driver.title)  # 当前页面标题
print(driver.page_source)  # 当前页面源码
print(driver.window_handles)  # 当前窗口所有句柄
print(driver.current_window_handle)  # 当前窗口句柄
  • 方法
driver.maximize_window()  # 窗口最大化

driver.get_cookies()  # 获取cookies

driver.forward()  # 前进
driver.back()  # 后退

driver.refresh()  # 刷新页面

driver.close()  # 关闭页面
driver.quit()  # 退出浏览器
driver.switch_to.frame(element)  # 切换到frame
driver.switch_to.default_content()  # 返回主frame
driver.switch_to.parent_frame()  # 返回父frame

driver.switch_to.window(driver.window_handles[1])  # 切换标签页

driver.switch_to.active_element  # 切换到活动元素
  • 执行js
# 执行js脚本
driver.execute_script("js_cmd")  # 同步
driver.execute_async_script("js_cmd")  # 异步
# 命令中使用return才能得到返回值
js_cmd = "return JSON.stringify(performance.timing)"
loc = driver.execute_script(js_cmd)
# 执行多条命令
loc = driver.execute_script("return js_code1; js_code2")
# 或者
for code in ["return js_code1", "return js_code2"]:
    print(driver.execute_script(code))
# 通常想要控制的元素带有readonly属性,可以用js移除readonly属性
code = 'a = document.getElementById("train_date"); a.removeAttribute("readonly"); a.value="2021-06-29"'
driver.execute_script(code)

Locators

Selenium v4.x 移除了 driver.find_element_by_xxx 这种写法

Traditional Locators

建议优先选择顺序:ID > CSS Selector > Xpath

from selenium import webdriver
from selenium.webdriver.common.by import By

"""
<label for="fname"> First name </label>
<input class="testclass" type="text" id="testid" name="testname" value="testvalue">
<a href ="www.selenium.dev">Selenium Official Page</a>
"""

# ID
driver.find_element(By.ID, "testid")

# Name
driver.find_element(By.NAME, "testname")

# Class
driver.find_element(By.CLASS_NAME, "testclass")  # 不支持复合类

# Tag
driver.find_element(By.TAG_NAME, "input")

# 超链接文本
driver.find_element(By.LINK_TEXT, "Selenium Official Page")  # 精准匹配
driver.find_element(By.PARTIAL_LINK_TEXT, "Selenium")  # 模糊匹配

# CSS Selector
driver.find_element(By.CSS_SELECTOR, "#testid")

# XPath
driver.find_element(By.XPATH, "//input[@value='testvalue']")

Relative Locators

Selenium 4 引入了 RelativeLocator,可连用

  • above:定位某个元素上方的元素
  • below:定位某个元素下方的元素
  • to_left_of:定位某个元素左侧的元素
  • to_right_of:定位某个元素右侧的元素
  • near:定位某个元素附近的元素(默认为50像素内)
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with

driver = webdriver.Chrome()
driver.get('您的网页URL')

# 假设我们可以直接定位到一个元素
known_element = driver.find_element(By.ID, 'knownElementId')
# 定位到已知元素下方tag_name是input的元素
locator = locate_with(By.TAG_NAME, 'input').below(known_element)
element_below = driver.find_element(locator)

# 对找到的元素进行操作,例如点击
element_below.click()

# ... 进行你的其他操作 ...

# 最后,关闭浏览器
driver.quit()

Script Locators

  • DOM
dom = 'document.'
driver.execute_script(js)
  • JQuery
jq_css = '$("#id")'
jq_xpath = '$x("#id")'
driver.execute_script(jquery)

定位技巧

如果有多个相同的元素,使用 find_element 查找时只会返回第一个,使用 find_elements 会以列表的形式全部返回,如果没有匹配项,则返回空列表。

  • 尽量使用 CSS 或 XPATH 选择器替代嵌套查找

  • 查找子元素

先定位到父元素,基于父元素定位其子元素

# 示例1
fruits = driver.find_element(By.ID, "fruits")
fruit = fruits.find_element(By.CLASS_NAME,"tomatoes")

# 示例2
element = driver.find_element(By.TAG_NAME, 'div')
elements = element.find_elements(By.TAG_NAME, 'p')
  • 可以直接移动到活动的元素 active_element
driver.find_element(By.CSS_SELECTOR, '[name="q"]').send_keys("webElement")
attr = driver.switch_to.active_element.get_attribute("title")

与元素交互

https://www.selenium.dev/documentation/webdriver/elements/interactions

send_keys

  • 输入文本内容

需要是 text 类型并具有 content-editable 属性

element.send_keys("xxx")
  • 输入快捷键
element.send_keys(Keys.CONTROL, "v")
  • 上传文件

如果一个 input 元素是 type 类型,可以使用 send_keys 方法上传文件(需要使用完整路径)

import os
current_dir = os.path.dirname(__file__)
file_path = os.path.join(current_dir, 'images', 'demo.png')

file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
file_input.send_keys(file_path)

clear

清空输入内容

需要是 text 类型并具有 content-editable 属性

element.clear()

clear

单击,适用于任何元素

element.click()

获取元素信息

e.id  # 标示
e.size  # 宽高
e.rect  # 宽高和坐标
e.text  # 文本内容
e.tag_name  # 标签名

e.is_selected  # 是否被选中
e.is_enabled  # 是否可用
e.is_displayed  # 是否显示
e.value_of_css_property  # css属性值

e.get_attribute("属性") # 获取属性值
e.get_attribute("class")  # 获取类
e.get_attribute("value")  # 获取输入值

弹框的处理

弹框主要有三种,处理方式基本一致

  • alert
  • confirm
  • prompt
# 先定位到弹框
# 然后切换到alert
a = driver.switch_to.alert

a.accept()  # 确认
a.dismiss()  # 取消
a.send_keys("xxx")  # 输入内容
a.text  # 显示弹框的文本

下拉框的处理

from selenium import webdriver
# 需要引入Select类
from selenium.webdriver.support.select import Select

se = self.driver.find_element_by_id('s4Id')
select = Select(se)
# 通过索引勾选
select.select_by_index(3)

for i in range(7):
    select.select_by_index(i)

# 通过选项value值勾选
select.select_by_value('o4val')
# 通过选项文本勾选
select.select_by_visible_text('o1')

# 取消勾选
select.deselect_by_index(3)
select.deselect_by_value('o4val')
select.deselect_by_visible_text('o1')
# 取消所有已选选项
select.deselect_all()

# 返回所有选项
for option in select.options:
    print(option.text)

# 返回所有被选中的选项
for option in select.all_selected_options:
    print(option.text)

# 返回被选中的第一个选项
print(select.first_selected_option.text)

等待的处理

隐性等待和显性等待同时存在时,取两者中最长的等待时间为有效等待时间。

强制等待

强制线程休眠一定时间,一般只在调试过程中使用

import time

time.sleep(5)

隐性等待

在指定时间内,自动每隔0.5s查找一次元素,找到则退出循环,没找到则继续,超过指定的时常则抛出异常。

需要注意的是,当定位到元素后与 .click() 连用时,隐性等待会失效

driver.implicitly_wait(5)

显性等待

from selenium.webdriver.support.ui import WebDriverWait

# driver
# timeout 超时时间
# poll_frequency 检测频率,默认为0.5s
# ignored_exceptions 超时后抛出异常,默认为NoSuchElementException
wait = WebDriverWait(self.driver, timeout, poll_frequency=0.5, ignored_exceptions=None)

# 满足预期条件时跳出等待向下执行,超时则抛出TimeoutException`、异常
wait.until(method, message='')
wait.until_not(method, message='')

其中的method可以自定义也可以使用EC模块提供的函数

预期条件·EC

貌似必须得配合显性等待使用

from selenium.webdriver.support import expected_conditions as EC

最常用的三个

# 判断某个元素是否被加到了dom里,但不一定可见
EC.presence_of_element_located(locator)

# 判断某个元素是否可见,宽和高等大于0
EC.visibility_of_element_located(locator)

# 判断某个元素中是否可点击
EC.element_to_be_clickable(locator)

其它

# 判断页面上是否存在弹框
EC.alert_is_present()

# 判断该frame是否可以switch进去,如果可以的话,返回true并且switch进去,否则返回false
EC.frame_to_be_available_and_switch_to_it()
# 判断title是否为xxx
EC.title_is("xxx")
# 判断title是否包含xxx
EC.title_contains("xxx")

# 判断某元素的文本是否包含xxx
EC.text_to_be_present_in_element(locator, xxx)
# 判断某元素的属性值是否包含xxx
EC.text_to_be_present_in_element_value(locator, xxx)
# 判断某个元素中是否存在于dom或不可见
EC.invisibility_of_element_located(locator)
# 判断等待某个元素从dom树中是否移除
EC.staleness_of()

# 判断是否至少有1个元素存在于dom中
EC.presence_of_all_elements_located(locator)
# 判断是否至少有一个元素可见
EC.visibility_of_any_elements_located(locator)
# 判断某个元素是否被选中了,一般用在下拉列表中
EC.element_to_be_selected(element)
# 判断某个元素的选中状态是否符合预期
EC.element_selection_state_to_be(element)
# 判断某个元素的选中状态是否符合预期
EC.element_located_selection_state_to_be(locator, is_selected)

ActionChains

调用ActionsChains方法时,不会立即执行,而是将所有操作按顺序存放在一个队列中,当调用perform()方法时,队列中的事件会依次执行。

from selenium.webdriver import ActionChains

action = ActionChains(driver)  # 生成一个动作
action.添加方法1
action.添加方法2
action.perform()  # 执行

鼠标操作

from selenium.webdriver import ActionChains

ActionChains(self.driver).func1().func2().perform()  # 按顺序执行func1和func2

.reset_actions()  # 清除已存储的actions
.pause(seconds)  # 保持n秒
.click()  # 单击左键
.context_click()  # 单击右键
.double_click()  # 双击左键

.click_and_hold()  # 单击左键,不松开
.release()  # 在某个元素上松开按键

.drag_and_drop(source, target)  # 拖拽到某个元素后松开
.drag_and_drop_by_offset(source, xoffset, yoffset)  # 拖拽到某个坐标后松开

.move_to_element(to_element)  # 鼠标移到某个元素hover
.move_by_offset(xoffset, yoffset)  # 鼠标移到某个坐标hover
.move_to_element_with_offset(to_element, xoffset, yoffset)  # 鼠标移动到距某个元素一定距离的位置hover

按键操作

# Actions类中的sendKeys
from selenium.webdriver.common.keys import Keys

element = driver.find_element_by_xxx
element.click()

# 注意:执行完perform后面如果再想执行其它actions,需要重新实例化actions
actions = ActionChains(self.driver)

actions.send_keys(Keys.SPACE)
actions.send_keys_to_element(element, Keys.xxx)

actions.key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL)

actions.perform()

还可以

# WebElement.sendKeys
e.send_keys(Keys.CONTROL, "a")

截图

driver.save_screenshot('xxx.png')  # 保存截图
driver.get_screenshot_as_file(file_path)  # 获取当前截图的完整路径
driver.get_screenshot_as_base64()  # 获取当前截图的base64字符串
driver.get_screenshot_as_png()  # 获取当前截图的二进制数据

异常

from selenium.common.exceptions import TimeoutException