python yaml文件操作
一:yaml简介及基础语法
yaml是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便。
1.1 yaml基础语法规则
- 大小写敏感
- 使用缩进表示层级关系
- 不允许使用 TAB 键来缩进,只允许使用空格键来缩进
- 缩进的空格数量不重要
- 使用”#”来表示注释
1.2 yaml 支持的数据结构有三种
- 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
- 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
- 纯量(scalars):单个的、不可再分的值
1.2.1 对象
对象的一组键值对,使用:(冒号)结构表示。冒号之后必须有一个空格
#test.yml文件 animal: pets #转为python 如下: {'animal': 'pets'}
yaml 也允许另一种写法,将所有键值对写成一个行内对象。
#test.yml文件 hash: { name: Steve, foo: bar } #转为python如下: {'hash': {'name': 'Steve', 'foo': 'bar'}}
1.2.2 数组
一组连词线开头的行,构成一个数组。
#test.yml文件 - Cat - Gog - Goldfis #转为python 如下: ['Cat', 'Gog', 'Goldfis']
数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。
#test.yml 文件 - - Cat - Dog - Goldfish #转为 python 如下: [['Cat', 'Dog', 'Goldfish']]
数组也可以采用行内表示法。
#test.yml文件 animal: [Cat, Dog] #转为 python 如下; {'animal': ['Cat', 'Dog']}
1.2.3 复合结构
对象和数组可以结合使用,形成复合结构。
#test.yml文件 languahes: - JavaScript - java - Python websites: YAML: yaml.org Ruby: ruby-lang.org Python: python.org Perl: use.perl.org #转为 python 如下; { 'languahes': ['JavaScript', 'java', 'Python'], 'websites': {'YAML': 'yaml.org', 'Ruby': 'ruby-lang.org', 'Python': 'python.org', 'Perl': 'use.perl.org'} }
1.2.4 纯量
纯量是最基本的、不可再分的值。
纯量包含:字符串 整数 浮点数 布尔值 Null(用~表示) 时间 日期 其中:用~表示null ; 时间、日期采用IS08601格式
#test.yml文件 name: 'xiaoli' age: 22 weight: 57.30 isStudent: true address: ~ time: 2001-12-14t21:59:43.10-05:00 date: 1976-07-31 #转为 python 如下: {'name': 'xiaoli', 'age': 22, 'weight': 57.3, 'isStudent': True, 'address': None,
'time': datetime.datetime(2001, 12, 14, 21, 59, 43, 100000, tzinfo=datetime.timezone(datetime.timedelta(-1, 68400))),
'date': datetime.date(1976, 7, 31)}
yaml允许使用两个感叹号,强制转换数据类型。
#test.yml文件 e: !!str 22 f: !!str true #转为 python 如下: {'e': '22', 'f': 'true'}
1.2.5 锚点&和引用*
在yaml文件中如何引用变量?当我们在一个yaml文件中写很多测试数据时候,比如一些配置信息像用户名,邮箱,数据库配置等很多地方都会重复用到。
重复的数据,如果不设置变量,后续维护起来就很困难。
yaml文件里面也可以设置变量(锚点&),其它地方重复用到的话,可以用*引用
#test.yml文件
defaults: &defaults
adapter: postgres
host: localhost
development:
database: myapp_development
<<: *defaults
test:
database: myapp_test
<<: *defaults
等同于下面的代码:
defaults:
adapter: postgres
host: localhost
development:
database: myapp_development
adapter: postgres
host: localhost
test:
database: myapp_test
adapter: postgres
host: localhost
&用来建立锚点(defaults),<<表示合并到当前数据,*用来引用锚点。
1.2.6 *引用value值
上面的例子是对defaults整体的数据,引用到其它地方了,有时候我们只想引用其中的一个值
#test.yml 文件 defaults: adapter: postgres host: &host 127.0.0.1 development: database: myapp_development myhost: *host #等同于下面的数据 defaults: adapter: postgres host: &host 127.0.0.1 development: database: myapp_development myhost: 127.0.0.1
二:yaml文件的读写
python本身并没有自带的处理yaml文件的库,需要下载并安装第三方库PyYAML 或 ruamel.yaml ,这里我们先安装PyYAML。
#安装命令 pip install pyyaml # 下载速度慢的话加上清华镜像源 pip install pyyaml -i https://pypi.tuna.tsinghua.edu.cn/simple
2.1 从yaml中读取数据
# yaml文件,文件名为test.yml os: Android osVersion: 10 account: username: xiaoqq password: 123456 deviceName: null appPackage: ~ bool1: True
读取代码:
import yaml import os file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test.yml')) def read_yaml_data(): with open(file_path, 'r', encoding='utf-8') as f: data = yaml.load(f, Loader=yaml.FullLoader) print(f'读取的数据:{data}') print(f'数据类型为:{type(data)}') if __name__ == '__main__': read_yaml_data()
读取结果:
读取的数据:{'os': 'Android', 'osVersion': 10, 'account': {'username': 'xiaoqq', 'password': 123456}, 'deviceName': None, 'appPackage': None, 'bool1': True} 数据类型为:<class 'dict'>
从读取结果可以看出:
1,读取出来的数据不会改变原数据类型,即yaml里是什么数据类型,读出来就是什么类型。
2,Loader=yaml.FullLoader参数不写的话对结果不会有影响,但运行时会出现警告信息。
3,yaml.load(f.read(), Loader=yaml.FullLoader)也可以写成yaml.load(f, Loader=yaml.FullLoader),读取出来的结果相同。
注意:
pyyaml模块在python中用于处理yaml格式数据,主要使用yaml.safe_dump()、yaml.safe_load()函数将python值和yaml格式数据相互转换。当然也存在yaml.dump()、yaml.load()函数,同样能实现数据转换功能,只是官方不太推荐使用。官方给出的解释,因为yaml.safe_dump()、yaml.safe_load() 能够,而且yaml.safe_dump()、yaml.safe_load()比yaml.dump()、yaml.load()安全:
2.2 从yaml中读取多组数据
yaml多组数据时,每组数据之间需要用3横杠分隔’—’,如下:
#test.yml文件 os: Android osVersion: 10 account: username: xiaoqq password: 123456 deviceName: null appPackage: ~ bool1: True --- os: ios osVersion: 12 account1: username2: Lilei password2: 888888
从yaml中读取多组数据时需要使用yaml.load_all()或者yaml.safe_load_all()方法,返回结果为一个生成器,需要使用for循环语句获取每组数据。代码如下:
def read_yaml_data(): file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test.yml')) with open(file_path, 'r', encoding='utf-8') as f: data = yaml.safe_load_all(f) print(f'读取的数据:{data}') print(f'数据类型为:{type(data)}') for i in data: print(i) if __name__ == '__main__': read_yaml_data()
读取结果:
读取的数据:<generator object load_all at 0x102b28258> 数据类型为:<class 'generator'> {'os': 'Android', 'osVersion': 10, 'account': {'username': 'xiaoqq', 'password': 123456}, 'deviceName': None, 'appPackage': None, 'bool1': True} {'os': 'ios', 'osVersion': 12, 'account1': {'username2': 'Lilei', 'password2': 888888}}
2.3 单组数据写入yaml文件
使用yaml.dump()或者yaml.safe_dump()方法,加入allow_unicode=True参数防止写入的中文乱码,如下
def write_yaml_data(): data = {'hash': {'name': 'Steve', 'foo': '公寓'}} file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test.yml')) with open(file_path, 'w', encoding='utf-8') as f: yaml.safe_dump(data, f, allow_unicode=True) if __name__ == '__main__': write_yaml_data()
执行结果:
2.4 多组数据写入yaml文件
使用yaml.dump_all()或者yaml.safe_dump_all()方法,如下:
def write_yaml_data(): # 写入多组数据 data = {'hash': {'name': 'Steve', 'foo': '公寓'}} data1 = {'hash1': {'name1': 'admin', "name": "流动人口社区"}} file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test.yml')) with open(file_path, 'w', encoding='utf-8') as f: yaml.safe_dump_all(documents=[data,data1], stream=f, allow_unicode=True) if __name__ == '__main__': write_yaml_data()
执行结果:
2.5 ruamel.yaml 读写yaml文件
用yaml模块写入字典嵌套字典这种复杂的数据,会出现大括号{ },不是真正的yaml文件数据,可以用ruamel模块就解决。
参考:https://www.cnblogs.com/yoyoketang/p/9255109.html
安装方法:
pip install ruamel.yaml
2.5.1 用原生的yaml模块写入这种字典嵌套字典的复杂数据
import os import yaml # 将字典写入到yaml desired_caps = { 'platformName': 'Android', 'platformVersion': '7.0', 'deviceName': 'A5RNW18316011440', 'appPackage': 'com.tencent.mm', 'appActivity': '.ui.LauncherUI', 'automationName': 'Uiautomator2', 'unicodeKeyboard': [True,"hh"], 'resetKeyboard': True, 'noReset': True, 'chromeOptions': {'androidProcess': 'com.tencent.mm:tools'} } curpath = os.path.dirname(os.path.realpath(__file__)) yamlpath = os.path.join(curpath, "caps.yaml") # 写入到yaml文件 with open(yamlpath, "w", encoding="utf-8") as f: yaml.dump(desired_caps, f)
运行结果:
由运转结果,发现字典嵌套的字典,出现了大括号:{androidProcess: ‘com.tencent.mm:tools’},(在pyyaml版本为6.0时 已经不存在该问题)这不是真正的yaml数据,不是我们想要的,解决办法看下文
2.5.2 使用ruamel模块 方法跟yaml差不多,只是在使用dump方法多个一个参数:Dumper=yaml.RoundTripDumper
import os from ruamel import yaml # 将字典写入到yaml desired_caps = { 'platformName': 'Android', 'platformVersion': '7.0', 'deviceName': 'A5RNW18316011440', 'appPackage': 'com.tencent.mm', 'appActivity': '.ui.LauncherUI', 'automationName': 'Uiautomator2', 'unicodeKeyboard': True, 'resetKeyboard': True, 'noReset': True, 'chromeOptions': {'androidProcess': 'com.tencent.mm:tools'} } curpath = os.path.dirname(os.path.realpath(__file__)) yamlpath = os.path.join(curpath, "caps.yaml") # 写入到yaml文件 with open(yamlpath, "w", encoding="utf-8") as f: yaml.dump(desired_caps, f, Dumper=yaml.RoundTripDumper)
执行结果:
2.6 ruamel.yaml 读yaml文件
2.6.1.使用ruamel.yaml模块也能读yaml文件,使用方法相对于之前的yaml.load方法多加一个参数:Loader=yaml.Loader
def read_yaml_data(): # 读取单组数据 file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'test.yml')) with open(file_path, 'r', encoding='utf-8') as f: data = yaml.load(f, Loader=yaml.Loader) print(f'读取的数据:{data}') print(f'数据类型为:{type(data)}') if __name__ == '__main__': read_yaml_data()
读取结果:
读取的数据:{'os': 'Android', 'osVersion': 10, 'account': {'username': 'xiaoqq', 'password': 123456}, 'deviceName': None, 'appPackage': None, 'bool1': True} 数据类型为:<class 'dict'>
三:使用template替换yaml文件中的变量
在接口自动化测试的时候,yaml 文件一般放测试的数据或当配置文件使用,yaml 文件存放静态的数据是没问题的,python的数据类型基本上都是支持的。
有时候我们想在 yaml 文件中引用变量来读取 python 代码的设置值
3.1 template 使用
template 是字符串模板,用于替换字符串中的变量,是 string 的一个类引用变量有 2 种格式
- $variable 使用 $变量名 引用变量
- ${variable} 使用 ${变量名} 大括号包起来
第一种 $variable
from string import Template tempTem = Template("My name is $name , i like $fancy") d = {'name': 'admin', 'fancy': 'python'} print(tempTem.substitute(d))
执行结果:
My name is admin, i like python
第二种 ${variable}
from string import Template tempTemp1 = Template("My name is ${name} , i like ${fancy}") d = {'name': 'admin', 'fancy': 'python'} print(tempTemp1.substitute(d))
执行结果:
My name is admin, i like python
3.2 safe_substitute使用
上面的方式只能严格的匹配变量,当字符串中有$符号,不想匹配变量的时候,会报错
from string import Template tempTemplate = Template("$My name is ${name} , i like ${fancy}") d = {'name': 'admin', 'fancy': 'python'} print(tempTemplate.substitute(d))
这段,$符号加在My的前面,我只想让它是一个普通的字符串,不想引用变量,就出现了报错说找不到这个key
Traceback (most recent call last): File "E:/PycharmScripts/test_selenium_wire_yaml/test_yaml.py", line 25, in <module> print(tempTemp2.substitute(d)) File "C:\Python37\lib\string.py", line 132, in substitute return self.pattern.sub(convert, self.template) File "C:\Python37\lib\string.py", line 125, in convert return str(mapping[named]) KeyError: 'My'
虽然字符串定义了多个变量,但是引用的时候只给了name这个值,也不影响运行,没给值的当普通字符串出来,这样就很完美了
tempTemplate = Template("$My name is ${name} , i like ${fancy}") d = {'name': 'admin','fancy': 'python'} print(tempTemplate.safe_substitute(d))
执行结果;
$My name is admin , i like python
3.3 yaml 文件引用变量
通过前面 Template 的基础使用,已经掌握了基本的用法了,接下来在 yaml 文件中引用变量
request: headers: Content-Type: application/json User-Agent: python-requests/2.18.4 json: username: $user password: $psw
python读yaml文件代码:
from string import Template import yaml with open("test.yml", encoding='utf-8') as fp: read_yml_str = fp.read() # print(read_yml_str) tempTemplate1 = Template(read_yml_str) new_yaml = tempTemplate1.safe_substitute({"user": "admin", "psw": "123456 "}) print(f'Template补全的数据:\n{new_yaml}') # yml 文件数据,转 python 类型 yaml_data = yaml.safe_load(new_yaml) print(f'yml文件转python的数据:\n{yaml_data}')
执行结果:
''' Template补全的数据: request: headers: Content-Type: application/json User-Agent: python-requests/2.18.4 json: username: admin password: 123456 yml文件转python的数据: {
'request': {'headers': {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4'},
'json': {'username': 'admin', 'password': 123456}}
} '''
四:yaml文件调用python外部函数
如何在 yaml文件中引用一个 python 的函数?
问题分析
其实yaml 和 json 文件本质上是一样的,都是静态的文件,是不能直接引用 python 的函数。
那这时候就有人问到了,那为什么 httprunner 框架可以在yaml文件中引用函数呢?
这是因为 httprunner 框架封装过对 yaml 文件的读取了,它是先读取文件内容,正则提取到 ${} 括号里面的函数内容,再把函数的值替换过去
那么我们能不能实现这种效果呢?
当然是可以的,可以参考httprunner的实现,也可以用到 python 的模板 jinja2 来实现。
使用模板可以编写出可读性更好,更容易理解和维护的代码,并且使用范围非常广泛,因此怎么使用模板主要取决于我们的想象力和创造力。
python的模板库jinja2 功能是非常强大的
4.1 jinja2 模板库
pip install jinja2
4.2 render 函数实现
在yaml文件中,通过 {{ 函数名称() }}
来引用函数
写个 render 函数读取 yaml 文件内容
import jinja2 import os,random def jinja2_template_render(tpl_path, **kwargs): path, filename = os.path.split(tpl_path) env = jinja2.Environment(loader=jinja2.FileSystemLoader(path or './')) # 创建一个包加载器对象 template = env.get_template(filename) # 获取一个模板文件 return template.render(**kwargs) # 进行模板渲染
读取到的yaml文件本质上都是字符串来读取的,通过jinja2 模板来读取,会先把函数的值替换进去。最后再转成python的dict结构
import jinja2 import os,random,yaml def jinja2_template_render(tpl_path, **kwargs): path, filename = os.path.split(tpl_path) env = jinja2.Environment(loader=jinja2.FileSystemLoader(path or './')) # 创建一个包加载器对象 template = env.get_template(filename) # 获取一个模板文件 return template.render(**kwargs) # 进行模板渲染 # yaml 文件调用以下函数 def rand_str(): return str(random.randint(1000000, 2000000)) if __name__ == '__main__': r = jinja2_template_render('test.yml',**{"user":"admin","pwd":1234,"rand_str":rand_str}) print(f'jinja2渲染后的数据\n{r}') print(yaml.safe_load(r))
执行的结果:
''' jinja2渲染后的数据: username: admin password: 1234 func: 1980646 {'username': 'admin', 'password': 1234, 'func': 1980646} '''
上面读取函数是写死的,我们希望能自动加载类似于debugtalk.py的文件来自动加载函数
4.3 自动加载debug.py里面的函数
写一个debug.py 文件,实现 yaml 文件里面定义的函数去替换值
#debug.py文件 import random # yaml 文件调用以下函数 def rand_str(): return str(random.randint(1000000, 2000000))
在run.py里面定义一个函数自动读取debug.py里面的函数,生成dict 键值对格式
关键动态导入对象请参考:https://blog.csdn.net/edward_zcl/article/details/88809212
def all_functions(): """加载debug.py模块""" debug_module = importlib.import_module("debug") all_function = inspect.getmembers(debug_module, inspect.isfunction) print(dict(all_function)) return dict(all_function)
函数返回 {'rand_str': <function rand_str at 0x0000000002E3F798>}
完整的run.py文件内容
import jinja2 import os,random,yaml import importlib,inspect def jinja2_template_render(tpl_path, **kwargs): path, filename = os.path.split(tpl_path) env = jinja2.Environment(loader=jinja2.FileSystemLoader(path or './')) # 创建一个包加载器对象 template = env.get_template(filename) # 获取一个模板文件 return template.render(**kwargs) # 进行模板渲染 def all_functions(): """加载debug.py模块""" debug_module = importlib.import_module("debug") all_function = inspect.getmembers(debug_module, inspect.isfunction) print(dict(all_function)) return dict(all_function) if __name__ == '__main__': temlplate_params = all_functions() #获取调用函数 temlplate_params["user"] = "admin" #获取user变量值 temlplate_params["pwd"] = "1234" #获取pwd变量值 r = jinja2_template_render('test.yml',**temlplate_params) print(f'jinja2渲染后的数据\n{r}') print(yaml.safe_load(r))
执行结果:
''' #all_functions()返回值 {'rand_str': <function rand_str at 0x0000000002E3F708>} jinja2渲染后的数据 username: admin password: 1234 func: 1730704 {'username': 'admin', 'password': 1234, 'func': 1730704} '''