所以你试图用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
就到这里吧。
**今天学到了什么新东西?**和别人分享一下吧,这真的很有帮助