Python自动化测试工具selenium使用指南

概述

selenium是网页应用中最流行的自动化测试工具,可以用来做自动化测试或者浏览器爬虫等。官网地址为:相对于另外一款web自动化测试工具QTP来说有如下优点:

  • 免费开源轻量级,不同语言只需要一个体积很小的依赖包
  • 支持多种系统,包括Windows,Mac,Linux
  • 支持多种浏览器,包括Chrome,FireFox,IE,safari,opera等
  • 支持多语言,包括Java,C,python,c#等主流语言
  • 支持分布式测试用例执行
python+selenium环境安装

首先需要安装python(推荐3.7+)环境,然后直接用pip install selenium安装依赖包即可。

另外还需要下载浏览器相应的webdriver驱动程序,注意下载的驱动版本一定要匹配浏览器版本

  • Firefox浏览器驱动:
  • Chrome浏览器驱动:
  • IE浏览器驱动:
  • Edge浏览器驱动:
  • Opera浏览器驱动:

下载以后可以把驱动程序加到环境变量,这样使用时就不用手动指定驱动程序路径。

使用selenium启动浏览器

可以在python中使用下面的代码启动一个Chrome浏览器,然后控制这个浏览器的行为或者读取数据。

  1. from selenium import webdriver

  2. # 启动Chrome浏览器,要求chromedriver驱动程序已经配置到环境变量

  3. # 将驱动程序和当前脚本放在同一个文件夹也可以

  4. driver = webdriver.Chrome()

  5. # 手动指定驱动程序路径

  6. driver = webdriver.Chrome(r'D:/uusama/tools/chromedriver.exe')

  7. driver = webdriver.Ie() # Internet Explorer浏览器

  8. driver = webdriver.Edge() # Edge浏览器

  9. driver = webdriver.Opera() # Opera浏览器

  10. driver = webdriver.PhantomJS() # PhantomJS

  11. driver.get('http://uusama.com') # 打开指定路径的页面

启动的时候还可以设置启动参数,比如下面的代码实现启动时添加代理,并且忽略https证书校验。

  1. from selenium import webdriver

  2. # 创建chrome启动选项对象

  3. options = webdriver.ChromeOptions()

  4. options.add_argument("--proxy-server=127.0.0.1:16666") # 设置代理

  5. options.add_argument("---ignore-certificate-errors") # 设置忽略https证书校验

  6. options.add_experimental_option("excludeSwitches", ["enable-logging"]) # 启用日志

  7. # 设置浏览器下载文件时保存的默认路径

  8. prefs = {"download.default_directory": get_download_dir()}

  9. options.add_experimental_option("prefs", prefs)

  10. driver = webdriver.Chrome(options=options)

启动的时候还可以设置启动参数,比如下面的代码实现启动时添加代理,并且忽略https证书校验。

  1. from selenium import webdriver

  2. # 创建chrome启动选项对象

  3. options = webdriver.ChromeOptions()

  4. options.add_argument("--proxy-server=127.0.0.1:16666") # 设置代理

  5. options.add_argument("---ignore-certificate-errors") # 设置忽略https证书校验

  6. options.add_experimental_option("excludeSwitches", ["enable-logging"]) # 启用日志

  7. # 设置浏览器下载文件时保存的默认路径

  8. prefs = {"download.default_directory": get_download_dir()}

  9. options.add_experimental_option("prefs", prefs)

  10. 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()强制等待一定时间,该方法只能设置一个固定时间等待,如果页面提前加载完成,则会空等阻塞。

  1. from time import sleep

  2. from selenium import webdriver

  3. driver = webdriver.Chrome()

  4. driver.get('http://uusama.con')

  5. time.sleep(10)

  6. print('load finish')

使用implicitly_wait设置最长等待时间

另外还可以使用implicitly_wait设置最长等待时间,如果在给定时间内页面加载完成或者已经超时,才会执行下一步。该方法会等到所有资源全部加载完成,也就是浏览器标签栏的loading图表不再转才会执行下一步。有可能页面元素已经加载完成,但是js或者图片等资源还未加载完成,此时还需要等待。

另需注意使用implicitly_wait只需设置一次,且对整个driver生命周期都起作用,凡是遇到页面正在加载都会阻塞。

示例如下:

  1. from selenium import webdriver

  2. driver = webdriver.Chrome()

  3. driver.implicitly_wait(30) # 设置最长等30秒

  4. driver.get('http://uusama.com')

  5. print(driver.current_url)

  6. driver.get('http://baidu.com')

  7. 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模块中的方法或者自己封装的方法。

  1. from selenium import webdriver

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

  3. from selenium.webdriver.support.ui import WebDriverWait

  4. from selenium.webdriver.support import expected_conditions

  5. driver = webdriver.Chrome()

  6. driver.get("http://baidu.com")

  7. # 判断id为`input`的元素是否被加到了dom树里,并不代表该元素一定可见,如果定位到就返回WebElement

  8. element = WebDriverWait(driver, 5, 0.5).until(expected_conditions.presence_of_element_located((By.ID, "s_btn_wr")))

  9. # implicitly_wait和WebDriverWait都设置时,取二者中最大的等待时间

  10. driver.implicitly_wait(5)

  11. # 判断某个元素是否被添加到了dom里并且可见,可见代表元素可显示且宽和高都大于0

  12. WebDriverWait(driver,10).until(EC.visibility_of_element_located((By.ID, 'su')))

  13. # 判断元素是否可见,如果可见就返回这个元素

  14. 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的,这种需要使用WebDriverWait检测某个元素是否渲染完成。

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代码动态创建的元素,可能需要轮询获取或者监控。

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

  1. def check_element_exists(xpath):

  2. try:

  3. driver.find_element_by_xpath(xpath)

  4. except NoSuchElementException:

  5. return False

  6. 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): 在某个元素位置松开鼠标左键
模拟鼠标事件

下面代码模拟鼠标移动,点击,拖拽等操作,注意操作时需要等待一定时间,否则页面还来不及渲染。

  1. from time import sleep

  2. from selenium import webdriver

  3. # 引入 ActionChains 类

  4. from selenium.webdriver.common.action_chains import ActionChains

  5. driver = webdriver.Chrome()

  6. driver.get("https://www.baidu.cn")

  7. action_chains = ActionChains(driver)

  8. target = driver.find_element_by_link_text("搜索")

  9. # 移动鼠标到指定元素然后点击

  10. action_chains.move_to_element(target).click(target).perform()

  11. time.sleep(2)

  12. # 也可以直接调用元素的点击方法

  13. target.click()

  14. time.sleep(2)

  15. # 鼠标移动到(10, 50)坐标处

  16. action_chains.move_by_offset(10, 50).perform()

  17. time.sleep(2)

  18. # 鼠标移动到距离元素target(10, 50)处

  19. action_chains.move_to_element_with_offset(target, 10, 50).perform()

  20. time.sleep(2)

  21. # 鼠标拖拽,将一个元素拖动到另一个元素

  22. dragger = driver.find_element_by_id('dragger')

  23. action.drag_and_drop(dragger, target).perform()

  24. time.sleep(2)

  25. # 也可以使用点击 -> 移动来实现拖拽

  26. action.click_and_hold(dragger).release(target).perform()

  27. time.sleep(2)

  28. 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)
  • 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)

示例:定位到输入框,然后输入内容

  1. # 输入框输入内容

  2. driver.find_element_by_id("kw").send_keys("uusamaa")

  3. # 模拟回车删除多输入的一个字符a

  4. driver.find_element_by_id("kw").send_keys(Keys.BACK_SPACE)

警告框处理

用于处理调用alert弹出的警告框。

  • driver.switch_to_alert(): 切换到警告框
  • text:返回alert/confirm/prompt中的文字信息,比如js调用alert('failed')则会获取failed字符串
  • 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缓存到本地,然后启动时加载,这样可以保留登录态。实现如下

  1. import os

  2. import json

  3. from selenium import webdriver

  4. driver = webdriver.Chrome()

  5. driver.get("https://www.baidu.cn")

  6. # 读取所有cookie并保存到文件

  7. cookies = driver.get_cookies()

  8. cookie_save_path = 'cookie.json'

  9. with open(cookie_save_path, 'w', encoding='utf-8') as file_handle:

  10. json.dump(cookies, file_handle, ensure_ascii=False, indent=4)

  11. # 从文件读取cookie并加载到浏览器

  12. with open(cookie_save_path, 'r', encoding='utf-8') as file_handle:

  13. cookies = json.load(file_handle)

  14. for cookie in cookies:

  15. driver.add_cookie(cookie)

selenium打开新的标签页窗口

使用driver.get(url)会默认在第一个标签窗口打开指定连接,点击页面中的_blank的链接时也会打开一个新的标签窗口。

还可以用下面的方式手动打开一个指定页面的标签窗口,需要注意打开新窗口或者关闭以后,还需要手动调用switch_to.window切换当前活动的标签窗口,否则会抛出NoSuchWindowException异常。

  1. from selenium import webdriver

  2. driver = webdriver.Chrome()

  3. driver.get("https://www.baidu.cn")

  4. new_tab_url = 'http://uusama.com'

  5. driver.execute_script(f'window.open("{new_tab_url}", "_blank");')

  6. time.sleep(1)

  7. # 注意:必须调用switch_to.window手动切换window,否则会找不到tab view

  8. # 聚焦到新打开的tab页面,然后关闭

  9. driver.switch_to.window(driver.window_handles[1])

  10. time.sleep(2)

  11. driver.close() # 关闭当前窗口

  12. # 手动回到原来的tab页面

  13. driver.switch_to.window(driver.window_handles[0])

  14. 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属性是获取不到该元素文本内容的,其值是空字符串,这时可以用下面的方式获取:

  1. element = driver.find_element_by_id('uusama')

  2. driver.execute_script("return arguments[0].textContent", element)

  3. driver.execute_script("return arguments[0].innerHTML", element)

  4. # 相应的也可以把隐藏的元素设置为非隐藏

  5. 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基础上封装的自定义代理请求处理,其代码如下:

  1. import os

  2. import gzip

  3. from mitmproxy.options import Options

  4. from mitmproxy.proxy.config import ProxyConfig

  5. from mitmproxy.proxy.server import ProxyServer

  6. from mitmproxy.tools.dump import DumpMaster

  7. from mitmproxy.http import HTTPFlow

  8. from mitmproxy.websocket import WebSocketFlow

  9. class ProxyMaster(DumpMaster):

  10. def __init__(self, *args, **kwargs):

  11. super().__init__(*args, **kwargs)

  12. def run(self, func=None):

  13. try:

  14. DumpMaster.run(self, func)

  15. except KeyboardInterrupt:

  16. self.shutdown()

  17. def process(url: str, request_body: str, response_content: str):

  18. # 抓包请求处理,可以在这儿转存和解析数据

  19. pass

  20. class Addon(object):

  21. def websocket_message(self, flow: WebSocketFlow):

  22. # 监听websockt请求

  23. pass

  24. def response(self, flow: HTTPFlow):

  25. # 避免一直保存flow流,导致内存占用飙升

  26. # flow.request.headers["Connection"] = "close"

  27. # 监听http请求响应,并获取请求体和响应内容

  28. url = flow.request.url

  29. request_body = flow.request

  30. response_content = flow.response

  31. # 如果返回值是压缩的内容需要进行解压缩

  32. if response_content.data.content.startswith(b'\x1f\x8b\x08'):

  33. response_content = gzip.decompress(response_content.data.content).decode('utf-8')

  34. Addon.EXECUTOR.submit(process, url, request_body, response_content)

  35. def run_proxy_server():

  36. options = Options(listen_host='0.0.0.0', listen_port=16666)

  37. config = ProxyConfig(options)

  38. master = ProxyMaster(options, with_termlog=False, with_dumper=False)

  39. master.server = ProxyServer(config)

  40. master.addons.add(Addon())

  41. master.run()

  42. if __name__ == '__main__':

  43. with open('proxy.pid', mode='w') as fin:

  44. fin.write(os.getpid().__str__())

  45. run_proxy_server()

在使用mitmproxy过程中,随着时间推移proxy.py会出现占用内存飙升的问题,在github的issue区有人也遇到过,有说是因为http连接keep-alive=true请求会一直保存不会释放,导致请求越多越占用内存,然后通过添加flow.request.headers["Connection"] = "close"来手动关闭连接,我加了以后有一定缓解,但还是不能从根本上解决。

最后通过写入proxy.pid记录代理程序进程,然后用另外一个程序定时重启proxy.py来解决内存泄漏的问题。

相关推荐
运维老司机3 分钟前
Jenkins修改LOGO
运维·自动化·jenkins
傻啦嘿哟14 分钟前
如何使用 Python 开发一个简单的文本数据转换为 Excel 工具
开发语言·python·excel
D-海漠19 分钟前
基础自动化系统的特点
运维·自动化
B站计算机毕业设计超人20 分钟前
计算机毕业设计SparkStreaming+Kafka旅游推荐系统 旅游景点客流量预测 旅游可视化 旅游大数据 Hive数据仓库 机器学习 深度学习
大数据·数据仓库·hadoop·python·kafka·课程设计·数据可视化
IT古董1 小时前
【人工智能】Python在机器学习与人工智能中的应用
开发语言·人工智能·python·机器学习
湫ccc1 小时前
《Python基础》之pip换国内镜像源
开发语言·python·pip
hakesashou1 小时前
Python中常用的函数介绍
java·网络·python
菜鸟的人工智能之路1 小时前
极坐标气泡图:医学数据分析的可视化新视角
python·数据分析·健康医疗
菜鸟学Python1 小时前
Python 数据分析核心库大全!
开发语言·python·数据挖掘·数据分析
小白不太白9501 小时前
设计模式之 责任链模式
python·设计模式·责任链模式