likes
comments
collection
share

Python解析YAML: PyYAML使用小记

作者站长头像
站长
· 阅读数 17

一、PyYAML介绍

以下介绍为chatgpt生成。

PyYAML 是一个用于解析和生成 YAML 数据的 Python 库。它提供了简单易用的接口,用于读取和写入 YAML 格式的文件、字符串或流。

以下是一些关于 PyYAML 的重要特点和功能:

  1. 解析和生成:PyYAML 支持将 YAML 数据解析为 Python 对象,并且可以将 Python 对象转换为 YAML 格式。
  2. 支持标准:PyYAML 实现了 YAML 1.1 规范,兼容大多数 YAML 实现。它支持常见的 YAML 特性,如标量、映射、序列、锚点引用等。
  3. 安全加载:PyYAML 提供了 SafeLoader,它是一个相对较安全的加载器,用于加载 YAML 数据时限制执行的操作,以降低潜在的安全风险。
  4. 高级加载器:除了 SafeLoader,PyYAML 还提供了 Loader,它是一个更灵活的加载器,支持加载自定义类型和构造函数。
  5. 写入和流:PyYAML 可以将 Python 对象转换为 YAML 格式的字符串,并且可以将 YAML 数据流写入文件或流。
  6. 自定义类型:PyYAML 允许注册和使用自定义类型和构造函数,以支持特定的对象序列化和反序列化需求。
  7. 扩展性: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 tagPython type
Standard YAML tags
!!nullNone
!!boolbool
!!intint or long (int in Python 3)
!!floatfloat
!!binarystr (bytes in Python 3)
!!timestampdatetime.datetime
!!omap, !!pairslist of pairs
!!setset
!!strstr or unicode (str in Python 3)
!!seqlist
!!mapdict
Python-specific tags
!!python/noneNone
!!python/boolbool
!!python/bytes(bytes in Python 3)
!!python/strstr (str in Python 3)
!!python/unicodeunicode (str in Python 3)
!!python/intint
!!python/longlong (int in Python 3)
!!python/floatfloat
!!python/complexcomplex
!!python/listlist
!!python/tupletuple
!!python/dictdict

五、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'}

注意:

  1. 环境变量通常都是字符串形式。如果你使用了数字、布尔等其他类型,需要解析后手动转换。
  2. os.path.expandvars是一个内置函数,用于展开字符串中的环境变量。它会在目标字符串中查找形式为$VAR${VAR}的环境变量引用,并将其替换为响应的环境变量的值。如果找不到匹配的环境变量,那么引用保持不变。
  3. 这里使用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 Docs

ruamel.yaml是基于 PyYAML 3.11的扩展库,提供了额外的功能和改进。

这是一个功能丰富且兼容性较好的 YAML 库,支持 YAML 1.2 标准,并提供了对保留注释、保留标签、自定义类型等高级功能的支持。它可以与 PyYAML 代码兼容,并具有更好的性能和一些额外的功能。

ruamel.yaml和PyYAML的差异

九、总结

本文介绍了Python的YAML解析库PyYAML,它的概念、简单使用、自定义三大组件,以及自定义实现PyYAML加载时渲染环境变量。还扩展了一些YAML的概念和主要语法,如标量、序列、映射和锚点引用。为大家理解和使用 PyYAML 尽绵薄之力~

十、参考链接

PyYAML Documentation YAML 1.2 YAML 入门教程 YAML 教程 ChatGPT convert-yaml-to-json