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

fly*_*lyx 6 formatting yaml

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

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

first:
  - foo: {a: "b"}
  - "bar": [1, 2, 3]
second: |   # some comment
  some long block scalar value
Run Code Online (Sandbox Code Playgroud)

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

  • 标量的格式不同,例如"b"丢失引号,的值second不再是文字块标量等。
  • 集合的格式不同,例如 的映射值foo以块样式而不是给定的流样式"bar"写入,类似地, 的序列值以块样式写入
  • 映射键的顺序(例如first/ second)改变
  • 评论没了
  • 缩进级别不同,例如 中的项目first不再缩进。

如何保留原始文件的格式?

fly*_*lyx 15

前言:在整个答案中,我提到了一些流行的 YAML 实现。这些提及永远不会详尽,因为我不知道那里的所有 YAML 实现。

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

如果您使用的是 Python,请考虑使用ruamel(可能从 PyYAML 切换),因为它实现了到本机结构的往返,并且此答案的大部分内容不适用于它。

背景

加载 YAML 的过程也是一个丢失信息的过程。让我们看一下规范中给出的加载/转储 YAML 的过程:

当您加载 YAML 文件时,您将在加载方向上执行部分或全部步骤,从Presentation (Character Stream) 开始。YAML 实现通常会提升其最高级别的 API,这些 API 将 YAML 文件一直加载到Native (Data Structure)。这适用于大多数常见的 YAML 实现,例如 PyYAML/ruamel、SnakeYAML、go-yaml 和 Ruby 的 YAML 模块。其他实现,如 libyaml 和 yaml-cpp,由于其实现语言的限制,只提供了直到Representation(节点图)的反序列化。

对我们来说重要的信息是那些盒子里的东西。每个框都提到了在剩下的框中不再可用的信息。所以这意味着,根据 YAML 规范,样式注释仅存在于实际的 YAML 文件内容中,但在解析YAML 文件后立即被丢弃. 对您而言,这意味着一旦您将 YAML 文件加载到本机数据结构中,所有有关它在输入文件中的原始外观的信息都将消失。这意味着,当您转储数据时,YAML 实现会选择它认为对您的数据有用的表示形式。一些实现让您提供一般提示/选项,例如所有标量都应该被引用,但这并不能帮助您恢复原始格式。

值得庆幸的是,这张图只描述了加载 YAML的逻辑过程;符合 YAML 的实现不需要严格遵守它。大多数实现实际上保存数据的时间比他们需要的要长。这适用于 PyYAML/ruamel、SnakeYAML、go-yaml、yaml-cpp、libyaml 等。在所有这些实现中,标量、序列和映射的样式一直被记住,直到表示(节点图)级别。

另一方面,评论会很快被丢弃,因为它们不属于事件或节点(这里的例外是 ruamel,它将评论链接到以下事件)。一些 YAML 实现(libyaml、SnakeYAML)提供对令牌流的访问,该流甚至比Event Tree更底层。此令牌流确实包含注释,但它仅可用于执行语法突出显示等操作,因为 API 不包含再次使用令牌流的方法。

那么该怎么办?

装卸

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

最好使用事件树而不是节点图,因为一些实现在组合. 例如,这个问题详细说明了使用 SnakeYAML 加载/转储事件。

由于注释在早期就被废弃了,除非您想分叉现有的 YAML 实现并修补它以保留注释(就像 ruamel 用 PyYAML 做的那样。go-yaml (v3) associates节点图中带有节点的注释,因此您有机会在某种程度上访问和保留它们。

另请注意,保持风格并不完美,也不能真正做到。例如,以这个标量为例:

"1 \x2B 1"
Run Code Online (Sandbox Code Playgroud)

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

"1 + 1"
Run Code Online (Sandbox Code Playgroud)

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

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

  • 样式:未引用/引用/块标量,流/块集合(序列和映射)
  • 映射中的键顺序
  • YAML 标签

你通常会失去:

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

如果您使用Node Graph而不是Event Tree,您可能还会丢失映射中的键顺序。某些 API(如 go-yaml)不提供对Event Tree 的访问,因此您别无选择,只能使用Node Graph

修改数据

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

您可以选择处理事件树节点图(假设您的 API 允许您访问它)。哪个更好通常取决于您想要做什么:

  • 事件树通常作为事件的流提供。对于大数据来说可能更好,因为您不需要在内存中加载完整的数据;相反,您检查每个事件,跟踪您在输入结构中的位置,并相应地进行修改。这个问题的答案显示了如何使用 PyYAML 的事件 API 将提供路径和值的项目附加到给定的 YAML 文件。
  • 节点图是高度结构化的数据更好,而且如果你用锚和别名在您的YAML因为他们解决了那里。与事件不同,您需要自己跟踪当前位置,数据在此处显示为完整的图形,您可以直接进入相关部分(对于事件,您可能需要通过管道通过您不感兴趣的大型子结构)全部)。

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

由于我们没有使用原生加载,因为我们需要保留格式信息,因此不会执行这种类型解析。但是,您需要知道它在两种情况下是如何工作的:

  • 当您需要决定标量节点或事件的类型时,例如您有一个包含内容的标量42并且需要知道它是string还是integer
  • 当您需要创建稍后应作为特定类型加载的新事件或节点时。例如,如果您附加string "42",则必须确保它稍后不会作为整数 加载42

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

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

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

对您来说重要的是,当您创建节点或事件时,您可能能够也可能需要分配标签。如果您不希望输出包含显式标签,请!对非普通标量和?事件级别的其他所有内容使用非特定标签。在节点级别,请查阅您的实现文档,了解您是否需要提供已解析的标签。如果不是,则适用非特定标签的相同规则。如果文档没有提到它(很少有人提到),请尝试一下。

所以总结一下:你修改将数据装入无论是事件树节点图,可以添加,删除或修改事件或节点在你得到的数据,然后你提出再次修改后的数据作为YAML。根据您想要做什么,它可以帮助您创建要添加到 YAML 文件的数据作为本机结构,将其序列化为 YAML,然后将其作为Node GraphEvent Tree再次加载。从那里,您可以将其包含在要修改的 YAML 文件的结构中。

结论 / TL;DR

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

这种对比就是为什么在保留格式的同时修改 YAML 文件如此困难的原因:YAML 格式被设计为瞬态数据格式,由一个应用程序编写,然后由另一个(或相同的)应用程序加载。在该过程中,保留格式无关紧要。但是,它确实适用于签入到版本控制的数据(您希望差异仅包含您实际更改的数据的行),以及您手动编写 YAML 的其他情况,因为您想要保持风格一致。

没有完美的解决方案可以只更改给定 YAML 文件中的一个数据项,而让其他所有内容保持不变。加载 YAML 文件不会让您查看 YAML 文件,它会为您提供它描述的内容。因此,不属于所描述内容的所有内容——最重要的是,评论和空格——都极难保留。

如果格式保留对您很重要,并且您无法忍受此答案中的建议所做的妥协,那么 YAML 不是适合您的工具。