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

问题描述

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

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

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

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

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

标签: formattingyaml

解决方案


前言:在这个答案中,我提到了一些流行的 YAML 实现。这些提及从来都不是详尽无遗的,因为我不知道所有的 YAML 实现。

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

如果您使用 Python,使用ruamel将帮助您保留相当多的格式,因为它实现了到本机结构的往返。但是,它并不完美,不能保留所有格式。

背景

加载 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 (Node Graph)的反序列化,这可能是由于它们的实现语言的限制(加载到本机数据结构中需要对类型进行编译时或运行时反射)。

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

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

另一方面,评论很早就被丢弃,因为它们不属于事件或节点(这里的例外是 ruamel 将评论链接到以下事件,而 go-yaml 会记住创建行之前、之后和之后的评论一个节点)。一些 YAML 实现(libyaml、SnakeYAML)提供对比事件树更底层的令牌流的访问。此令牌流确实包含注释,但它仅可用于执行语法高亮等操作,因为 API 不包含再次使用令牌流的方法。

那么该怎么办?

装卸

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

最好使用事件树而不是节点图,因为某些实现在编写时已经忘记了映射键的原始顺序(由于内部使用哈希图)。例如,这个问题详细介绍了使用 SnakeYAML 加载/转储事件。

在您的实现的事件流中已经丢失的信息,例如大多数实现中的注释,是不可能保存的。同样不可能保留的是标量布局,就像在这个例子中一样:

"1 \x2B 1"

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

"1 + 1"

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

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

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

你通常会失去:

  • 有关流标量中的转义序列和换行符的信息
  • 缩进和非内容间距
  • 注释——除非实现特别支持将它们放在事件和/或节点中

如果您使用节点图而不是事件树,您可能会丢失锚表示(即,稍后&foo可能会写出&a所有引用它的别名,*a而不是使用*foo)。您还可能会丢失映射中的键顺序。一些 API,比如 go-yaml,不提供对事件树的访问,所以你别无选择,只能使用节点图

修改数据

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

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

  • 事件树通常作为事件流提供。对于大数据可能会更好,因为您不需要将完整的数据加载到内存中;相反,您检查每个事件,跟踪您在输入结构中的位置,并相应地进行修改。这个问题的答案显示了如何使用 PyYAML 的事件 API 将提供路径和值的项目附加到给定的 YAML 文件。
  • 节点图更适合高度结构化的数据。如果您使用锚点和别名,它们将在那里被解析,但您可能会丢失有关它们名称的信息(如上所述)。与事件不同,您需要自己跟踪当前位置,数据在此处显示为完整图表,您可以直接进入相关部分。

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

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

  • 当您需要决定标量节点或事件的类型时,例如,您有一个带有内容的标量,42并且需要知道它是字符串还是整数
  • 当您需要创建稍后应作为特定类型加载的新事件或节点时。例如,如果您创建一个包含 的标量42,您可能想要控制稍后将其加载为整数 42还是字符串。 "42"

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

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

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

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

总结一下:您通过加载Event TreeNode Graph来修改数据,添加、删除或修改所获得数据中的事件或节点,然后再次修改后的数据显示为 YAML。根据您要执行的操作,它可能会帮助您创建要添加到 YAML 文件中的数据作为本机结构,将其序列化为 YAML,然后将其作为Node GraphEvent Tree再次加载。从那里,您可以将其包含在要修改的 YAML 文件的结构中。

结论 / TL;DR

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

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

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

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


推荐阅读