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

Posted

技术标签:

【中文标题】我想加载一个 YAML 文件,可能编辑数据,然后再次转储。如何保留格式?【英文标题】:I want to load a YAML file, possibly edit the data, and then dump it again. How can I preserve formatting? 【发布时间】:2020-07-08 11:56:35 【问题描述】:

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

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

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

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

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

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

【问题讨论】:

【参考方案1】:

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

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

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

背景

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

当您加载 YAML 文件时,您正在执行 加载 方向的部分或全部步骤,从 Presentation (Character Stream) 开始。 YAML 实现通常会提升其***别的 API,这些 API 将 YAML 文件一直加载到 Native(数据结构)。对于大多数常见的 YAML 实现来说都是如此,例如PyYAML/ruamel、SnakeYAML、go-yaml 和 Ruby 的 YAML 模块。其他实现,例如 libyaml 和 yaml-cpp,仅提供 Representation (Node Graph) 的反序列化,可能是由于其实现语言的限制(加载到本机数据结构需要编译时或类型的运行时反射)。

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

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

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

那该怎么办?

装卸

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

最好使用 Event Tree 而不是 Node Graph,因为当 em>作曲。 This question,例如,详细使用 SnakeYAML 加载/转储事件。

在您的实现的事件流中已经丢失的信息(例如大多数实现中的 cmets)是不可能保存的。标量布局也无法保留,如下例所示:

"1 \x2B 1"

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

"1 + 1"

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

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

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

你通常会输:

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

如果您使用 Node Graph 而不是 Event Tree,您可能会丢失锚表示(即 &foo 稍后可能会写成 &a使用*a而不是*foo引用它的所有别名)。您还可能会丢失映射中的键顺序。一些 API,如 go-yaml,不提供对 Event Tree 的访问,因此您别无选择,只能使用 Node Graph

修改数据

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

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

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

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

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

当您需要决定标量节点或事件的类型时,例如你有一个内容为 42 的标量,需要知道它是 string 还是 integer。 当您需要创建稍后应作为特定类型加载的新事件或节点时。例如。如果您创建一个包含 42 的标量,您可能希望控制它是稍后加载为 integer 42 还是 string "42"

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

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

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

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

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

结论/TL;DR

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

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

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

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

【讨论】:

以上是关于我想加载一个 YAML 文件,可能编辑数据,然后再次转储。如何保留格式?的主要内容,如果未能解决你的问题,请参考以下文章

python小脚本Yaml配置文件动态加载

15.资源加载器,根据配置文件自动加载文件

发生 gpload 控制文件处理错误。条目必须是 YAML 序列

在 markdown 文件中编辑 YAML Frontmatter

Node.js写入YAML文件

apk编辑器怎么用