【Python】使用Beautiful Soup等三种方式定制Jmeter测试脚本
背景介绍
我们在做性能调优时,时常需要根据实际压测的情况,调整线程组的参数,比如循环次数,线程数,所有线程启动的时间等。
如果是在一台Linux机器上,就免不了在本机打开图形页面修改,然后最后传递到压测机上面的过程,所有为了解决这个业务痛点
,使用Python写了一个能直接修改Jmeter基础压测参数的脚本,能修改jmx脚本的线程组数、循环次数、线程组全部启动需要花的时间。
实现思路
刚开始准备写这个脚本的时候,想了两个思路:
把脚本数据读出,使用正则表达式(re库)匹配关键数据进行修改
优点:可以快速的改写数据
缺点:无法进行区块的修改
把脚本数据读出,使用BeautifulSoup的xml解析功能解析后修改
注:我们的Jmx脚本其实就是一个标准格式的xml
优点: 能快速的查找元素并进行修改
缺点: 需要熟悉BeautifulSoup的用法
通过Beautiful Soup
Beautiful Soup
Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.我们使用BeautifulSoup解析xml或者html的时候,能够得到一个 BeautifulSoup 的对象,我们可以通过操作这个对象来完成原始数据的结构化数据。具体的使用可以参照这份文档。
具体实现
主要使用了bs4的soup.find 和self.soup.find_all功能。结化或数据的修改如loops.string = num。
值得注意的是,find_all支持正则匹配,甚至如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数。
修改后的脚本将以"T{}L{}R{}-{}_{}.jmx".format(thread_num, loop_num, ramp_time, self.src_script, self.get_time()) 的形式保存,具体封装如下:
import time
import os
from bs4 import BeautifulSoup
class OpJmx:
def __init__(self, file_name):
self.src_script = self._split_filename(file_name)
with open(file_name, "r") as f:
data = f.read()
self.soup = BeautifulSoup(data, "xml")
@staticmethod
def _split_filename(filename):
"""
新生成的文件兼容传入相对路径及文件名称
:param filename:
:return:
"""
relative = filename.split("/")
return relative[len(relative)-1].split(".jmx")[0]
def _theard_num(self):
"""
:return: 线程数据对象
"""
return self.soup.find("stringProp", {"name": {"ThreadGroup.num_threads"}})
def _ramp_time(self):
"""
:return: 启动所有线程时间配置对象
"""
return self.soup.find("stringProp", {"name": {"ThreadGroup.ramp_time"}})
def _bean_shell(self):
"""
:return: bean_shell对象
"""
return self.soup.find("stringProp", {"name": {"BeanShellSampler.query"}})
def _paths(self):
"""
:return: 请求路径信息对象
"""
return self.soup.find_all("stringProp", {"name": {"HTTPSampler.path"}})
def _methods(self):
"""
:return: 请求方法对象
"""
return self.soup.find_all("stringProp", {"name": {"HTTPSampler.method"}})
def _argument(self):
"""
:return: post请求参数对象
"""
# Argument.value 不唯一 通过HTTPArgument.always_encode找到
return self.soup.find_all("boolProp", {"name": {"HTTPArgument.always_encode"}})[0].find_next()
def _loops(self):
"""
循环次数,兼容forever 与具体次数
:return: 循环次数对象
"""
_loops = self.soup.find("stringProp", {"name": {"LoopController.loops"}})
if _loops:
pass
else:
_loops = self.soup.find("intProp", {"name": {"LoopController.loops"}})
return _loops
@staticmethod
def get_time():
return time.strftime("%Y-%m-%d@%X", time.localtime())
def get_bean_shell(self):
_str = self._bean_shell().string
logger.info("bean_shell: " + _str)
return _str
def set_bean_shell(self, new_bean_shell):
old_bean_shell = self._bean_shell()
old_bean_shell.string = new_bean_shell
def get_ramp_time(self):
_str = self._ramp_time().string
logger.info("ramp_time: " + _str)
return _str
@check_num
def set_ramp_time(self, num):
loops = self._ramp_time()
loops.string = num
def get_loops(self):
_str = self._loops().string
logger.info("loops: " + _str)
return _str
@check_num
def set_loops(self, num):
"""
:param num: -1 为一直循环,其他为具体循环次数
:return:
"""
loops = self._loops()
loops.string = num
def get_argument(self):
_str = self._argument().string
logger.info("argument: " + _str)
return _str
def set_argument(self, **kwargs):
"""
设置请求参数(JSON,传入字典)
:param kwargs:
:return:
"""
param = self._argument()
param.string = str(kwargs)
def get_thread_num(self):
_str = self._theard_num().string
logger.info("thread_num: " + _str)
return _str
@check_num
def set_thread_num(self, num):
"""
设置线程数信息
:param num:
:return:
"""
thread_num = self._theard_num()
thread_num.string = num
# print(self.soup.find_all("stringProp", {"name": {"ThreadGroup.num_threads"}})[0].string)
def mod_header(self, key, value, index=0):
"""
修改指定header的信息,默认修改第一个值
:param key:
:param value:
:param index:
:return:
"""
headers = self.soup.find_all("elementProp", {"elementType": {"Header"}})
headers[index].find("stringProp", {"name": {"Header.name"}}).string = key
headers[index].find("stringProp", {"name": {"Header.value"}}).string = value
# for header in headers:
# header.find("stringProp", {"name": {"Header.name"}}).string = key
# header.find("stringProp", {"name": {"Header.value"}}).string = value
def save_jmx(self):
logger.info("参数设置完毕,开始保存数据")
cur_path = os.path.dirname(os.path.realpath(__file__))
thread_num = self.get_thread_num()
loop_num = self.get_loops()
ramp_time = self.get_ramp_time()
script_name = "T{}L{}R{}-{}_{}.jmx".format(thread_num, loop_num, ramp_time, self.src_script, self.get_time())
script_path = os.path.join(cur_path, '..', 'script')
if not os.path.exists(script_path):
os.mkdir(script_path)
script_location = os.path.join(script_path, script_name)
logger.info("测试脚本已保存于 {}".format(script_location))
with open(script_location, "w") as f:
f.write(str(self.soup))
return script_name
if __name__ == '__main__':
jmx = OpJmx("templates/template.jmx")
argvs = sys.argv
len_argvs = len(argvs) - 1
if len_argvs == 0:
pass
elif len_argvs == 1:
jmx.set_thread_num(argvs[1])
elif len_argvs == 2:
jmx.set_thread_num(argvs[1])
jmx.set_loops(argvs[2])
elif len_argvs == 3:
jmx.set_thread_num(argvs[1])
jmx.set_loops(argvs[2])
jmx.set_ramp_time(argvs[3])
jmx.save_jmx()
未完待续...
使用string.Template字符替换
如果只是简单的字符串替换,使用 format 或者 %s 也能完成,选择使用string.Template的原因是string.Template可以自动化匹配规则,且能修改操作符,
而不管是fstring还是format都是用的{}来进行关键字的定位,{}在jmx脚本中本身就存在特定的意义。
思路:
- 修改jmx脚本中的关键数据,使用特定操作符
- 定义相关字典,使用
safe_substitute进行赋值
具体实现
#! /usr/bin/python
# coding:utf-8
"""
@author:Bingo.he
@file: str_temp.py
@time: 2019/08/20
"""
import string
# with open("template_str.jmx", "r") as f:
# data = f.read()
set_value = {
"num_threads": 10,
"loops": 1011,
"ramp_time": 10
}
str_temp = """
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">%loops</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">%num_threads</stringProp>
<stringProp name="ThreadGroup.ramp_time">%ramp_time</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
</ThreadGroup>
"""
class MyTemplate(string.Template):
# 修改操作符为"%"
delimiter = '%'
# 修改匹配规则(正则)
# idpattern = '[a-z]+_[a-z]+'
t = MyTemplate(str_temp)
print(t.safe_substitute(set_value))
输出:
...
<stringProp name="LoopController.loops">1011</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">101</stringProp>
<stringProp name="ThreadGroup.ramp_time">10</stringProp>
...
使用re.sub
str_temp = """
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">$loops</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">$num_threads</stringProp>
<stringProp name="ThreadGroup.ramp_time">$ramp_time</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
</ThreadGroup>
"""
str_l = re.sub(r"\$loops", "101", str_temp)
str_t = re.sub(r"\$num_threads", "102", str_l)
str_r = re.sub(r"\$ramp_time", "103", str_t)
print(str_r)
输出:
···
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">101</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">102</stringProp>
<stringProp name="ThreadGroup.ramp_time">103</stringProp>
···
延展
相信大家也注意到了,我们每替换一个参数都需要调用一次re.sub,而且要将上一次调用的输出作为下一次的输入,像极了递归调用。但是我们今天不介绍递归改写的方法,而是使用闭包的方式,具体的例子如下:
import re
def multiple_replace(text, adict):
rx = re.compile('|'.join(map(re.escape, adict)))
def one_xlat(match):
return adict[match.group(0)]
return rx.sub(one_xlat, text) # 每遇到一次匹配就会调用回调函数
# 把key做成了 |分割的内容,也就是正则表达式的OR
map1 = {'1': '2', '3': '4', '5': '6'}
_str = '113355'
print(multiple_replace(_str, map1))
【Python】使用Beautiful Soup等三种方式定制Jmeter测试脚本的更多相关文章
- Python实现微信支付(三种方式)
Python实现微信支付(三种方式) 关注公众号"轻松学编程"了解更多. 如果需要python SDk源码,可以加我微信[1257309054] 在文末有二维码. 一.准备环境 1 ...
- Python中字符串拼接的三种方式
在Python中,我们经常会遇到字符串的拼接问题,在这里我总结了三种字符串的拼接方式: 1.使用加号(+)号进行拼接 加号(+)号拼接是我第一次学习Python常用的方法,我们只需要把我们要加 ...
- python 获取表单的三种方式
条件:urls.py文件中配置好url的访问路径.models.py文件中有Business表. 在views.py文件中实现的三种方式: from app01 improt models def b ...
- python之配置日志的三种方式
以下3种方式来配置logging: 1)使用Python代码显式的创建loggers, handlers和formatters并分别调用它们的配置函数: 2)创建一个日志配置文件,然后使用fileCo ...
- 【Python】蟒蛇绘制(三种方式+import用法)
第一种方式不会出现函数重名问题,而第二种会.可以用第三种解决问题 方式一: #pythondraw.py import turtle #引用 绘制(海龟)库 turtle.setup(650,350, ...
- 【转】Python中执行cmd的三种方式
原文链接:http://blog.csdn.net/menglei8625/article/details/7494094 目前我使用到的python中执行cmd的方式有三种: 1. 使用os.sys ...
- python 入门学习---模块导入三种方式及中文凝视
Python 有三种模块导入函数 1. 使用import 导入模块 import modname : 模块是指一个能够交互使用,或者从还有一Python 程序訪问的代码段.仅仅要导入了一个模块,就能够 ...
- Python读取文件内容的三种方式并比较
本次实验的文件是一个60M的文件,共计392660行内容. 程序一: def one(): start = time.clock() fo = open(file,'r') fc = fo.readl ...
- appium+python自动化46-安装app三种方式
前言 adb安装 1.在app自动化之前,首先手机上有要被测试的app,如何把电脑本地上的app安装到手机上呢?可以在运行自动化代码前,在cmd输入adb指令,把电脑app安装到手机上 adb ins ...
随机推荐
- 逐步探究ObjC的Weak技术底层
前言 之前的文章有说过 Atomic 原子操作的原理,其作为一个特殊的修饰前缀,影响了存取操作. 在属性修饰定义中,还有另一类修饰前缀,他们分别是 strong weak assign copy,这些 ...
- 8.Redis的复制(Master/Slave)
Redis的复制(Master/Slave) a)是什么 行话:也就是我们所说的主从复制,主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave ...
- vue axios传参报错的解决方法
今天有人问同一套后台系统为什么jquery可以正常使用,axios却报错呢,下面总结如下: 总的来说是jquery和axios传参类型不同,那为什么jquery和axios请求时传参类型不同? 1)j ...
- SpringBoot LoggerFactory is not a Logback LoggerContext but Logback is on the classpath
SpringBoot 在启动项目的时候一起错误: SLF4J: Class path contains multiple SLF4J bindings.SLF4J: Found binding in ...
- Paper Reading:FPN
FPN 论文:Feature Pyramid Networks for Object Detection 发表时间:2017 发表作者:(Facebook AI Research)Tsung-Yi L ...
- php中float浮点型字段查询问题(数据对应不上)
薪资表 查询员工提成时候 比如说表里面是88.8 查询出来则是89 反复调试 未果,最后查看表设计 把金额字段类型设置成float 最后换成double 解决问题!!!!
- 【简易DFS/BFS+标记搜索次序的数组】zznu-2025 : 简单环路
2025 : 简单环路 时间限制:1 Sec 内存限制:128 MiB提交:145 答案正确:41 提交 状态 编辑 讨论区 题目描述 有一个N x M 大小的地图,地图中的每个单元包含一个大写字母. ...
- vue-cli webpack打包后index.html引入文件没有引号
参考地址:https://blog.csdn.net/i_coffer/article/details/81005733 在对vue-cli项目打包后出现index.html引入的css和js没有引号 ...
- 修改HTTPS加密协议TLS1.0为TLS1.2
一:首先为什么要改为TLS1.2 因为各大浏览器相继发布声明将停止支持 TLS 1.0 和 TLS 1.1 https://www.cnblogs.com/jpush88/p/9846047.html ...
- WPF界面开发者注意啦!Scheduler控件支持时区功能了,你get了吗
DevExpress广泛应用于ECM企业内容管理. 成本管控.进程监督.生产调度,在企业/政务信息化管理中占据一席重要之地.通过DevExpress WPF Controls,您能创建有着强大互动功能 ...