9.5 使用YAML进行转储和加载

    关于YAML,yaml.org网页中是这样描述的:

    YAML™(与“camel”押韵)是一种人性化的,跨语言的,基于Unicode编码进行数据序列化,围绕敏捷编程语言中数值类型而设计的语言。

    在Python标准库文档中关于json模块的部分是这样描述的:

    JSON是YAML1.2的一个子集。基于这个模块的默认设置(尤其是默认的分隔符),将生成JSON,也同样是YAML1.0和1.1的子集。这个模块也可用于YAML的序列化器。

    从技术的角度上来看,可使用json模块来生成YAML数据。然而json模块不能被用来完成很复杂的YAML数据的反序列操作。使用YAML有两点优势:首先,它的格式本身很复杂,我们可以为对象的附加信息进行编码;其次,pyYAML的实现在底层很好地集成了Python,可以很简单地为Python对象进行YAML编码。YAML的缺点是它的使用范围不像JSON这样广泛。我们需要下载安装一个YAML模块。可以通过这里进行下载:http://pyyaml.org/wiki/PyYAML。一旦完成安装,就可以使用YAML格式为对象执行转储操作。

    import yaml
    text= yaml.dump(travel2)
    print( text )

    以下是使用YAML编码后的博客。

    !!python/object:main.Blog
    entries:
    - !!python/object:main.Post

     date: 2013-11-14 17:25:00
     rsttext: Some embarrassing revelation. Including
    and
     tags: !!python/tuple ['#RedRanger', '#Whitby42', '#ICW']
     title: Hard Aground
    - !!python/object:_main
    .Post

     date: 2013-11-18 15:30:00
     rst_text: Some witty epigram. Including < & > characters.
     tags: !!python/tuple ['#RedRanger', '#Whitby42', '#Mistakes']
     title: Anchor Follies

    此时的输出相对简洁但很完整。而且,可以直接通过编辑YAML文件进行更新。类名使用了YAML中!!标签进行编码。YAML中包含了11个标准的标签。yaml模块包含了很多为Python定制的标签以及5个complex Python标签。

    在Python类名中包含所定义的模块是有意义的。对于我们的例子来说,模块是一个精简的脚本,因此类名为main.Blogmain.Post。当在另一个模块中导入它们时,从类名就可以看出哪个模块定义了这些类。

    一个列表中的项将以一个块序列的形式呈现。每个项以一个-序列为起始,其余部分以两个空格缩进。当集合或元组足够小,可缩进为一行。当长度增加时,就显示为多行。为了完成从一个YAML文档中加载Python对象的过程,可使用如下代码。

    copy= yaml.load(text)

    使用标签提供的信息来完成对类定义的查找定位,并将YAML文档中拿到的值传给类的构造函数,进而完成微博对象的构造。

    9.5.1 YAML文件的格式化

    当写YAML文件时,通常会像如下代码这样做。

    with open("some_destination.yaml", "w", encoding="UTF-8") as target:
      yaml.dump( some_collection, target )

    使用所需的编码打开文件。将文件对象传给yaml.dump()方法;进而完成输出。当读取YAML文件时,会使用类似的技巧。

    with open("some_source.yaml", "r", encoding="UTF-8") as source:
      objects= yaml.load( source )

    思路是将YAML作为文本的表示与字节转换的过程分开。有一些格式化的方式,可用来为我们的数据创建更好的YAML的表示。下表列出了其中的一些。


    explicit start

    如果是true,则在每个对象前写一个—-标记

    explicit end

    如果为true,则在每个对象前写一个…标记。当我们将一个YAML文档的序列转储到一个文件中并且操作是串行的时候,可以使用它或者explicitstart

    version

    指定一个整数对(x,y),在文件头输出%YAML x.y,这应该是版本=(1,2)

    tags

    指定一种映射,在文件头使用不同的标签缩写输出一个YAML %TAG

    canonical

    如果为true,则每块数据都包含一个标签,如果为false,则认为包含了很多标签

    indent

    如果设定一个数字,就会改变块之间的缩进

    width

    如果设定一个数字,当项太长以至于显示为多行,缩进行时,这个设置会改变行宽度

    allow unicode

    如果设为true,将支持完整的、没有包含转义符的Unicode编码。否则,在ASCII自己外部的字符就会包含使用了转义符的字符

    line _ break

    使用一种不同的行结束符,默认是换行符

    以上这些选项中,explicit_endallow_unicode可能是最常用的。

    9.5.2 扩展YAML的表示

    有时,YAML默认的对属性值的转储行为,某些类有更简洁的表达方式。例如,对于21点中的Card类定义来说,默认的YAML会包括一些衍生的值,而它们并不需要被使用或保存。

    有关为类定义添加描述器和构造器这点,yaml模块中包括了一个条款。描述器被用于创建一种YAML的表示方式,包括一个标签和值。构造器用于基于给定的值创建一个Python对象,这里是Card类层次结构的另一种定义。

    class Card:
      def init( self, rank, suit, hard=None, soft=None ):
        self.rank= rank
        self.suit= suit
        self.hard= hard or int(rank)
        self.soft= soft or int(rank)
      def str( self ):
        return "{0.rank!s}{0.suit!s}".format(self)
    class AceCard( Card ):
      def init( self, rank, suit ):
        super().init( rank, suit, 1, 11 )

    class FaceCard( Card ):
      def init( self, rank, suit ):
        super().init( rank, suit, 10, 10 )

    我们为纸牌定义了基类并为扑克牌和人头牌(扑克中的J、Q、K)定义了子类。在之前的例子中,使用了可扩展的工厂函数来简化构造函数的逻辑。工厂完成了从牌面值为1到AceCard类和牌面值为11、12以及13到FaceCard类的映射。这样做是必需的,因为只有这样我们才能够确保可以使用range(1,14)这样一个简单的语句来完成纸牌的初始化,进而创建一个deck对象。

    当加载一个YAML文件时,类必须以YAML的!!标签来阐明。唯一缺失的信息就是与纸牌子类中的软点数和硬点数。对于软点数和硬点数来说,有3种简单的情况可以通过可选的初始化参数来解决。当使用默认的序列化行为转储这些对象到YAML格式时,可能会表示如下。

    - !!python/object:main.AceCard {hard: 1, rank: A, soft: 11, suit:

    ♣}
    - !!python/object:main.Card {hard: 2, rank: '2', soft: 2, suit: ♥}
    - !!python/object:main.FaceCard {hard: 10, rank: K, soft: 10,

    suit: }

    它们是正确的,但对于打牌这样的简单场景又显得有些多余。可通过扩展yaml模块来生成更简洁的输出,并且它们将主要用于简单对象的表示。接下来要做的是为Card子类定义描述器和构造器。以下代码包含了3个函数的定义以及如何将它们注册到yaml模块:

    def card_representer(dumper, card):
      return dumper.represent_scalar('!Card',
      "{0.rank!s}{0.suit!s}".format(card) )
    def acecard_representer(dumper, card):
      return dumper.represent_scalar('!AceCard',
      "{0.rank!s}{0.suit!s}".format(card) )
    def facecard_representer(dumper, card):
      return dumper.represent_scalar('!FaceCard',
      "{0.rank!s}{0.suit!s}".format(card) )

    yaml.add_representer(Card, card_representer)
    yaml.add_representer(AceCard, acecard_representer)
    yaml.add_representer(FaceCard, facecard_representer)

    我们将每个Card实例表示为一个精简的字符串。YAML中包含了一个标签用来指定这个字符串被用来创建哪个类。这3个类使用了相同的格式化字符串,正好与str()方法匹配,因而可以进一步被优化。

    另一个需要解决的问题是从解析后的YAML文档来创建Card实例。对这点来说,我们需要构造器,以下定义了3个构造器以及它们的注册过程。

    def card_constructor(loader, node):
      value = loader.construct_scalar(node)
      rank, suit= value[:-1], value[-1]
      return Card( rank, suit )

    def acecard_constructor(loader, node):
      value = loader.construct_scalar(node)
      rank, suit= value[:-1], value[-1]
      return AceCard( rank, suit )

    def facecard_constructor(loader, node):
      value = loader.construct_scalar(node)
      rank, suit= value[:-1], value[-1]
      return FaceCard( rank, suit )

    yaml.add_constructor('!Card', card_constructor)
    yaml.add_constructor('!AceCard', acecard_constructor)
    yaml.add_constructor('!FaceCard', facecard_constructor)

    当一个标准值被解析时,就会使用标签来对特定的构造器进行查找定位。构造器然后会对字符串进行分解并创建Card子类的实例。如下是一个实例,从每个类中转储一张纸牌。

    deck = [ AceCard('A','♣',1,11), Card('2','♥',2,2),
    FaceCard('K','',10,10) ]
    text= yaml.dump( deck, allow_unicode=True )

    以下是输出结果。

    [!AceCard 'A♣', !Card '2♥', !FaceCard 'K']

    这里给出了一种简洁、优雅的使用YAML来表示纸牌的方式,可用于创建Python对象。

    我们可以使用如下的简单语句来重新创建3张牌:

    cards= yaml.load( text )

    这将使用构造器函数来解析表达式,然后创建所期望的对象。因为构造器函数会确保初始化过程可被正常完成,以及软硬点数属性也会被正确地创建。

    9.5.3 安全性与安全加载

    从原则上来说,YAML可以创建任何类型的对象。在网络上传输YAML文件的过程中,如果没有使用SSL进行控制,应用程序就有可能遭到攻击。

    YAML模块提供了safe_load()方法,在创建对象的过程中会拒绝Python代码的执行。这样就在加载上进行了限制。比如数据交换,我们使用yaml.safe_load()来创建Python的dictlist对象,它们只包含内置类型。然后可以基于dictlist实例来创建应用程序中的类。这点与使用JSON或CSV来进行dict数据交换的方式是类似的,其中dict用于创建适当的对象。

    一个更好的方式是使用yaml.YAMLObject mixin类来创建对象。我们使用它来设置类级别的属性,这些属性为yaml提供一些提示并确保对象构造过程的安全性。以下是为安全传输而定义的一个基类。

    class Card2( yaml.YAMLObject ):
      yaml_tag = '!Card2'
      yaml_loader= yaml.SafeLoader

    这两个特性会提示yaml,这些对象可被安全加载,没有包含任意可执行的、不可预料的Python代码。Card2的每个子类只需设置YAML标签,它们也是唯一会被用到的。

    class AceCard2( Card2 ):
      yaml_tag = '!AceCard2'

    我们加入了一个特性,用于提示yaml这些对象只在这个类定义中使用。这些对象可被安全加载,它们不会执行任何可疑的代码。

    类定义经过这些修改后,现在就可以在YAML流使用yaml.safeload()方法了,而无需担心在不安全的网络链接中文档被注入了不安全代码。为类对象显式地使用yaml.YAMLObject mixin类,并设置yamltag属性会有一些优势。它使得文件被进一步压缩得更紧凑了,也生成了更美观的YAML文件——看起来长一些并且通用的!!python/object:__mainAceCard标签被替换成了短一些的!AceCard2标签。