yaml: 无法确定标签的构造函数。

1,367 阅读4分钟

所以你试图用PyYAML读取一些YAML,结果得到一个类似这样的异常。

>>> yaml.safe_load("!!python/tuple [0,0]") Traceback (most recent call last): ... yaml.constructor.ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/tuple' in "<unicode string>", line 1, column 1: !!python/tuple [0,0] ^

...或者像这样。

>>> yaml.safe_load("!GetAZs us-east-1") Traceback (most recent call last): ... yaml.constructor.ConstructorError: could not determine a constructor for the tag '!GetAZs' in "<unicode string>", line 1, column 1: !GetAZs us-east-1 ^

这是什么意思? #

首先,介绍一下背景。

在基本类型(字符串、整数、序列等)的基础上,YAML可以表示本地和用户定义的数据结构。 为了表示一个节点的类型,你可以用一个明确的标签来标记它。 即使是基本类型也会有一个标签;下面这些都是等同的。

>>> yaml.safe_load("[implicit]") ['implicit'] >>> yaml.safe_load("!!seq [global, shorthand]") ['global', 'shorthand'] >>> yaml.safe_load("!<tag:yaml.org,2002:seq> [global, full]") ['global', 'full']

上面的错误都意味着加载器遇到了一个显式标签,但不知道如何用该标签构造对象。

为什么会发生这种情况? #

!!python/tuple 是一个特定语言的标签,对应于Python的本地数据结构(一个元组)。然而, ,只解析基本的YAML标签,已知对不信任的输入是安全的。safe_load()

!GetAZs PyYAML没有办法在没有明确告知的情况下知道它。

这是设计上的,来自于规范

也就是说,标签解析是针对应用程序的。因此,YAML处理器应该提供一种机制,允许应用程序覆盖和扩展这些默认的标签解析规则。

现在怎么办? #

Python特定的标签 #

对于Python特定的标签,你可以使用full_load() ,它可以解决所有的标签*,除了*那些已知的不安全的标签;这包括这里列出的所有标签。

你也可以使用unsafe_load() ,但大多数时候这不是你想要的。

警告

yaml.unsafe_load() 对于不受信任的数据不安全的,因为它允许运行任意代码。 考虑使用 或 来代替。safe_load() full_load()

例如,你可以这样做。

>>> yaml.unsafe_load("!!python/object/new:os.system [echo WOOSH. YOU HAVE been compromised]") WOOSH. YOU HAVE been compromised 0

有一堆关于它的CVE

应用程序特定的标签 #

对于特定应用的标签,你可以为该标签定义一个构造函数。

@dataclass class GetAZs: region: str class Loader(yaml.SafeLoader): pass def construct_GetAZs(loader, node): return GetAZs(loader.construct_scalar(node)) Loader.add_constructor('!GetAZs', construct_GetAZs)

在这里,我们将值包装在一个数据类中,以表明它不仅仅是一个简单的字符串。

注意

我们对SafeLoader进行子类化,因为对它调用add_constructor() ,会对它进行就地修改,对每个人来说,这不一定是好事;想象一下从safe_load() ,当你期望只有内置类型时,得到一个GetAZs。

要使用它,请将加载器类传递给load()

>>> yaml.load("!GetAZs us-east-1", Loader=Loader) GetAZs(region='us-east-1')

当然,你不需要存储这个值,你可以用它做一些事情--毕竟,这就是CloudFormation的作用。

KNOWN_AZS = { 'us-east-1': ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1e'], 'eu-west-1': ['eu-west-1a', 'eu-west-1b', 'eu-west-1c'], } def construct_GetAZs(loader, node): value = loader.construct_scalar(node) if value not in KNOWN_AZS: raise yaml.constructor.ConstructorError( None, None, f"GetAZs got unknown region {value!r}", node.start_mark ) return KNOWN_AZS[value] Loader.add_constructor('!GetAZs', construct_GetAZs)

>>> yaml.load("!GetAZs us-east-1", Loader=Loader) ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1e']

但我事先不知道这些标签 #

要使上述方法奏效,你需要为每个预期的标签注册构造函数。

但有时你并不事先知道标签,或者标签太多,或者你只想访问数据,而不关心它的含义(例如,因为你只想改变一个小东西并把它写回去)。

YAML 允许你为未知的标签注册一个万能的构造函数......但你仍然需要实现某种通用的包装器来配合它。

幸运的是,我已经写了一整篇关于如何做到这一点的文章,其中有完整的代码。

>>> yaml.load("!GetAZs us-east-1", Loader=Loader) Tagged('!GetAZs', 'us-east-1')

它适用于任意嵌套的YAML。

>>> value = yaml.load(""" ... Properties: ... ImageId: !FindInMap [RegionMap, !Ref 'AWS::Region', HVM64] ... """, Loader=Loader) >>> value { 'Properties': { 'ImageId': Tagged( '!FindInMap', ['RegionMap', Tagged('!Ref', 'AWS::Region'), 'HVM64'] ) } }

...允许你在大多数情况下忽略标签。

>>> value['Properties']['ImageId'][-1] = 'HVMG2'

...并且也可以输出有标签的YAML。

>>> print(yaml.dump(value, Dumper=Dumper)) Properties: ImageId: !FindInMap - RegionMap - !Ref 'AWS::Region' - HVMG2

看看吧。在Python中处理带有任意标签的YAML


就到这里吧。

**今天学到了什么新东西?**和别人分享一下吧,这真的很有帮助