Python解析YAML:深入了解PyYAML
Python解析YAML: PyYAML详解
Python解析YAML: PyYAML详解
一、PyYAML介绍
以下介绍为chatgpt
生成。
PyYAML
是一个用于解析和生成 YAML
数据的 Python
库。它提供了简单易用的接口,用于读取和写入 YAML
格式的文件、字符串或流。
以下是一些关于 PyYAML
的重要特点和功能:
-
解析和生成:
PyYAML
支持将YAML
数据解析为Python 对象
,并且可以将Python 对象
转换为YAML 格式
。 -
支持标准:
PyYAML
实现了YAML 1.1 规范
,兼容大多数YAML
实现。它支持常见的YAML 特性
,如标量、映射、序列、锚点引用等。 -
安全加载:
PyYAML
提供了SafeLoader
,它是一个相对较安全的加载器,用于加载YAML 数据
时限制执行的操作,以降低潜在的安全风险。 -
高级加载器:除了
SafeLoader
,PyYAML
还提供了Loader
,它是一个更灵活的加载器,支持加载自定义类型和构造函数。 -
写入和流:
PyYAML
可以将Python 对象
转换为YAML 格式
的字符串,并且可以将YAML 数据
流写入文件或流。 -
自定义类型:
PyYAML
允许注册和使用自定义类型和构造函数,以支持特定的对象序列化和反序列化需求。 -
扩展性:
PyYAML
提供了灵活的 API,
允许你根据需要进行自定义扩展,例如添加标签处理程序、构造函数和序列化器等。
使用 PyYAML
,你可以轻松地在 Python
中处理 YAML 数据
,无论是读取和解析现有的 YAML 文件
,还是将数据转换为 YAML 格式
并进行写入。这使得 PyYAML
成为处理配置文件、数据交换、持久化存储等场景中的有用工具。
以下是一个简单示例,展示了如何使用 PyYAML
加载和保存 YAML 文件
:
import yaml
# 加载 YAML 文件
with open('config.yaml', 'r') as file:
config = yaml.safe_load(file)
# 修改配置数据
config['database']['host'] = 'localhost'
config['database']['port'] = 3306
# 保存配置到 YAML 文件
with open('config.yaml', 'w') as file:
yaml.dump(config, file)
在上述示例中,我们首先使用 safe_load
函数加载 YAML 文件
。然后,我们对配置数据进行修改,并使用 dump 函数
将修改后的数据保存回 YAML 文件
。
总的来说,PyYAML
提供了一种简单和方便的方式来处理 YAML 数据
,并与 Python
的数据类型和对象进行交互。
二、YAML 标量、映射、序列
在YAML
中,有3种基本的数据结构:Scalars
(标量)、Sequences
(序列)和Mappings
(映射)。它们用于构建和表示复杂的数据。
标量(Scalars)
标量是单个的、不可再分的值。标量可以是字符串、数字、布尔值、null、日期等。下面是一些标量数据类型的例子:
boolean:
- TRUE #true, True都可以
- FALSE #false, False都可以
float:
- 3.14
- 6.8523015e+5 #可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 #二进制表示
null: ~ #使用~或者null表示null
string:
- 哈哈
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行, 每一行会被转化成一个空格
date:
- 2018-02-17 #日期必须使用ISO 8601格式, 即yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式, 时间和日期之间使用T连接, 最后使用+代表时区
对于多行字符串标量,YAML
有多种表示方式:
1、普通样式、单/双引号样式(plain, single-quoted, double-quoted)
lines: line1
line2
line3
line4
line5
lines: 'line1
line2
line3
line4
line5'
转换为python对象
:
{'lines': 'line1 line2 line3\nline4 line5'}
转换时,去掉缩进,换行->空格,空行->换行。
2、字面样式(literal )
lines: |
line1
line2
line3
line4
line5
转换为python对象
:
{'lines': 'line1\n line2\nline3\n\nline4\nline5\n'}
转换时,保留缩进和换行
3、折叠样式(folded)
lines: >
line1
line2
line3
line4
line5
转换为python对象
:
{'lines': 'line1\n line2\nline3\nline4 line5\n'}
转换时,保留缩进和换行。但相邻、无缩进的两个非空行,会合并成一行,并用一个空格字符分隔。
4、如果是序列中的元素表示多行字符串,请用以下格式:
- >
line1
序列(Sequences)
序列是一组按次序排列的值,每一个元素都可以是任意数据类型。
以连字符-开头的行构成一个序列:
- A
- B
- C
序列的行内表示:
key: [value1, value2, ...]
映射(Mappings)
映射是一种键值对的集合,其中每个键都必须唯一。
每个键值对用冒号+一个空格分隔(key: value
)
key:
child_key1: value1
child_key2: value2
映射的行内表示:
key: {child_key1: value1, child_key2: value2}
三、YAML语法: 锚点和引用
我们在编写yaml
时,会遇到一些相同的数据被重复使用,比如一些配置信息如用户名、邮箱、数据库配置等。如果这类数据发生变更,我们就需要修改很多处,维护很麻烦。
在编程中我们通常用变量引用来解决这种问题,YAML
也提供了一种类似的机制实现变量引用,它就是锚点引用(Anchors and Aliases)。
在 YAML
中,锚点引用是一种可以引用先前定义的数据节点的机制。锚点引用允许你在 YAML 文件
中重用相同的数据节点,避免了数据的重复定义,提高了可维护性和可读性。通过给节点添加一个锚点(使用 &
符号),然后使用别名(使用 *
符号)引用该锚点,可以将值重复使用。
1、定义锚点:
使用 & 符号
可以定义一个锚点。锚点定义在数据节点的位置,并且可以在 YAML 文件
中的其他位置引用该锚点。例如:
key: &anchor value
如上,&anchor
了一个锚点,名为anchor
,相当于value
这个数据节点的别名。
2、引用锚点:
使用 * 符号
+锚点名
可以引用先前定义的锚点。引用锚点时,你可以在 YAML 文件
的其他位置使用锚点的名称。例如:
key2: *anchor
key3: *anchor
在上述示例中,*anchor
引用了先前定义的锚点,这样 key2
和 key3
的值将与 &anchor
定义的数据节点相同。
3、锚定映射和序列节点:
person: &person_anchor
name: Jane
age: 18
tasks:
- &task1_anchor task1
- task2
4、合并映射
YAML
提供了一种<<语法
用于合并映射(Mappings)
。它允许你将一个映射的键值对合并到另一个映射中,从而实现映射之间的继承和合并。这种语法也被称为"merge key"
。
合并映射需要结合锚点引用来使用,例如:
base: &base
name: John
age: 30
extension:
<<: *base
age: 31
city: New York
转换成python对象
:
{'base': {'name': 'John', 'age': 30}, 'extension': {'name': 'John', 'age': 31, 'city': 'New York'}}
使用 << 语法
,你可以避免在多个映射中重复定义相同的键值对,从而提高代码的可维护性和可读性。这对于定义共享的配置或基本设置,并在派生配置中进行定制化非常有用。
需要注意的是,<< 语法
只适用于映射之间的合并,而不适用于序列(Sequences)
或标量(Scalars)
。
四、YAML标签
在YAML
中,标签(Tags)
用于对数据进行类型标识或自定义标识。它们提供了一种扩展YAML数据模型
的方式。
在 PyYAML
中,标签是用于标识和处理 YAML 数据节点
的类型的特殊注释。PyYAML
支持一些内置标签,用于标识常见的数据类型。以下是一些常用的 PyYAML 标签
的介绍:
# 标识字符串类型
name: !!str John
# 标识整数类型
count: !!int 10
# 标识浮点数类型
pi: !!float 3.1415926
# 标识布尔类型
is_valid: !!bool true
# 标识null
data: !!null
这些标签用于标识不同的数据类型,帮助解析器和处理程序正确地理解和处理 YAML 数据
。标签的使用是可选的,因为 YAML 解析器
可以根据数据的结构和内容自动识别大多数常见的数据类型。
对于PyYAML
而言,标签可以是隐式的或显式的。
显式声明的如:
boolean: true
integer: 3
float: 3.14
隐式的如:
boolean: !!bool "true"
integer: !!int "3"
float: !!float "3.14"
它们都被转换为相同的python对象:
{'boolean': True, 'integer': 3, 'float': 3.14}
在PyYAML中,没有显式定义标签的标量,都服从隐式标签解析。隐式标签解析会根据一组正则表达式来检查标量,如果其中一个表达式匹配,就会分配对应的标签给标量。
自定义标签
除了内置标签外,你还可以定义自己的标签,用于标识自定义类型或特殊处理。这允许你在加载和转储 YAML 数据
时进行定制化操作。例如:
!CustomType
- item1
- item2
比如 PyYAML
就定义了很多!!python
/开头的自定义标签,用来帮助解析和生成python
对象数据。
下面这张表描述了PyYAML
如何将带有不同标签的数据节点转换为python对象
。
YAML tag | Python type |
---|---|
Standard YAML tags | |
!!null | None |
!!bool | bool |
!!int | int or long (int in Python 3) |
!!float | float |
!!binary | str (bytes in Python 3) |
!!timestamp | datetime.datetime |
!!omap, !!pairs | list of pairs |
!!set | set |
!!str | str or unicode (str in Python 3) |
!!seq | list |
!!map | dict |
Python-specific tags | |
!!python/none | None |
!!python/bool | bool |
!!python/bytes | (bytes in Python 3) |
!!python/str | str (str in Python 3) |
!!python/unicode | unicode (str in Python 3) |
!!python/int | int |
!!python/long | long (int in Python 3) |
!!python/float | float |
!!python/complex | complex |
!!python/list | list |
!!python/tuple | tuple |
!!python/dict | dict |
五、PyYAML 三大组件(构造器、表示器、解析器)
在 PyYAML
中,Constructors(构造器)
、Representers(表示器)
和Resolvers(解析器)
是用于处理 YAML 数据
的重要组件。它们的主要作用如下:
1. Constructors(构造器):
Constructors
是用于将 YAML 数据
解析为 Python 对象
的组件。它们负责将 YAML 数据
的不同类型转换为相应的 Python 对象
。PyYAML
提供了一些内置的构造器,用于处理常见的数据类型,如字符串、整数、浮点数、布尔值等。同时,你也可以自定义构造器,以便将 YAML 数据
解析为自定义的 Python 类型
。通过注册构造器,你可以扩展 PyYAML
的解析功能,使其能够处理更多的数据类型。
2. Representers(表示器):
与构造器相反,Representers
是用于将 Python 对象
表示为 YAML 数据
的组件。它们负责将 Python 对象
转换为 YAML
中的相应表示形式。PyYAML
提供了一些内置的表示器,用于处理常见的 Python 对象
类型,如字符串、整数、浮点数、布尔值等。同时,你也可以自定义表示器,以便将自定义的 Python 类型
表示为 YAML 数据
。通过注册表示器,你可以定制 PyYAML
的转储功能,使其能够生成符合特定需求的 YAML 数据
。
3. Resolvers(解析器):
Resolvers
是用于解析 YAML 数据
中的标签(Tags
)的组件。它们负责识别标签并将其映射到相应的构造器和表示器。PyYAML
提供了内置的标签解析器,用于处理常见的标准类型。同时,你也可以自定义解析器,以便识别和处理自定义的标签。通过注册解析器,你可以扩展 PyYAML
的标签识别功能,使其能够处理更多的数据类型和标签。
让我们通过两个自定义类型例子来理解这三大组件。
自定义类型自动注册构造器和表示器
假如我们有一个 python
类型 Monster
,现在需要自定义一个YAML标签
用于解析和生成Monster类型
数据。
最简单的实现方式是让 Monster
继承 yaml.YAMLObject
yaml.YAMLObjectclass Monster(yaml.YAMLObject):
yaml_tag = u'!Monster'
def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks
def __repr__(self):
return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % (
self.__class__.__name__, self.name, self.hp, self.ac, self.attacks)
定义如上这个Monster类
,已经足够让PyYAML
自动加载和转储 Monster 对象
了。
1、从 YAML
加载 Monster对象
:
yaml.load("""
!Monster
name: Cave spider
hp: [2, 6]
ac: 16
attacks: [BITE, HURT]
""", yaml.Loader)
执行结果:
Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
2、转储Monster
对象为YAML格式
数据:
yaml.dump(Monster(name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']))
执行结果:
!Monster
ac: 16
attacks:
- BITE
- HURT
hp:
- 3
- 6
name: Cave lizard
我们知道,PyYAML解析
和表示YAML数据
,都需要对应数据类型的构造器和表示器。那为什么我们仅仅是定义了一个Monster类
,就能够解析和表示这种数据类型呢?难道在定义Monster类
的时候PyYAML
自动帮我们创建了Monster类型
的构造器和表示器?
没错,正是这样的。我们知道元类的__init__()方法
只在每个类被定义时调用一次。而PyYAML
正利用了这种元类机制,在我们定义Monster
继承yaml.YAMLObject
时,触发元类的__init__()方法
,为当前类的所有yaml_loader
自动生成、注册了Monster
的构造器和表示器。
源码如下:
# YAML基础模型类
class YAMLObject(metaclass=YAMLObjectMetaclass):
"""
An object that can dump itself to a YAML stream
and load itself from a YAML stream.
"""
__slots__ = () # no direct instantiation, so allow immutable subclasses
# 设置当前YAMLObject模型类使用的Loader和Dumper, 这些Loader和Dumper会自动注册构造器和表示器
yaml_loader = [Loader, FullLoader, UnsafeLoader]
yaml_dumper = Dumper
...
# YAML基础模型类的元类,自动注册构造器和表示器
class YAMLObjectMetaclass(type):
"""
The metaclass for YAMLObject.
"""
def __init__(cls, name, bases, kwds):
super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
# 为Loader注册当前YAML模型类的构造器
if isinstance(cls.yaml_loader, list):
for loader in cls.yaml_loader:
loader.add_constructor(cls.yaml_tag, cls.from_yaml)
else:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
# 为Dumper注册当前YAML模型类的表示器
cls.yaml_dumper.add_representer(cls, cls.to_yaml)
自定义类型手动注册构造器和表示器
PyYAML
给我们提供了 yaml.add_constructor
和 yaml.add_representer
两个函数,用来手动注册自定义构造器和表示器。
假如我们需要加载和转储自定义类型 Dice
:
class Dice(tuple):
def __new__(cls, a, b):
return tuple.__new__(cls, [a, b])
def __repr__(self):
return "Dice(%s,%s)" % self
自定义表示器
让我们先看看PyYAML
的默认转储YAML格式
:
yaml.dump(Dice(3,6))
执行结果:
!!python/object/new:__main__.Dice
- !!python/tuple
- 3
- 6
看起来不太美好。假如我们想将它序列化为 AdB
的格式,即yaml.dump(Dice(3,6))
的结果为3d6
首先我们要定义一个表示器,用于将Dice对象转换成带有!dict标签的标量节点:
def dice_representer(dumper, data):
return dumper.represent_scalar(u'!dice', u'%sd%s' % data)
然后将表示器注册到默认的Dumper
:
yaml.add_representer(Dice, dice_representer)
再次尝试转储Dice对象
:
yaml.dump(Dice(3,6))
转储结果符合预期:
!dice '3d6'
自定义构造器
自定义构造器并注册到Loader
、FullLoader
和UnsafeLoader
def dice_constructor(loader, node):
value = loader.construct_scalar(node)
a, b = map(int, value.split('d'))
return Dice(a, b)
yaml.add_constructor(u'!dice', dice_constructor)
让我们加载YAML数据试试:
yaml.load("initial hit points: !dice 8d4", yaml.Loader)
加载结果符合预期:
{'initial hit points': Dice(8,4)}
识别和处理隐式的自定义标签
我们可能希望自定义的标签也能像内置标签一样,被隐式的识别和解析。比如我们在YAML
中不指定!dice标签
,而是把所有XdY格式
的未标记标量,都识别成!dice
并自动调用构造器解析。这时候我们可以添加一个解析器来实现:
import re
pattern = re.compile(r'^\d+d\d+$')
yaml.add_implicit_resolver(u'!dice', pattern)
再次尝试转储Dice对象
,发现不再显示!dice标签
:
>>> yaml.dump({"treasure": Dice(10, 20)})
'treasure: 10d20\n'
加载YAML数据
,也能隐式解析XdY格式
的标量、加载成Dice对象
:
>>> yaml.load("damage: 5d10", yaml.Loader)
{'damage': Dice(5,10)}
六、PyYAML 引用环境变量
我们在编写YAML文件
时,可能会希望能够引用环境变量,以提供更灵活的配置选项、或提高配置安全性。
我们可以通过自定义标签,标识环境变量类型,并自定义这个标签的构造器,以替换环境变量。
自定义替换环境变量的构造器并注册到SafeLoader
(或是其他Loader
)
def render_env_constructor(loader, node):
value = loader.construct_scalar(node)
return os.path.expandvars(value)
yaml.SafeLoader.add_constructor('!ENV', render_env_constructor)
让我们来测试效果:
import os
os.environ["a"]="111"
os.environ["b"]="222"
yaml.safe_load("test: !ENV a=${a}, b=$b")
测试成功!在加载YAML
时自动替换了标签!ENV
标识标量的环境变量占位符:
{'test': 'a=111, b=222'}
注意:
-
环境变量通常都是字符串形式。如果你使用了数字、布尔等其他类型,需要解析后手动转换。
-
os.path.expandvars
是一个内置函数,用于展开字符串中的环境变量。它会在目标字符串中查找形式为$VAR
或${VAR}
的环境变量引用,并将其替换为响应的环境变量的值。如果找不到匹配的环境变量,那么引用保持不变。 -
这里使用
SafeLoader
而不是Loader
,因为它相较Loader
更安全。Loader
支持加载任意的Python对象
,可能导致执行任意 Python 代码
。而SafeLoader
限制了加载过程中执行的操作,只允许加载基本类型的对象,如字符串、整数、列表和字典等。 当你处理来自不受信任的源或不确定性的YAML 文件
时,特别是从外部来源加载时,使用SafeLoader
是一个良好的实践,可以提高安全性并防止可能的代码注入或其他潜在的安全问题。
七、集成 LibYAML 加快yaml解析和生成
LibYAML
是一个独立的C库
,用于处理 YAML 数据
的解析和生成。PyYAML
可以使用 LibYAML
作为其解析和生成 YAML 数据
的底层引擎。LibYAML
具有高性能和低内存占用的特点,可以加速 YAML 数据
的处理。
PyYAML
提供 CLoader
和 CDumper
两个组件用来集成 LibYAML
:
CLoader
: PyYAML
的解析器(Loader
)的C扩展版本
CDumper
: PyYAML
的生成器(Dumper
)的C扩展版本
CLoader
和CDumper
是PyYAML
提供的优化组件,它们通过与LibYAML
的集成,提供了更高效的解析和生成功能。使用CLoader
和CDumper
可以在处理大型YAML数据
时获得更好的性能和效率。
CLoader
和 CDumper
是可选组件,在使用它们之前需要下载安装 LibYAML
: (PyYAML 6.x
似乎不需要)
# 下载源码包
wget http://pyyaml.org/download/libyaml/yaml-0.2.5.tar.gz
# 解压
tar xf yaml-0.2.5.tar.gz
cd yaml-0.2.5/
# 配置构建。检查系统环境、依赖项和其他设置,生成适合系统的 Makefile 文件
./configure
# 编译源码并构建软件。根据 Makefile 中的指令,执行编译、链接等操作,生成可执行文件或库文件。
make
# 将构建好的软件安装到系统中。
# 将可执行文件、库文件和其他必要的文件复制到指定的目标位置,使软件可以在系统中被使用和访问。
make install
安装好 LibYAML
后,再下载 PyYAML
的源码包python setup.py --with-libyaml install
重新安装。(究极麻烦,告辞!)
使用 CLoader
和 CDumper
:
from yaml import load, dump
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
# ...
data = load(stream, Loader=Loader)
# ...
output = dump(data, Dumper=Dumper)
八、扩展库 ruamel.yaml
ruamel.yaml
是基于 PyYAML 3.11
的扩展库,提供了额外的功能和改进。
这是一个功能丰富且兼容性较好的 YAML 库
,支持 YAML 1.2
标准,并提供了对保留注释、保留标签、自定义类型等高级功能的支持。它可以与 PyYAML
代码兼容,并具有更好的性能和一些额外的功能。
九、总结
本文介绍了Python
的YAML
解析库PyYAML
,它的概念、简单使用、自定义三大组件,以及自定义实现PyYAML
加载时渲染环境变量。还扩展了一些YAML
的概念和主要语法,如标量、序列、映射和锚点引用。为大家理解和使用 PyYAML
尽绵薄之力~
—————————END—————————
Python_魔力猿
Python解析YAML: PyYAML详解
作者:坦笑&&life