如何区分YAML中内容相同但锚点来源不同的引用节点
在YAML中,我们可以使用引用机制:通过&mynode定义节点锚点,再通过*mynode将被锚定的节点内容复制到多个位置使用。在实际场景中,可能存在不同锚点对应节点内容完全一致的情况(例如两个同名的不同人物),但我们仍需要判断两处内容是否来自同一个锚点引用。例如参考如下YAML示例:
people: - &HarryPotter - firstName: Harry - lastName: Potter - &HermioneGranger - firstName: Hermione - lastName: Granger - &HarryPotterTwin - firstName: Harry - lastName: Potter books: HarryPotter1: - *HarryPotter - *HermioneGranger HarryPotter2: - *HarryPotterTwin - *HermioneGranger
可以看到,第一本书中出现的哈利·波特和第二本书中的哈利·波特并非同一个实体,因为二者引用的锚点不同(分别为&HarryPotter和&HarryPotterTwin)。因此即使二者内容完全一致(姓名相同),我们也需要能够判断它们是否实际指向同一个实体。但遗憾的是,默认加载解析后对两个节点做相等性判断时,会返回内容相同的结果:
>>> stream = open("a.yml") >>> data = yaml.safe_load(stream) >>> data {'people': [[{'firstName': 'Harry'}, {'lastName': 'Potter'}], [{'firstName': 'Hermione'}, {'lastName': 'Granger'}], [{'firstName': 'Harry'}, {'lastName': 'Potter'}]], 'books': {'HarryPotter1': [[{'firstName': 'Harry'}, {'lastName': 'Potter'}], [{'firstName': 'Hermione'}, {'lastName': 'Granger'}]], 'HarryPotter2': [[{'firstName': 'Harry'}, {'lastName': 'Potter'}], [{'firstName': 'Hermione'}, {'lastName': 'Granger'}]]}} >>> data["books"]["HarryPotter1"][1] == data["books"]["HarryPotter2"][1] True
请问是否存在可行方案(以Python为例,也支持JavaScript等其他语言),可以判断两个通过引用填充的节点是否实际指向同一个原始锚点节点?
备注:我希望避免手动新增ID字段这类方案,因为锚点本身就应当作为身份标识符;同时也不希望完全放弃引用机制、手动编写节点ID路径再自行解析,这类方案额外成本过高。
Python(PyYAML)实现方案
默认yaml.safe_load的问题在于构造别名节点时会递归生成全新的Python对象,既没有保留锚点元信息,也没有保留对象引用关系,所以只能判断值相等,无法区分内容相同但锚点不同的节点。
不需要修改原始YAML内容、不需要手动添加ID字段,只要自定义加载器重写锚点和别名的处理逻辑即可:
- 遇到锚点定义时,将锚点名和生成的Python对象存入映射表,同时给对象附加锚点元信息
- 遇到别名引用时,直接返回映射表中存储的原对象,不生成新的拷贝对象
实现代码如下:
import yaml from yaml.reader import Reader from yaml.scanner import Scanner from yaml.parser import Parser from yaml.composer import Composer from yaml.constructor import SafeConstructor from yaml.resolver import Resolver class AnchorTrackingLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): def __init__(self, stream): Reader.__init__(self, stream) Scanner.__init__(self) Parser.__init__(self) Composer.__init__(self) SafeConstructor.__init__(self) Resolver.__init__(self) self.anchor_map = {} def construct_yaml_map(self, node): data = {} if node.anchor: data.__anchor__ = node.anchor self.anchor_map[node.anchor] = data yield data value = self.construct_mapping(node) data.update(value) def construct_yaml_seq(self, node): data = [] if node.anchor: data.__anchor__ = node.anchor self.anchor_map[node.anchor] = data yield data data.extend(self.construct_sequence(node)) AnchorTrackingLoader.add_constructor( 'tag:yaml.org,2002:map', AnchorTrackingLoader.construct_yaml_map ) AnchorTrackingLoader.add_constructor( 'tag:yaml.org,2002:seq', AnchorTrackingLoader.construct_yaml_seq ) # 加载文件 with open("a.yml", "r") as f: data = yaml.load(f, Loader=AnchorTrackingLoader)
解析完成后有两种判断方式:
- 直接用Python原生的
is运算符判断:同一个锚点引用的节点是同一个Python对象实例,不同锚点的节点即使内容完全一致也是不同实例print(data["books"]["HarryPotter1"][0] is data["people"][0]) # True,同属HarryPotter锚点 print(data["books"]["HarryPotter2"][0] is data["people"][0]) # False,属于HarryPotterTwin锚点 print(data["books"]["HarryPotter1"][1] is data["books"]["HarryPotter2"][1]) # True,同属HermioneGranger锚点 - 直接读取对象上附加的
__anchor__属性获取锚点名做判断print(data["books"]["HarryPotter1"][0].__anchor__) # 输出 HarryPotter print(data["books"]["HarryPotter2"][0].__anchor__) # 输出 HarryPotterTwin
注意:因为同锚点引用指向同一个Python对象,如果你后续修改其中一个节点的内容,所有同锚点的节点都会同步变更。如果需要独立修改某一处节点的内容,提前对该节点做copy.deepcopy即可。
JavaScript(js-yaml)实现思路
js-yaml提供了锚点处理的回调钩子,逻辑和Python方案完全一致:在解析时通过回调记录锚点和生成对象的映射关系,处理别名时直接返回原对象引用,不生成新副本。解析完成后直接用===严格相等即可判断两个节点是否来自同一个锚点,不需要额外修改YAML内容。
内容的提问来源于stack exchange,提问作者tobiasBora

