当前位置: 首页 > 知识库问答 >
问题:

我想加载一个YAML文件,可能编辑数据,然后再次转储。如何保留格式?

林星阑
2023-03-14

这个问题试图以一种与语言无关的方式收集关于不同语言和YAML实现的问题的信息。

假设我有一个像这样的YAML文件:

first:
  - foo: {a: "b"}
  - "bar": [1, 2, 3]
second: |   # some comment
  some long block scalar value

我想将这个文件加载到本机数据结构中,可能更改或添加一些值,然后再次转储。但是,当我转储它时,原始格式将不被保留:

    null

共有1个答案

慕高阳
2023-03-14

前言:在整个回答中,我提到了一些流行的YAML实现。这些提及并不是详尽无遗的,因为我并不了解所有的YAML实现。

我将对数据结构使用YAML术语:原子文本内容(偶数)是一个标量。项序列,在别处称为数组或列表,是序列。键值对的集合(在别处称为字典或哈希)是一种映射。

如果您使用的是Python,请考虑使用ruamel(可能是从PyYAML切换过来的),因为它实现了到本机结构的往返,所以这个答案并不适用于它。

另一方面,注释会很快被丢弃,因为它们不属于事件或节点(这里的例外是ruamel,它将注释链接到下面的事件)。一些YAML实现(libyaml,SnakeYAML)提供了对令牌流的访问,令牌流甚至比事件树更低级。这个令牌流确实包含注释,但是它只能用于执行语法突出显示之类的操作,因为API不包含用于再次使用令牌流的方法

如果只需要加载YAML文件,然后再次转储它,请使用实现的一个较低级别的API,只加载YAML直到表示(节点图)或序列化(事件树)级别。要搜索的API函数分别是compose/parse和serialize/present。

最好使用事件树而不是节点图,因为一些实现在组合时已经忘记了映射键的原始顺序(由于内部使用了hashmaps)。例如,这个问题详细说明了使用SnakeyAML加载/转储事件。

"1 \x2B 1"

在解析转义序列后,此加载作为字符串“1+1”。即使在事件流中,关于转义序列的信息在我所知道的所有实现中都已经丢失了。事件只记得它是一个双引号标量,因此回写它将导致:

"1 + 1"

类似地,折叠的块标量(以开头)通常不会记住原始输入中的换行符在哪里折叠成了空格字符。

因此,总而言之,加载到事件树并再次转储通常会保留:

    null
  • 关于流标量中转义序列和换行符的信息
  • 缩进和非内容间距
  • 注释

如果使用节点图而不是事件树,则可能还会丢失映射中的键顺序。有些API(如go-yaml)不提供对事件树的访问,因此您别无选择,只能改用节点图。

如果您希望修改数据并保留原始格式,则需要在不将数据加载到本机结构的情况下对数据进行操作。这通常意味着您对标量、序列和映射进行操作,而不是像您可能习惯的对字符串、数字、列表或目标编程语言提供的任何结构进行操作。

  • 事件树通常作为事件流提供。它可能更适合大数据,因为您不需要在内存中加载完整的数据;相反,您检查每个事件,跟踪您在输入结构中的位置,并相应地放置您的修改。这个问题的答案说明了如何使用PyYAML的事件API向给定的YAML文件追加给出路径和值的项。
  • 节点图对于高度结构化的数据更好,如果您在YAML中使用锚和别名也是如此,因为它们是在那里解析的。与事件不同,在事件中您需要自己跟踪当前位置,这里的数据以完整的图形式呈现,您可以直接进入相关的部分(对于事件,您可能需要通过您根本不感兴趣的大型子结构)。

在任何情况下,您都需要了解一点关于YAML类型解析的知识,以便正确处理给定的数据。当您将YAML文件加载到声明的本机结构中时(在具有静态类型系统的语言中很常见,例如Java或Go),如果可能的话,YAML处理器将把YAML结构映射到它。但是,如果没有给出目标类型(在像Python或Ruby这样的脚本语言中很典型,但在Java中也有可能),则从节点内容和样式推导出类型。

由于我们不使用本机加载,因为我们需要保留格式信息,因此将不执行此类型解析。但是,您需要了解它在两种情况下是如何工作的:

  • 当您需要确定标量节点或事件的类型时,例如,您有一个内容为42的标量,并且需要知道它是字符串还是整数。
  • 当您需要创建一个新的事件或节点(以后应该作为特定类型加载)时。例如。如果追加字符串“42”,则必须确保以后不会将其作为整数42加载。

我不在这里讨论所有的细节;在大多数情况下,只要知道如果一个字符串被编码为标量,但看起来像其他东西(例如一个数字),那么您应该使用带引号的标量就足够了。

根据您的实现,您可能会接触到YAML标记。很少在YAML文件中使用(它们看起来像!!str!!map!!int等等),它们包含关于节点的类型信息,可以在具有异构数据的集合中使用。更重要的是,YAML定义了所有没有显式标记的节点都将被分配一个作为类型解析的一部分。这可能在节点图级别已经发生,也可能没有发生。因此,在节点数据中,即使原始节点没有节点标记,也可以看到节点的标记。

以两个感叹号开头的标记实际上是简写,例如!!strtag:yaml.org,2002:str的简写。您可能在您的数据中看到这两种情况,因为实现处理它们的方式完全不同。

对您来说重要的是,当您创建节点或事件时,您可能能够并且也可能需要分配一个标记。如果不希望输出包含显式标记,则使用非特定标记表示非纯标量,使用表示事件级别上的其他所有内容。在节点级别上,请参考实现的文档,了解是否需要提供解析的标记。如果不是,则适用与非特定标记相同的规则。如果文档中没有提到它(很少提到),就尝试一下。

总而言之:通过加载事件树或节点图来修改数据,在所获得的数据中添加、删除或修改事件或节点,然后将修改后的数据再次显示为YAML。根据您想要做什么,它可能会帮助您创建您想要添加到YAML文件中的数据作为本机结构,将其序列化到YAML中,然后作为节点图或事件树再次加载。从那里,您可以将它包含在您想要修改的YAML文件的结构中。

YAML尚未设计用于此任务。事实上,它被定义为一种序列化语言,假设您的数据是以某种编程语言的原生数据结构编写的,并从那里转储到YAML。然而,实际上,YAML被大量用于配置,这意味着您通常手工编写YAML并将其加载到本机数据结构中。

这种对比就是为什么在保留格式的同时修改YAML文件如此困难的原因:YAML格式已经被设计为瞬态数据格式,由一个应用程序编写,然后由另一个(或相同的)应用程序加载。在这个过程中,保留格式并不重要。但是,对于签入到版本控制的数据(您希望diff只包含实际更改过的数据行),以及手工编写YAML的其他情况,因为您希望保持样式一致,则可以这样做。

对于只更改给定YAML文件中的一个数据项而保持其他所有内容不变,没有完美的解决方案。加载一个YAML文件并不是给你一个YAML文件的视图,而是给你它所描述的内容。因此,所有不属于所描述内容的部分--最重要的是,注释和空白--都极难保存。

如果格式保存对您很重要,并且您不能忍受这个答案中的建议所作的折衷,那么YAML不是适合您的工具。

 类似资料: