数据分析
一、爬虫概述
1、爬虫是什么
爬虫(Web Scraper)是指一类软件程序或脚本,其基本原理是模拟人类用户在网页上的浏览行为,通过发送 HTTP 请求来获取
网页内容,并解析
网页内容、提取
所需的信息。
2、爬虫的作用
爬虫通常用于从网站上抓取数据,它们批量化
地、自动
地、浏览互联网并收集特定网页既有的
信息。然后将这些数据用于各种目的,比如数据分析
、搜索引擎索引、内容聚合等
一般来说,爬虫是为了`获取数据
此外,爬虫还可以用于批量注册账号、批量领取优惠券、批量下单购买商品、自动签到等
3、爬虫的应用场景
在企业中,爬虫可以用于:做竞品调研数据采集、自动化地从公司地后台获取数据进行处理以实现办公自动等
对个人来说,爬虫可以用于:从带有广告的小说网站中抓取小说到本地,以不被广告所影响等
从盈利角度来说,爬虫可以用于:抢茅台、抢高铁票、论坛平台的自动评论自动点赞、开心农场定时收菜等
4、爬虫的分类
① 聚焦爬虫
:完成某一项特定数据的采集
② 通用爬虫
:什么内容都采集(eg. 百度、谷歌)
③ 增量爬虫
:既可以是聚焦爬虫,也可以是通用爬虫,特点是当内容发送变化的时候,可以增量地
获取内容,(eg. 抓取一条微博下的评论,第一次抓取了全部,过了一段时间后该微博又多了一些评论,于是进行二次爬虫时不会抓取所有的评论,而是只抓取新的那些评论)
④ 暗网爬虫
:也叫深网爬虫
,这种很少见,因为暗网/深网是一个很阔怕很阔怕的见不得光的网站(自行了解,最好不要了解 orz )
5、如何使用爬虫进行EARN MONEY
① 就业:找一份爬虫工程师岗位
② 抢购:抢购茅台、抢购各种电扇平台的优惠券、抢购机票高铁票演唱会门票
③ 付费内容:在小说平台充值,然后通过爬虫讲付费的、火爆的小说抓取下来,搭建一个自己的网站进行展示(还可以在自己的网站上接广子)
④ 引流比价:在电商平台抓取同一个商品关于不同商家、不同日期的价格,以直观地看到价格波动和比价
⑤ 点赞,收藏,刷粉丝,刷评论刷播放量
二、Selenium
1、概述
Selenium 是一个用于自动化网页浏览器操作
的工具,它提供了一系列的API,允许开发人员使用多种编程语言来控制浏览器
(如Python、Java、C#等),模拟用户在网页上的操作,如点击链接、填写表单、提交数据等
Selenium最初是为了Web应用程序测试
而创建的,但它也可以用于网页抓取
、自动化任务
和数据挖掘
等多种用途
Selenium支持多种浏览器,包括Chrome、Firefox、Safari等,并提供了各种浏览器驱动程序,用于与不同浏览器进行通信和控制
2、配置
①下载chromdriver 官方 淘宝镜像 :chromedriver是Chrome浏览器的一个独立程序,用作Chrome浏览器与Selenium WebDriver之间的中介。它充当了一个桥梁,使得Selenium WebDriver可以与Chrome浏览器进行通信和控制
Tip:chromdriver的版本要与Chrome浏览器的版本对应。Chrom浏览器老版本下载
目前chromdriver的版本最新是114.0.5735.90,那么chrome的浏览器需要下载104开头的版本,例如104.0.5112.102。
建议关闭chrom浏览器的自动更新 教程
②下载完成后在Pycharm中构建如下文件结构
根目录
├── venv/ # 存放虚拟环境的目录(配置virtual的时候自动生成的)
│ ├── Lib/
│ └── Script/
├── file/ # 存放文件的文件夹(手动创建,后续测试文件上传用)
│ └── img1.jpg
├── chromedriver.exe # chromedriver(将下载的chromedriver手动拉到这里)
├── chear.py # 清除后台的chromdriver.exe(手动创建,往下翻有代码实现)
└── test.py # 测试(手动创建,执行脚本的地方)
③ 安装selenium依赖
pip install selenium
④test.py文件编写如下
from selenium import webdriver
import time
driver = webdriver.Chrome() # 创建了一个Chrome浏览器的WebDriver实例
url = 'https://05x-docs.pyecharts.org/#/zh-cn/prepare?id=%e4%bd%bf%e7%94%a8%e4%b8%bb%e9%a2%98'
driver.get(url) # 自动打开chrome浏览器,并显示该页面
time.sleep(5)
driver.quit() # 关闭浏览器
⑤运行结果:弹出一个新的chrome浏览器
3、基础篇
说明:使用下表中的例子前,先执行
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
driver = webdriver.Chrome()
actions = ActionChains(driver)
3.1、页面属性
属性 | 描述 | 例子 |
---|---|---|
title | 网页标题 | driver.title |
current_url | 当前网址 | driver.current_url |
name | 浏览器标题 | driver.name |
page_source | 网页源码 | driver.page_source |
3.2、浏览器控制
浏览器API | 描述 | 例子 |
---|---|---|
get | 打开浏览器并访问页面 | driver.get(r’https://www.baidu.com/') |
maximize_window | 浏览器大小:全屏 | driver.maximize_window() |
set_window_size | 浏览器大小:自定义 | driver.set_window_size(500,500) |
forward | 前进 | driver.forward() |
back | 后退 | driver.back() |
close | 关闭浏览器 不关闭后台进程 | driver.close() |
quit(推荐) | 关闭浏览器 关闭后台进程 | driver.quit() |
Tip:每运行一次脚本,如果不执行quit方法的话,后台会多一个chromedriver.exe,不及时清除的话会十分占用内存,不过不执行quit方法的原因也是让我们在使用Pycharm执行脚本时能够看到中间过程,因此下方的代码可能不带有quit方法,如果一不小心后台有很多个chromedriver,exe的话:
① 可以打开任务管理器手动清除
②运行如下脚本,可以将其命名为clear.py
import os
import subprocess
def count():
# 使用tasklist命令查找所有chromedriver.exe进程
result = subprocess.run('tasklist | findstr chromedriver.exe', stdout=subprocess.PIPE, shell=True)
# 获取输出结果并按行分割
output_lines = result.stdout.decode().split('\n')
# 计算chromedriver.exe进程的数量
num_chromedriver_processes = len(output_lines) - 1 # 最后一行是空行,所以减去1
print("后台共有 {} 个 chromedriver.exe 进程运行".format(num_chromedriver_processes))
return num_chromedriver_processes
if __name__ == '__main__':
if count()>0:
try:
os.system('taskkill /im chromedriver.exe /F')
print("清理\033[92m完成\033[00m") # 92m表示绿色,00m表示恢复默认颜色
except:
print("清理\033[91m失败\033[00m") # 91m表示红色,00m表示恢复默认颜色
else:
print("不需要进行清理")
3.3、元素定位:find_element( by , value ) / find_elements( by , value )
参数 by (根据) | 描述 | 例子 |
---|---|---|
class_name | 类名 | driver.find_element(by=’class_name‘, value=‘abc’) |
xpath | 表达式 | driver.find_element(‘xpath’, ‘//input[@id=“username”]’) |
link_text | 超链接文本 | driver.find_element(‘link_text’, ‘click here’) |
partial_link_text | 链接文本的部分内容 | element = driver.find_element(‘partial_link_text’, ‘here’) |
tag_name | 标签名 | driver.find_element(‘tag name’, ‘input’) |
id | ID | driver.find_element(‘id’, ‘username’) |
css selector | 元素的属性、层次结构、关系等 | driver.find_element(‘css selector’, ‘input#username’) |
Tip:find_element(’id‘,‘username’)等同于driver.find_element_by_id(‘username’),其他同理
3.4、模拟鼠标操作
鼠标API | 描述 | 例子 |
---|---|---|
click | 单击 | actions.click(elem) |
context_click | 右击 | actions.context_click(elem) |
double_click | 双击 | actions.double_click(elem) |
drag_and_drop | 拖拽 | actions.drag_and_drop(elem1, elem2) |
move_to_element | 悬停 | actions.move_to_element(elem) |
perform | 执行 | actions.perform() |
Tip:进行单机、右击等鼠标操作的最后,需要再执行actions.perform()才能生效,actions.click(elem);actions.perform()等同于actions.click(elem).perform()
3.5、模拟键盘操作:send_keys(key)
参数 key (按键) | 描述 | 例子 |
---|---|---|
Keys.BACK_SPACE | 删除键 | elem.send_keys(Keys.BACK_SPACE) |
Keys.SPACE | 空格键 | elem.send_keys(Keys.SPACE) |
Keys.TAB | 制表键 | elem.send_keys(Keys.TAB) |
Keys.ESCAPE | 回退键 | elem.send_keys(Keys.ESCAPE) |
Keys.ENTER | 回车键 | elem.send_keys(Keys.ENTER) |
Keys.CONTROL | Ctrl | elem.send_keys(Keys.CONTROL,‘a’) – 全选 |
Keys.F1 | F1 | elem.send_keys(Keys.F1) |
条件判断API | 描述 | 例子 |
---|---|---|
title_is | 网页标题是否为 | EC.title_is(“百度一下,你就知道”) |
title_contains | 网页标题是否包含 | EC.title_contains(“百度”) |
visibility_of | 是否可见(传入DOM) | EC.visibility_of(baidu_logo) |
visibility_of_element_located | 是否可见(传入元组) | EC.visibility_of_element_located((By.ID, ‘su’)) |
invisibility_of_element_located | 是否不可见 | EC.invisibility_of_element_located((By.ID, “kw”)) |
presence_of_element_located | 页面中是否至少存在一个元素 | EC.presence_of_element_located((By.ID, ‘kw’)) |
presence_of_all_elements_located | 页面中是否所有元素都加载完成 | EC.presence_of_all_elements_located((By.CSS_SELECTOR, ’div‘)) |
text_to_be_present_in_element | 元素的text是否包含 | EC.text_to_be_present_in_element((By.ID, “su”), “百度一下”) |
text_to_be_present_in_element_value | 元素的value是否包含 | EC.text_to_be_present_in_element_value((By.ID, “su”), “百度一下”) |
frame_to_be_available_and_switch_to_it | fram是否可切入(传入元组 / DOM) | EC.frame_to_be_available_and_switch_to_it((By.ID, “frame_id”)) |
alert_is_present | 是否有alert出现 | EC.alert_is_present() |
element_to_be_clickable | 是否可点击 | EC.element_to_be_clickable((By.ID, “kw”)) |
element_to_be_selected | 是否被选中(传入DOM) | EC.element_to_be_selected(element) |
element_located_to_be_selected | 是否被选中(传入元组) | EC.element_located_to_be_selected((By.ID, “element_id”)) |
element_selection_state_to_be | 选中状态是否为(传入DOM) | EC.element_selection_state_to_be(element, True) |
element_located_selection_state_to_be | 选中状态是否为(传入元组) | EC.element_located_selection_state_to_be((By.ID, “element_id”), True) |
staleness_of | 元素是否仍在DOM树中 | EC.staleness_of(element) |
find_element | 判断元素是否存在 | driver.find_element(by=By.XPATH,value=xpath) |
3.6、模拟键盘操作:延时等待
类型 | 描述 | 例子 |
---|---|---|
强制等待 | 程序会完全停止执行,直到等待时间结束 | time.sleep(4) |
隐式等待 | 是一种全局性的等待设置 它会在操作任何元素时自动等待一定的时间,超时的话会抛出异常 | driver.implicitly_wait(4) |
显式等待 | 是一种灵活的等待设置 它会在操作特定元素时自动等待一定的时间,超时的话会抛出异常 | wait = WebDriverWait(driver, 10) button = wait.until(EC.element_to_be_clickable((By.ID, ‘button_id’))) |
WebDriverWait( driver , timeout , poll_frequency=0.5 , ignored_exceptions=None )参数说明:
driver: 浏览器驱动
timeout: 超时时间,等待的最长时间(同时要考虑隐性等待时间)
poll_frequency: 每次检测的间隔时间,默认是0.5秒
ignored_exceptions:超时后的异常信息,默认情况下抛出NoSuchElementException异常
WebDriverWait.until(method,message)参数说明:
method: 在等待期间,每隔一段poll_frequency时间就调用此方法,直到method的返回值不是False
message: 如果超时,抛出TimeoutException,将message传入异常
WebDriverWait.until_not(method,message=‘’)说明:
与until相反,until是当某元素出现或什么条件成立则继续执行,until_not是当某元素消失或什么条件不成立则继续执行,参数也相同
3.7、窗口、表单切换
① 窗口切换:switch_to.windows( handle ):从一个标签页(窗口)跳转到另一个标签页
import time
from selenium import webdriver
driver = webdriver.Chrome() # 创建Chrome浏览器的WebDriver实例
driver.get("http://www.example.com") # 打开第一个页面
first_window = driver.current_window_handle # 获取第一个页面的句柄
driver.execute_script("window.open('https://www.baidu.com', 'newwindow')") # 打开一个新的窗口
second_window = driver.current_window_handle # 获取第二个页面的句柄
driver.switch_to.window(second_window) # 切换到新打开的窗口
print(driver.title) # 在新窗口中进行操作
time.sleep(2) # 停留两秒
driver.switch_to.window(first_window) # 切换回第一个窗口
print(driver.title) # 在第一个窗口中进行操作
句柄(Handle):在Windows系统中,句柄通常是一个整数值,表示一个对象或资源的唯一标识符。当打开一个文件或者创建一个进程时,操作系统会为它分配一个句柄,并将句柄和对应的资源地址记录在句柄表(Handle Table)中。当你想要访问这个资源时,操作系统会根据句柄在句柄表中查找对应的资源地址,然后将资源地址返回给你的程序
② 表单切换 switch_to.frame( id / name ) /EC.frame_to_be_available_and_switch_to_it( 元组 ):从主文档切换到切换到指定的iframe(内嵌框架)
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 as EC
driver = webdriver.Chrome()# 创建Chrome浏览器的WebDriver实例
driver.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')# 打开网页
WebDriverWait(driver, 10).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframeResult")))# 等待iframe加载完成
# 在iframe中操作
try:
# 在iframe中查找元素
draggable = driver.find_element(By.ID, "draggable")
droppable = driver.find_element(By.ID, "droppable")
# 使用ActionChains拖拽元素
actions = webdriver.ActionChains(driver)
actions.drag_and_drop(draggable, droppable).perform()
print("成功拖拽元素")
except Exception as e:
print("发生异常:", e)
# 处理意外的警告框
try:
alert = driver.switch_to.alert
alert.accept()
except:
pass
print(driver.title)#打印ifram中页面的title
driver.switch_to.default_content()# 切换回主文档
print(driver.title)#打印主文档的title
总结:
switch_to.window(handle):切换到指定句柄(窗口)的窗口
switch_to.frame(frame_reference):切换到指定frame的内部
switch_to.default_content():从iframe切换回默认的主文档
switch_to.alert():切换到弹出的警告框(alert)
3.8、cookie操作
导入依赖:from selenium import webdriver
API | 描述 |
---|---|
get_cookies() | 返回所有cookie的信息 |
get_cookie(name_str) | 返回name字段为name_str的cookie信息 |
add_cookie(cookie_dict) | 添加cookie |
delete_cookie(name_str) | 删除name字段为name_str的cookie |
delete_all_cookies() | 删除所有cookie信息 |
例子: 知乎发现页的cookie操作
from selenium import webdriver
# 打印每个cookie的name、value字段
def printCookieItem(msg, cookies):
print(msg)
cookies_simple = [{'name': cookie['name'], 'value': cookie['value']} for cookie in cookies]
for item in cookies_simple:
print(item)
browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore') # 知乎发现页
fir_cookies = browser.get_cookies() # 获取cookie
printCookieItem('初始cookie:', fir_cookies)
browser.add_cookie({'name': 'HYH', 'value': '贺一航'}) # 添加cookie
sec_cookies = browser.get_cookies()
printCookieItem('添加cookie后:', sec_cookies)
browser.delete_cookie('HYH') # 删除特定cookie
thi_cookies = browser.get_cookies()
printCookieItem('删除特定cookie后:', thi_cookies)
browser.delete_all_cookies() # 删除所有cookie
forth_cookies = browser.get_cookies()
printCookieItem('删除所有cookie后:', forth_cookies)
4、进阶篇 —— execute_script
4.1、查找元素
from selenium import webdriver
driver = webdriver.Chrome()# 创建Chrome浏览器的WebDriver实例
driver.get("https://www.baidu.com")# 打开百度首页
# 使用execute_script方法查找搜索框
search_box = driver.execute_script("return document.evaluate('//input[@id=\"kw\"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue")
# 如果找到搜索框,则输出其属性值
if search_box:
print("找到了百度首页的搜索框,id属性值为:", search_box.get_attribute("id"))
else:
print("未找到百度首页的搜索框")
说明:
① document.evaluate()这是 JavaScript 中的一个方法,用于执行 XPath 查询。它接受五个参数:XPath 表达式、上下文节点(在这里是 document,即整个文档)、命名空间解析器(在这里设置为 null,表示不使用命名空间)、结果类型(在这里设置为 XPathResult.FIRST_ORDERED_NODE_TYPE,表示返回第一个匹配节点)、和结果(在这里设置为 null,表示将结果存储在默认的 result 属性中),evaluate()返回的结果是一个XPathResult 对象
② singleNodeValue是取出XPathResult 对象里的DOM元素
4.2、将页面滚动到底部
from selenium import webdriver
browser = webdriver.Chrome() # 创建Chrome浏览器的WebDriver实例
browser.get("https://www.baidu.com/s?wd=test") # 打开百度搜索结果页面
temp_height = 0 # 初始化临时高度变量
scroll_distance = 100 # 设置每次滚动的距离
while True:
# 执行JavaScript将滚动条向下滚动指定距离
js = "var q=document.documentElement.scrollTop={}".format(temp_height)
browser.execute_script(js)
# 更新滚动条高度
scroll_height = browser.execute_script("return document.documentElement.scrollTop;")
# 获取页面总高度
total_height = browser.execute_script("return document.body.scrollHeight")
# 前浏览器窗口的内部高度(即视口高度)
innerHeight = browser.execute_script("return window.innerHeight;")
print(scroll_height, total_height, innerHeight)
# 如果滚动条高度已经达到页面底部,退出循环
if scroll_height + innerHeight >= total_height:
break
# 更新临时高度变量
temp_height += scroll_distance
5、进阶篇 —— css selector
css selector定位器是一种通过css样式来定位元素(标签)的方法,因此需要了解一些常用的css选择器表达式
css选择器 | 格式 | 示例 | 示例说明 |
---|---|---|---|
标签选择器 | 标签 | input | 所有的<input>元素 |
ID选择器 | #id属性值 | #kw | 所有的id='kw’的元素 |
类选择器 | .class属性值 | .nav-search-input | 所有的class='nav-search-input '的元素 |
属性选择器 | [属性名] | [name=“wd”] [href*=‘baidu’] [href^=‘https://image.baidu’] [href$=‘.com’] | 所有的name=‘wd’的元素 所有的href包含baidu的元素 所有的href以https://image.baidu作为开头的元素 所有的href以’.com’作为结尾的元素 |
组合选择器 | 标签加属性描述 | input#kw input.s_iput | 所有的id='kw’的input元素 所有的class='s_input’的input元素 |
父子关系 | 元素1>元素2 | div>a | 所有直接父级是<div>的<a>元素 |
后代关系 | 元素1 元素2 | div a | <div>中所有的<a>元素 |
第一个元素 | :first-child | a:first-child | 选择所有作为其父级第一个子元素的<a>标签 |
最后一个元素 | :last-child | a:last-child | 选择所有作为其父级最后一个子元素的<a>标签 |
顺序选择器 | :nth-child(n) | a:nth-child(2) | 选择所有作为其父级第二个子元素的<a>标签 |
6、进阶篇 —— XPATH
XPath(XML Path Language)是一种用于在 XML 文档中定位元素的语言、一种基于树结构的语言,通过路径表达式(path expression)来选择 XML 文档中的节点,它提供了一套灵活的语法规则,允许开发者根据节点的层级关系、属性值、文本内容等条件来进行节点的定位和选择
路径表达式 | 描述 | 举例 |
---|---|---|
/ | 所有直接子节点 | //div/a —— 文档中所有div标签的所有直接 a 子标签 /html/div —— 从根节点出发选取(绝对路径) |
// | 所有子节点 | //div//a —— 文档中所有div标签的所有 a 子标签中 |
. | 当前节点 | .//div —— 当前标签下的所有div标签 ./@class —— 当前标签的class属性 |
.. | 选取当前节点的父节点 | ..//div —— 当前标签的父标签下所有的div标签 |
@ | 选取属性 | //input[@id=“kw”] —— 文档中所有id为kw的input标签 |
通配符 | 描述 | 举例 |
---|---|---|
* | 所有标签 | //div/* —— 文档中所有div标签下的所有标签 |
@* | 所有属性 | //div/@* —— 文档中所有div标签下的所有标签 |
type() | 类型 | //button[text()=“登录”] —— 文档中type:text值为登录的所有按钮 |
定位 | 描述 | 举例 |
---|---|---|
contains() | 模糊定位( 包含 ) | //*[contains(text(),‘cc’)] —— 文档中包含文本cc的所有标签 |
starts-with() | 模糊定位( 以…开头 ) | //*[starts-with(@title,‘ss’)] —— 文档中title字段以ss开头的所有标签 |
ends-with() | 模糊定位( 以…结尾 ) | //*[ends-with(text(),‘ee’)] —— 文档中以ee结尾的所有标签 |
and、or、not | 逻辑定位( 与或非 ) | //div[@attribute=‘aa’ and contains(text(),‘abc’)] —— 文档中attribute为aa且text包含abc的div标签 |
following-sibling:: | 轴定位( 相邻的兄弟标签 ) | //div[@class=‘c1’]/following-sibling::div[@class=‘c2’] —— 文档中class为c1的div标签的class为c2的div兄弟标签 |
follow:: | 轴定位( 全文档中当前标签之后的标签 ) | / |
following-sibling:: | 轴定位( 同一个父标签下当前标签之后的兄弟标签 ) | //div[@class=‘c1’]/following-sibling::div[@class=‘c2’] —— 文档中class为c1的div标签的class为c2的div兄弟标签 |
preceding:: | 轴定位( 全文档中当前标签之前的标签 ) | / |
preceding-sibling:: | 轴定位( 同一个父标签下当前标签之前的兄弟标签 ) | / |
child:: | 轴定位( 当前标签的所有子标签 ) | / |
parent:: | 轴定位( 当前标签的直接父标签 ) | / |
ancestor:: | 轴定位( 当前标签的所有祖先标签 ) | / |
下标 | 描述 | 举例 |
---|---|---|
(标签)[n] | 一组标签的第n个 | (//div)[n] —— 文档中所有div标签的第n个标签 |
(标签)[last()] | 一组标签的最后一个 | (//div)[last()-n+1] —— 文档中所有div标签的倒数第n个标签 |
7、元素定位最佳顺序
① 静态ID(唯一标识):如果元素具有唯一的ID属性,优先使用ID进行定位,因为它是最快和最可靠的定位方式
② CSS选择器:如果元素没有唯一的ID,可以考虑使用CSS选择器进行定位。CSS选择器具有灵活的语法,并且在性能上通常比XPath更高效
③ 类名(class):如果元素没有唯一的ID或合适的CSS选择器,可以使用元素的类名进行定位。尽量选择具有明确含义的类名,并避免选择过于通用的类名
④ Name属性:如果元素具有唯一的name属性,可以使用name属性进行定位。但要注意,name属性并不是所有元素都具有的,所以不是一种通用的定位方式
⑤ XPath:如果前面的方式都无法定位元素,可以使用XPath进行定位。XPath提供了强大的定位功能,但在性能上相对较低,因为XPath需要遍历整个文档,直到找到匹配的元素。暂无性能对比数据,我觉得好用就用,及时行乐
⑥ 标签名(TagName):如果元素无法使用上述方式进行定位,可以考虑使用标签名进行定位。但要注意,标签名定位方式通常会返回多个匹配的元素,需要结合其他条件来缩小范围
建议:
① 定位列表的元素时,尽量结合文本信息定位而不是下标(比如百度首页的左上角,可能顺序会打乱,后台配置)
② 相对定位时,尽量选择不易改变的元素作为锚点,比如拥有id的元素或者一个目录。比如后台管理系统中的主菜单,比如H5页面的tab
8、实战训练
① 常用的实战训练网址推荐:Element View Design SahiTest
② 在Web开发者工具中验证所写的选择器、XPath表达式是否能定位到想要的元素:【打开浏览器->F12->Ctrl+F】
③ 注意点:可以通过【右键开发者工具中的标签->复制Selector/XPath】的方式快速的获取到元素Selector/XPath的表达式,表达式中可能会包含id、name等属性。但是:id、name等这些属性可能是动态的,页面一刷新同一个标签的id、name这些属性的值可能会发生变动,因此需要自己对表达式进行调整,以保证每次访问页面时,所写的表达式能够定位到想要的元素,这样就可以将表达式写进脚本中了
Tip:
id:如果标签中没有显示地id,其Selector/XPath表达式中又有的话,很可能是动态生成的 ;如果标签中有显示地给出id,很可能是静态的
name:标签中显示地给出也有可能是动态的
④ 例子
Radio测试页:按序带点击Apple、Android、Windows单选框(默认是Apple)
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://www.iviewui.com/view-ui-plus/component/form/radio")
# 1、在表达式里带下标
driver.find_element(By.XPATH,r"(//div[@id='group']//input)[1]").click()
time.sleep(2)
# 2、在List[WebElement]后带下标
driver.find_elements(By.XPATH,r"(//div[@id='group']//input)")[1].click()
time.sleep(2)
# 3、使用XPath的last()
driver.find_element(By.XPATH,r"(//div[@id='group']//input)[last()-4+1]").click()
time.sleep(2)
driver.quit()
Tip:上述代码中
① 在表达式种写下标的,下标的范围是从1开始;访问一个代码中数据结构下标的,下标的范围是从0开始
② 返回的只有一个元素的话,使用find_element;返回的有多个元素的话,使用find_elements,如果使用了find_element的话只会返回一组元素的第一个元素
CheckBox测试页:按序勾选香蕉+苹果、苹果+西瓜、香蕉+西瓜(默认是苹果)
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://www.iviewui.com/view-ui-plus/component/form/checkbox")
# 页面自己勾选了苹果,勾选香蕉就可以
driver.find_element(By.XPATH, r"(//div[@id='group']//input)[5]").click()
time.sleep(2)
# 取消香蕉的勾选,勾选西瓜
driver.find_element(By.XPATH, r"(//div[@id='group']//input)[5]").click()
driver.find_element(By.XPATH, r"(//div[@id='group']//input)[7]").click()
time.sleep(2)
# 取消苹果的勾选,勾选香蕉
driver.find_element(By.XPATH, r"(//div[@id='group']//input)[5]").click()
driver.find_element(By.XPATH, r"(//div[@id='group']//input)[6]").click()
time.sleep(2)
driver.quit()
下拉框测试页:点击Preferred Contact Mode下拉框的选项(默认是–SELECT–)
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
driver = webdriver.Chrome()
driver.get("https://sahitest.com/demo/selectTest.htm")
select = Select(driver.find_element(By.ID, 's1'))
# 根据index选择,下标从0开始
select.select_by_index(1)
time.sleep(2)
# 根据value选择
select.select_by_value("51")
time.sleep(2)
# 根据文本选择
select.select_by_visible_text("Email")
time.sleep(2)
driver.quit()
级联选择器测试页:在下拉框种选择北京/天坛选项(默认是请选择)
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://www.iviewui.com/view-ui-plus/component/form/cascader")
# 点击下拉框 虽然这个表达式能够定位到12个,但是要的input是第一个,所以可以使用find_element 下面同理
driver.find_element_by_xpath("//input[@class='ivu-input ivu-input-default']").click()
# 选择北京
driver.find_element_by_xpath("//li[contains(text(),'北京')]").click()
# 选择天坛
driver.find_element_by_xpath("//li[contains(text(),'天坛')]").click()
time.sleep(2)
driver.quit()
时间选择器测试页:选择1983/1/12(默认是Select date)
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://www.iviewui.com/view-ui-plus/component/form/date-picker")
# 点击下拉框
driver.find_element_by_xpath("//input[@placeholder='Select date']").click()
#点击年份
driver.find_element_by_xpath("//span[@class='ivu-date-picker-header-label']").click()
# 选择年份
while True:
year_element = driver.find_element_by_xpath("//span[@class='ivu-date-picker-header-label']")
year = int(year_element.text[0:4])
if year != 1984:
driver.find_element_by_xpath("//i[@class='ivu-icon ivu-icon-ios-arrow-back']").click()
continue
driver.find_element_by_xpath("//em[text()=1983]").click()
break
#选择月份
driver.find_element_by_xpath("//em[text()='1月']").click()
#选择日期
driver.find_element_by_xpath("//span[@class='ivu-date-picker-cells-cell']/em[text()='12']").click()
time.sleep(3)
driver.quit()
弹框测试页:点击Click for Alert按钮,输入,分别点击弹框的取消确定
from time import sleep
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://sahitest.com/demo/promptTest.htm")
# 输入完取消
driver.find_element_by_xpath("//input[@name='b1']").click() # 弹出弹框
print(driver.switch_to.alert.text) # 获取alert弹窗文字
driver.switch_to.alert.send_keys("hello")
sleep(2)
driver.switch_to.alert.dismiss() # 取消
# 输入完确定
driver.find_element_by_xpath("//input[@name='b1']").click()
driver.switch_to.alert.send_keys("world")
sleep(2)
driver.switch_to.alert.accept() # 确定
# 再输入
driver.find_element_by_xpath("//input[@name='b1']").click()
driver.switch_to.alert.send_keys("!")
sleep(2)
driver.switch_to.alert.accept()
sleep(2)
driver.quit()
文件上传测试页:上传一张图片
from time import sleep
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://sahitest.com/demo/php/fileUpload.htm")
# 不需要点击上传文件的按钮,直接send_key到上传文件的按钮即可 不需要点击它
upload = driver.find_element_by_xpath("//input[@id='file']")
upload.send_keys(r'D:\资料\学习\数据分析学习\file\img1.jpg')
driver.find_element_by_name("submit").click()
# 下载文件
sleep(2)
driver.quit()
文件下载测试页:下载一个文件
from time import sleep
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 as EC
#指定下载地址
chromeOptions = webdriver.ChromeOptions()
pref={"download.default_directory": "D:\资料\学习\数据分析学习\\file\\"}
chromeOptions.add_experimental_option("prefs",pref)
driver = webdriver.Chrome(chrome_options=chromeOptions)
driver.get("https://registry.npmmirror.com/binary.html?path=chromedriver/")
# 因为这个页面加载比较慢 所以需要等待一下 等待页面所有的tr都加载完成
WebDriverWait(driver,10).until(EC.presence_of_all_elements_located((By.XPATH, '/html/body/table/tbody/tr[156]/td[2]/a')))
# 下载文件
driver.find_element_by_xpath('/html/body/table/tbody/tr[156]/td[2]/a').click()
sleep(2)
driver.quit()
三、Pytest
1、概述
Pytest 是一个用于 Python 的测试框架。它提供了简单且灵活的方式来编写测试,并且能够与 Python 中的其他测试工具和框架很好地集成。
安装依赖:pip install pytest
其特点包括:
① 简单易用的语法
② 丰富的插件系统:pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等
③ 支持多种类型的测试:如单元测试、功能测试、还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests)、自动发现测试文件、丰富的断言支持等,
通过 pytest,开发者可以更加方便地编写和运行测试,以确保其代码的质量和稳定性。
2、阶段一:编写测试用例
① 创建如下文件结构:
根目录
├── venv/ # 存放虚拟环境的目录(配置virtual的时候自动生成的)
│ ├── Lib/
│ └── Script/
├── pytest.ini # 配置文件(手动创建)
└── test_one.py # 测试(手动创建)
命名规范:
测试文件名要以 test_* 或 *_test 开头
测试类名要以 Test_* 开头,并且不能有**init**函数
测试函数名要以 test_* 开头
不然测试的时候会找不到测试用例
② 编写测试用例(test_one.py)
def test_1():
assert 1 == 2
def test_2():
assert 2 == 3
说明:assert(断言)是 Python 中的一个关键字,用于在代码中检查特定条件是否为真,语法结构为" assert 表达式 ",如果 表达式 的值为真则程序继续执行;为假则会引发 AssertionError 异常,并在输出中显示相关的信息。ssert 语句通常用于在代码中插入断言(Assertion),用于检查特定的条件是否满足。它主要用于在开发和调试阶段对代码进行断言检查,以验证假设是否成立。通过合理使用 assert,开发者可以在运行时检查程序的状态,从而提前发现潜在的错误和问题。然而在生产环境中(发布后),默认情况下 Python 解释器会忽略 assert 语句,因此不应该依赖于 assert 语句来处理正常的错误情况。正式发布的代码中应该使用适当的错误处理机制来处理异常情况
常见的断言类型 | 含义 | 常见的断言类型 | 含义 |
---|---|---|---|
== | 等于 | != | 不等于 |
> | 大于 | < | 小于 |
>= | 大于等于 | <= | 小于等于 |
in | 属于 | not in | 不属于 |
is | 是 | is not | 不是 |
in、not in、is、is not的使用如下
def test():
assert "e" in "abc"
assert "e" not in "def"
assert True is True
assert False is not True
3、阶段二:测试用例的执行
② 法1:打开【文件->设置->工具->Python集成工具->测试->默认测试运行工具】,勾选pytest,勾选后可以看到符合测试文件命名规范测试文件中的测试函数前多了一个运行的按键。此时点击运行文件,则文件中的测试方法都会进行测试,如果只想测试某个函数的话,则点击测试函数前的运行按键即可
运行结果:可以看到运行的结果中会给出哪些函数测试通过、哪些函数测试不通过、测试不通过的函数哪一行出错等信息
Tip:当勾选了pytest作为测试运行工具之后,代码会以测试的方式运行,因此想要恢复到最原始的运行模式,可以打开【文件->设置->工具->Python集成工具->测试->默认测试运行工具】,勾选unitytest即可
③ 法2:在终端输入命令:pytest 要测试的文件,运行结果如下
④法3:在测试文件中调用pytest,main()
import pytest
class TestCase:
# 不能使用__init__
def test_1(self):
assert 1 == 1
# 不符合命名规范 因此不会测试到
def aa(self):
assert 2 == 3
if __name__ == '__main__':
pytest.main()
4、基础篇 – Pytest命令
命令 | 描述 | 举例 |
---|---|---|
pytest | 测试当前目录下所有测试文件的所有测试用例 | pytest |
pytest 文件 | 测试指定测试文件的所有测试用例 | pytest .\test_one.py pytest .\test_one.py .\test_two.py |
pytest 文件名 .py ::函数 | 测试指定测试文件的指定测试用例 | pytest test_one .py ::test_func |
pytest 文件名 .py ::类::函数 | 测试指定测试文件的指定测试用例 | pytest test_one .py ::TestClass::test_func |
pytest --pyargs 包 | 测试软件包(包含__init__.py的目录)中的所有测试用例 | pytest --pyargs mypkg |
pytest -x [文件] | 测试到第一处失败就停止 | pytest -x .\test_one.py |
pytest -q | 只输出测试结果中的失败信息和总结统计信息 | pytest .\test_one.py -q |
pytest -v | 输出详细的测试用例执行信息,包括每个测试用例的名称、执行结果以及可能的错误信息 | pytest .\test_one.py pytest .\test_one.py -v |
输出的详细程度:pytest -q < pytest < pytest -v
对于同一个含有错误的测试用例-q和-v的执行结果如下
#使用pytest -q 输出的结果
F #只有F [100%]
============================= FAILURES ============================ #测试用例的测试结果
_____________________________ test_abc ____________________________
def test_abc():
> assert 1 == 2
E assert 1 == 2
test_two.py:2: AssertionError #在哪个测试用例的哪一行
===================== short test summary info ======================
FAILED test_two.py::test_abc - assert 1 == 2 #总结统计信息
====================== test session starts ======================= #比-q多了测试用例的信息
platform win32 -- Python 3.6.8, pytest-7.0.1, pluggy-1.0.0 -- D:\资料\学习\数据分析学习\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\资料\学习\数据分析学习, configfile: pytest.ini
plugins: xdist-3.0.2
collected 1 item collected 1 item
test_two.py::test_abc FAILED #对-q的F [100%]
============================= FAILURES ============================ #测试用例的测试结果
_____________________________ test_abc ____________________________
def test_abc():
> assert 1 == 2
E assert 1 == 2
E +1 #比-q的多了期望值
E -2 #比-q的多了实际值
test_two.py:2: AssertionError
===================== short test summary info ======================
FAILED test_two.py::test_abc - assert 1 == 2
========================= 1 failed in 0.02s ======================== #比-q的多了统计和运行时间
命令 | 描述 | 举例 |
---|---|---|
pytest --maxfail=n | 测试到第n出失败就停止 | pytest --maxfail=3 |
pytest -k “xx and xx …” | 测试完整限定名符合表达式的测试用例 | pytest -k “sub and one and not e and d” 解析:完整限定名要有sub、one、d,不能有e 例如test_one.TestMyClass_d.test_sub测试用例 |
完整限定名(Fully Qualified Name):通常指的是一个对象在其所在的模块层级中的唯一标识符。在 Python 中,如果一个对象(如函数、类等)是在模块内定义的,那么它的完整限定名通常由模块名、类名(如果适用)和对象名组成,使用点号 . 来分隔。例如一个函数 test_add() 在模块 test_one.py 中,则它的完整限定名通常是 test_one.test_add;又如一个函数 test_sub() 在模块 test_two.py 的TestMyClass中,则它的完整限定名通常是 test_two.TestMyClass.test_sub
命令 | 描述 | 举例 |
---|---|---|
pytest -m 标记名 | 测试被装饰器 @pytest.mark.标记名 修饰的测试用例 | pytest -m num .\test_two.py pytest -m “num or str” .\test_one.py |
① 首先需要在pytest,ini配置文件中写好markers的配置,再于测试文件中引用配置中存在了的标记,否则引用了一个没有在配置文件中声明的标记就会相应地弹出一个弹出警告,虽然不会报错但是很影响观感
② pytest.ini文件中testpaths为 ./ 表示当前目录,该文件的代码不要出现中文,注释也尽量不要(例如在注释使用了中文,后面使用pytest -n指令的时候会报错),否则很可能报错
③ 在测试函数的上头添加装饰器:@pytest.mark.标记名
④ 如果对多个标记做测试的话,多个标记名需要用引号括起来并且用or连接,例如pytest -m “num or str”
示例代码如下
#pytest.ini文件
[pytest]
#要使用到标记的文件存在于哪个文件夹
testpaths = ./
#对标记的说明不要用中文,例如str:字符串相关的测试用例
markers=
num:number related test cases
str:string related test cases
char:char related test cases
#测试文件
import pytest
@pytest.mark.numrk.num
def test_num1():
assert 1 == 2
@pytest.mark.str
def test_str():
assert "ss" == "cc"
@pytest.mark.char
def test_char():
assert 'c' == 'a'
命令 | 描述 | 举例 |
---|---|---|
pytest -n 数量 | n个进程并发运行测试 | pytest -n 4 |
① 下载插件:pip install pytest-xdist
② 尝试pytest -n 3,如果报错信息有io.write((repr(source) + “\n”).encode(“ascii”))的话,在venv\Lib\site-packages\execnet\gateway_bootstrap.py文件中找到io.write((repr(source) + “\n”).encode(“ascii”))这一句,并修改改为 io.write((repr(source) + “\n”).encode(“utf-8”))即可
③ 尝试pytest -n 3,如果报错信息有UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xae in position 46: illegal multibyte sequence,检查pytest.ini文件中是否有中文注释,有的话去掉即可
命令 | 描述 | 举例 |
---|---|---|
pytest -s | 正常地执行用例中的print,否则是不执行的 | pytest -s .\test_one.py |
Tip:
① pytest的多种运行模式是可以相互叠加的,例如pytest test_one.py -s -v -n 4
② 命令行选项和文件名的位置可以互换,例如 pytest test_one.py -s 等同于 pytest -s test_one.py
5、基础篇 – setup和teardown函数
概述:setup 和 teardown 是测试框架(例如 pytest)中常见的概念,用于在测试用例执行之前和之后执行一些准备和清理工作。这些工作可以包括设置测试环境、准备测试数据、连接数据库、启动服务等等
分类 | 使用方法 | 说明 |
---|---|---|
模块级 | 在类外使用setup()、teardown() | 单个文件一共执行一次setup、teardown |
类外函数级 | 在类外使用setup_function()、teardown_function() | 类外的函数分别都执行一次setup、teardown 类内的函数不执行 |
类级 | 在类中使用setup_class(self)、teardown_class(self) | 单个类一共执行一次setup、teardown |
类中函数级 | 在类中使用setup(self)、teardown(self) | 类中的函数分别都执行一次setup、teardown 类外的函数不执行 |
举例:
import pytest
#类外函数级
def setup_function():
print("----------s-----------")
def teardown_function():
print("----------e-----------")
def test_1(): #输出时有执行setup_function、teardown_function
assert 1 == 2
def test_2(): #输出时有执行setup_function、teardown_function
assert 2 == 3
class TestMyClass:
def test_3(self): #输出时不执行setup_function、teardown_function
assert 3 == 4
def test_4(self): #输出时不执行setup_function、teardown_function
assert 4 == 5
#执行pytest -q -s .\test_two.py的部分输出结果
t----------s-----------
F----------e----------- #第一个F即test_1,说明其有执行setup_function、teardown_function
----------s-----------
F----------e----------- #第二个F即test_2,说明其有执行setup_function、teardown_function
FF #第三、四个F即test_3、test_4,说明其没有执行setup_function、teardown_function
6、基础篇 – Pytest.ini配置文件
pytest.ini 是 pytest 测试框架的配置文件,用于配置 pytest 运行测试时的一些行为和参数,通过编辑 pytest.ini 文件可以定义一些默认的选项和参数,从而影响 pytest 的运行方式
构成 | 说明 |
---|---|
[pytest] | 特殊的配置部分,用于指定 pytest 的默认配置选项 在 pytest 运行时,会读取项目中的 pytest.ini 文件或者 setup.cfg 文件 若其中包含 [pytest] ,则会根据该部分的配置选项来设置 pytest 的行为 |
addopts | 指定额外的命令行选项,这样以后运行时默认地会带上这些选项 若有多个命令行参数,则要以空格分隔 |
testpaths | 测试文件所在的路径 |
python_files | 测试文件的文件名模式,默认为 ** test*.py** |
python_classes | 指定测试类的类名模式,默认为 Test* |
python_functions | 定测试函数的函数名模式,默认为 test*或 *test |
plugins | 测试时要使用的插件 |
markers | 测试用例的标记(使用pytest -k的时候要配置) |
norecursedirs | 忽略某些文件或文件夹 |
cov-report cov | 设置覆盖率报告的输出路径和文件名 |
addopts | 配置 pytest-html 插件 |
log_cli_level | 设置日志级别为 INFO |
timeout | 设置超时时间 |
jobs | 限制同时运行的测试线程数量 |
举例:pytest.ini编写如下,在实际的编写中记得把中文注释去掉,以免报错
[pytest]
testpaths = tests #测试文件所在的路径
addopts = -s -v #指定额外的命令行选项
testpaths = ./ #测试文件所在的路径
python_files = test*.py #测试文件的文件名模式
python_classes = Test_* #测试文件的类名模式
python_functions = test_* #测试文件的函数名模式
# 指定测试时要使用的插件
plugins =
pytest-cov
pytest-html
# 定义测试用例的标记
markers =
slow: mark tests as slow (deselect with '-m "not slow"')
smoke: mark tests as smoke tests
# 忽略某些文件或文件夹
norecursedirs =
.git
.venv
# 设置覆盖率报告的输出路径和文件名
cov-report = html:coverage_report
cov = my_project
# 配置 pytest-html 插件
addopts =
--html=reports/test_report.html
--self-contained-html
# 设置日志级别为 INFO
log_cli_level = INFO
# 设置超时时间
timeout = 300
# 限制同时运行的测试线程数量
jobs = auto
7、基础篇 – Pytest常用的插件
插件 | 描述 |
---|---|
pytest-cov | 用于生成测试覆盖率报告,它会分析你的代码,然后生成一个报告,告诉你哪些代码被测试覆盖了,哪些没有被覆盖 |
pytest-html | 用于生成 HTML 格式的测试报告,它会将测试结果以 HTML 格式展示,包括测试用例的执行情况、失败信息、错误信息等 |
pytest-xdist | 用于支持多进程和分布式测试,它可以加速测试的执行速度,特别是在运行大量测试用例时 |
pytest-sugar | 用于改善 pytest 的默认输出样式,使得测试结果更易读和美观 |
pytest-rerunfailures | 用于重新运行测试失败的测试用例,当有测试用例失败时,它会自动重新运行失败的测试用例,直到它们通过或达到最大重试次数 |
pytest-flake8 | 用于集成 Flake8 工具,用于静态代码分析和代码风格检查,帮助在运行测试时同时检查代码的质量和风格 |
pytest-django | 用于集成 Django 框架,提供对 Django 项目的测试支持,帮助编写和运行 Django 项目的测试用例 |
pytest-mock | 用于提供对 Python 的 unittest.mock 模块的支持,帮助方便地模拟和替换测试中的对象和函数 |
pytest-timeout | 用于设置测试用例的超时时间,当测试用例执行时间超过指定的时间时,它会自动将该测试用例标记为失败,并停止执行 |
pytest-repeat | 用于重复运行测试用例多次,它可以帮助你发现测试用例的稳定性问题,以及识别偶发性的失败 |
① 例1:pytest-sugar插件
安装:pip install pytest-sugar
测试:pytest -q .\test_one.py
② 例2:pytest-html插件
安装:pip install pytest-html
测试:pytest --html=./report.html .\test_one.py
8、进阶篇 – fixture
概述:fixture 是一种用于在测试之前进行准备工作,并在测试之后进行清理工作的机制。它可以帮助你管理测试环境的设置和清理,使得测试代码更加简洁和可维护,fixture 可以用于以下情况:
①:准备测试数据: 可以使用 fixture 来准备测试所需的数据,比如创建数据库连接、初始化对象等
②:设置测试环境: 可以使用 fixture 来设置测试所需的环境,比如启动服务器、设置配置参数等
③:清理测试环境: 可以使用 fixture 来清理测试之后的环境,比如关闭数据库连接、清理临时文件等
fixture(scope=“function”, params=None, autouse=False, ids=None, name=None)
参数 | 说明 |
---|---|
scope | 被标记方法的作用域 “function”(默认):作用于每个测试方法,每个test都运行一次 ”class“:作用于整个类,每个class的所有test只运行一次 ”module“:作用于整个模块,每个module的所有test只运行一次 ”session“:作用于整个session(慎用),每个session只运行一次 |
import pytest
@pytest.fixture(scope="class") # 类级别的 Fixture,作用范围是整个测试类
def setup():
print("\nSetting up test environment")
yield {"username": "test_user", "password": "test_password"}
print("\nTearing down test environment")
class TestClass: # Fixture 在一个类中只会被创建和销毁一次
def test_login(self, setup):
user_info = setup
assert user_info["username"] == "aaa"
assert user_info["password"] == "test_password"
def test_logout(self, setup): # 使用同一个 Fixture 实例
user_info = setup
assert "aaa" in user_info
assert "password" in user_info
yield: 是一个关键字,用于定义生成器函数。生成器函数是一种特殊的函数,它可以在需要时暂停执行,并在下次调用时恢复执行。yield 语句用于向调用者返回一个值,并将函数的执行状态保存起来,以便下次调用时可以从上次暂停的地方继续执行
简单来说:yield相当于return,但是第二次调用生成器函数的时候,会直接执行yield那一行,而不会执行yield前面的代码,如果希望该函数能够不卡在yield那一行而正常执行后面的代码的话,可以使用next(生成器函数),其返回值也是yield携带的东西,只是顺便让生成器函数跳过了yield那一行,例子如下:
def generate_numbers():
yield 1
yield 2
yield 3
# 调用生成器函数,得到一个生成器对象
numbers_generator = generate_numbers()
# 使用 next() 函数逐个获取生成器函数返回的值
print(next(numbers_generator)) # 输出 1
print(next(numbers_generator)) # 输出 2
print(next(numbers_generator)) # 输出 3
# 当生成器函数返回值用尽时,再次调用 next() 函数会触发 StopIteration 异常
try:
print(next(numbers_generator))
except StopIteration:
print("No more items")
参数 | 说明 |
---|---|
params | 允许指定一个参数列表,用于在多个不同的测试用例中重复使用 fixture。每个参数都会生成一个独立的 fixture 实例,每个 fixture 实例都会与对应的测试用例一起运行。这样可以让你在同一个 fixture 上运行多个测试用例,每个测试用例都使用不同的参数 |
import pytest
@pytest.fixture(params=[1, 2, 3])
def setup(request): #必须是request,这是系统封装的参数,不能够更改
return request.param
def test_example(setup):
print(setup)
assert setup > 0
参数 | 说明 |
---|---|
autouse | 是否自动运行,默认为False不运行,设置为True自动运行 |
ids | 为每个参数化 Fixture 提供一个可读性更好的标识符,用于在测试报告中标识不同的 Fixture 实例,默认情况下,参数化 fixture 的标识符为参数的值,可以通过 ids 参数手动指定 |
import pytest
@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"])
def setup(request):
return request.param
def test_example(setup):
assert setup > 0
参数 | 说明 |
---|---|
name | 指定 fixture 的名称。默认情况下,fixture 的名称与定义 fixture 的函数名相同,但可以通过 name 参数手动指定 Fixture 的名称 |
import pytest
@pytest.fixture(name="custom_setup")
def setup():
return {"username": "test_user", "password": "test_password"}
def test_example(custom_setup):
assert custom_setup["username"] == "test_user"
Fixture的用法:
①:作为参数引用
import pytest
class Testmyclass_add:
@pytest.fixture
def before(self):
print('---before---')
def test_abc(self,before): #测试前会调用一次before
assert 3 == 4
def test_def(self,before): #测试前会调用一次before
assert 4 == 5
②:使用装饰器pytest.mark.usefixtures引用
import pytest
@pytest.fixture
def before():
print('---before---')
@pytest.mark.usefixtures('before')
class Testmyclass_add: #类种的函数使用前都会调用一次before
def test_abc(self,before): #测试前会调用一次before
assert 3 == 4
def test_def(self,before): #测试前会调用一次before
assert 4 == 5
③:将fixture的autouse参数值置为True
import pytest
@pytest.fixture(autouse=True)
def before():
print('---before---')
class Testmyclass_add:
def test_abc(self):
assert 3 == 4
def test_def(self):
assert 4 == 5
注意点:fixture和setup、teardown的作用类似,但是
①:同在类外、类内的fixture和函数级setup、teardown,优先级setup>fixture>teardown
②:类外的的fixture和类内的函数级setup、teardown,优先级fixture(yield前)>setup>teardown>fixture(yield后)
9、进阶篇 – skipif、xfail、parametrize 装饰器
skipif概述:用于在特定条件下跳过测试
skipif(condition, reason=None)
参数 | 描述 |
---|---|
condition(必传) | 条件表达式 |
reason(必传) | 标注原因 |
import pytest
class Test_ABC:
def test_a(self):
print("------->test_a")
assert 1
@pytest.mark.skipif(condition=2 > 1, reason="跳过该函数")
def test_b(self):
print("------->test_b")
assert 0
xfail概述:表示预期测试会失败,这个失败与否是由condition表达式来指定的,如果condiction表达式为真,说明这个函数是测试失败的,但由于被xfail装饰器标记,因此测试结果不会视为failed,而是视为xfailed,区别是failed会告诉你测试用例的错误信息是哪些哪些,而xfailed不会告诉你这些信息,因为你已经预知这个测试用例会失败的,所以没有必要再输出一些错误信息给你了。那问题就在既然知道测试用例会失败,那为什么不直接把它删掉,而是留着然后用xfail标记呢,原因是例如为了测试程序员小贺开发的功能A,编写了一个用例a,小贺说功能A开发了一半,待会测试一定会报错的,那我没必要把用例a给删了不然下次还要再一次,于是这个用例的测试结果是xfail,像passed测试结果一样简洁,只不过类型是xfail,这也有利于后续对bug进行修复后能够及时发现还有这个用例没测完
xfail(condition=None, reason=None, raises=None, run=True, strict=False)
参数 | 描述 |
---|---|
condition(必传) | 预期失败的条件表达式,如果条件表达式为真,则 |
reason(必传) | 标注原因 |
raises | 预期异常类型,如果测试函数抛出了与 raises 参数指定的异常类型相同的异常,那么 xfail 标记的测试将被标记为通过 |
run | 控制 xfail 标记的测试是否应该被执行,默认情况下,run 参数为 True,表示 xfail 标记的测试会被执行,若 run 参数设置为 False,则 xfail 标记的测试将不会被执行,而是被跳过 |
strict | 用于控制 pytest 是否应该对 xfail 标记的测试执行严格检查,默认情况下,strict 参数为 False |
import pytest
class Test_ABC:
def test_a(self):
print("------->test_a")
assert 1
@pytest.mark.xfail(2 > 1, reason="标注为预期失败")
def test_b(self):
print("------->test_b")
assert 0
parametrize概述:它允许你为测试函数指定多组参数,并自动运行多次测试,每次使用不同的参数组合,这使得你可以更轻松地编写和管理大量相似的测试用例
parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)
参数 | 描述 |
---|---|
argnames | 参数名 |
argvalues | 参数值,类型为List |
indirect | 参数会间接传递给测试函数(不知道在干嘛,先别管啦) |
ids | 为每组参数提供可读性更好的标识符,如果未指定 ids,则默认情况下,参数的标识符为其值的字符串表示 |
scope | 指定参数化的作用范围。默认情况下,参数化的作用范围为函数级(即每个测试函数的每个参数组合都会单独运行) |
import pytest
# 单个参数:a参数被赋予两个值,函数会运行两遍
@pytest.mark.parametrize("a", [3, 6])
# 参数必须和parametrize里面的参数一致
def test_a(a):
print("test data:a=%d" % a)
assert a % 3 == 0
# 多个参数:参数a,b均被赋予两个值,函数会运行两遍
@pytest.mark.parametrize("a,b", [(1, 2), (0, 3)])
# 参数必须和parametrize里面的参数一致
def test_b(a, b):
print("test data:a=%d,b=%d" % (a, b))
assert a + b == 3
def return_test_data():
return [(1, 2), (0, 3)]
# 使用函数返回值的形式传入参数值
@pytest.mark.parametrize("a,b", return_test_data())
def test_c(a, b):
print("test data:a=%d,b=%d" % (a, b))
assert a + b == 3
10、进阶篇 – 异常回溯显示方式
命令 | 描述 |
---|---|
pytest --tb=auto | 默认值,对于异常回溯的第一行和最后一行采用详细的 ‘long’ 格式,但对于其他行采用更简洁的 ‘short’ 格式 |
pytest --tb=long | 详尽、详细的异常回溯格式 |
pytest --tb=short | 较短的异常回溯格式 |
pytest --tb=line | 每个失败仅显示一行异常回溯 |
pytest --tb=no | 完全禁用异常回溯 |
pytest --tb=native | 使用 Python 标准库的异常回溯格式 |
pytest --showlocals 或 pytest -l | 在异常回溯中显示本地变量 |
pytest --full-trace | 打印超多的错误信息 |
举例:
def test_1():
numerator = 10
denominator = 0
result1 = numerator / denominator #除0异常
assert 4==5
#pytest --tb=auto .\test_one.py的核心输出内容
―――――――――――――――――――――――――― test_1 ――――――――――――――――――――――――
def test_1():
numerator = 10
denominator = 0
> result1 = numerator / denominator
E ZeroDivisionError: division by zero
test_one.py:4: ZeroDivisionError
test_one.py ⨯ 100% ██████████
#pytest --tb=long .\test_one.py的核心输出内容 跟auto结果一样
―――――――――――――――――――――――――― test_1 ――――――――――――――――――――――――
def test_1():
numerator = 10
denominator = 0
> result1 = numerator / denominator
E ZeroDivisionError: division by zero
test_one.py:4: ZeroDivisionError
test_one.py ⨯ 100% ██████████
#pytest --tb=short .\test_one.py的核心输出内容 比auto简略一点
―――――――――――――――――――――――――― test_1 ――――――――――――――――――――――――
test_one.py:4: in test_1
result1 = numerator / denominator
E ZeroDivisionError: division by zero
test_one.py ⨯ 100% ██████████
#pytest --tb= .\test_one.py的核心输出内容 比short更简洁
D:\资料\学习\数据分析学习\test_one.py:4: ZeroDivisionError: division by zero
test_one.py ⨯ 100% ██████████
#pytest --tb=long .\test_one.py的核心输出内容 跟其他的比较不一样
―――――――――――――――――――――――――― test_1 ――――――――――――――――――――――――
Traceback (most recent call last):
File "D:\资料\学习\数据分析学习\test_one.py", line 4, in test_1
result1 = numerator / denominator
ZeroDivisionError: division by zero
test_one.py ⨯ 100% ██████████
#pytest -l .\test_one.py的核心输出内容 比auto多了发生异常相关变量的输出
―――――――――――――――――――――――――― test_1 ――――――――――――――――――――――――
def test_1():
numerator = 10
denominator = 0
> result1 = numerator / denominator
E ZeroDivisionError: division by zero
denominator = 0
numerator = 10
test_one.py:4: ZeroDivisionError
test_one.py ⨯ 100% ██████████
#pytest --full-trace .\test_one.py的核心输出内容 省略 太多了
11、进阶篇 – pdb
命令 | 描述 |
---|---|
pytest --pdb | 每次遇到失败都跳转到 PDB |
pytest -x --pdb | 第一次遇到失败就跳转到 PDB,结束测试执行 |
pytest --pdb --maxfail=3 | 只有前三次失败跳转到 PDB |
举例:在test_one.py编写如下代码,然后执行pytest --pdb
def test_1():
numerator = 10
denominator = 0
result1 = numerator / denominator
assert 4==5
可以看到控制台在异常信息的后面多出了如下信息:
>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> PDB post_mortem >>>>>>>>>>>>>>>>>>>>>>
> d:\资料\学习\数据分析学习\test_one.py(4)test_1()
-> result1 = numerator / denominator
(Pdb)
可以在 PDB 提示符 (Pdb) 后面使用 PDB 提供的命令来检查程序状态,例如
Pdb提示符 | 描述 |
---|---|
p1 变量名 | 输出变量的值 |
bt | 查看完整的堆栈跟踪 |
c | 继续执行程序直到下一个断点或测试结束 |
q | 退出PDB调试并继续执行剩下的测试用例 |
exit | 退出PDB调试但不继续执行剩下的测试用例 |
在脚本中可以使用以下代码来插入断点
import pdb
pdb.set_trace()
12、进阶篇 – 其他命令
命令 | 描述 |
---|---|
pytest --durations=n | 获取用例执行性能数据 获取最慢的n个用例的执行耗时 |
pytest --junitxml=path | 生成 JUnitXML 格式的结果文件 这种格式的结果文件可以被Jenkins或其他CI工具解析 |
pytest -p no:doctest | 禁用插件 关闭 doctest 插件 |
13、阶段三:生成测试报告–Allure
概述:Allure 是一个用于生成漂亮测试报告的开源测试报告框架。它支持多种测试框架(如JUnit、TestNG、PyTest等)和多种编程语言(如Java、Python、JavaScript等),并且提供了丰富的报告功能,包括测试用例的展示、失败重试、历史结果跟踪等
使用 Allure,可以轻松地生成美观、交互式的测试报告,帮助你更好地理解测试结果、识别问题和改进测试质量。该框架提供了各种插件和工具,可以与各种持续集成工具和测试框架集成,为测试流程提供更好的可视化和分析功能
安装:
①:下载 allure,选择一个版本点击进去然后找到zip包下载
②:将allure的bin目录安置在环境变量中,并且确保JAVA_HOME环境变量配置了JDK1.8的jre目录(建议是1.8版本,我尝试了17版本会报错,然后重启Pycharm,在控制台输入allure --version有显示版本号则说明allure安装成功)
③:下载allure-pytest:pip install allure-pytest(建议重启Pycharm)
命令 | 描述 |
---|---|
pytest --alluredir .\report | 生成测试报告目录 |
pytest --alluredir .\report --clean-alluredir | 生成新的 Allure 报告之前清空先前的报告目录,避免旧的报告文件对新报告的影响 |
allure serve .\report\ | 根据指定的测试报错目录下的文件生成报告页面 |
举例:pytest --alluredir .\report–>allure serve .\report\ 于是allure就会开启一个服务器用于展示测试报告了,报告样子如下:
14、小案例
①:pytest+selenium
from selenium import webdriver
def test_baidu():
driver = webdriver.Chrome()
driver.get("http://www.baidu.com")
title = driver.title
url = driver.current_url
text = driver.find_element_by_xpath("//a[@href='http://news.baidu.com']").text
button_text = driver.find_element_by_id('su').get_attribute('value')
assert title == "百度一下,你就知道"
assert url == "http://www.baidu.com/"
assert text == "新闻"
assert button_text == "百度一下"
driver.quit()
四、声明
本文第二部分(Selenium)参考了:
Yunlord(博客) 的文章 selenium入门超详细教程——网页自动化操作
软件测试老白(小破站) 的视频 Python+Selenium4 Web自动化测试框架
本文第三部分(Pytest)参考了:
慕城南风(博客) 的文章 Python测试框架之pytest详解
软件测试老白(小破站) 的视频 Python+Selenium4 Web自动化测试框架
感谢大牛们的分享和帮助!