【问题标题】:Can't merge YAMLs with comments无法将 YAML 与评论合并
【发布时间】:2017-09-21 09:01:05
【问题描述】:

我正在做一个工具来自动化一些工作,我需要将一些配置 YAML 合并到一个中,但我需要 cmets,因为我需要描述未来的字段。
我已经设法在没有 cmets 的情况下做到这一点,将 YAML 转换为 JSON,合并并再次转换为 YAML。我愿意使用 XML 或其他东西,因为我可以在本地运行它。有谁知道有什么可以帮助我的吗?

像这样:

文件 1

project:
    general:
        environment: ?

    databases:
        # Main Database
        db1:
            host: localhost
            username: root
            password: root123
            dbname: project
            logFile: ?

文件 2:

project:
    general:
        environment: local

    databases:
        db1:
            # New Log File
            logFile: project.log

会导致:

project:
    general:
        environment: local

    databases:
        # Main Database
        db1:
            host: localhost
            username: root
            password: root123
            dbname: project
            # New Log File
            logFile: project.log

【问题讨论】:

  • 您能否举例说明您需要合并的结构类型以及输出应该是什么样的?因为我假设您的意思是比一个接一个地放置项目更复杂。您当前使用 JSON 的代码示例也可能会有所帮助。
  • 用一个例子编辑了话题。
  • @PauloNeves 没有必要在您的帖子中添加“编辑:”。我们有这方面的编辑历史记录,因此您可以使用其他相关信息更新帖子,并将其保持为一个连贯的整体。

标签: javascript json node.js xml yaml


【解决方案1】:

你不能用普通的 YAML 实现来做到这一点,因为 YAML 定义了 cmets 是一个表示细节并且不能传达内容信息。因此,一旦您解析 YAML,您将自动丢失评论信息。

ruamel 提供ruamel.yaml.round_trip_load()。这为您提供了一个CommentedMap(如果您的 YAML 具有作为根类型的映射),它保留了所有 cmets。您可以按元素合并此类映射,然后再次将它们输出为 YAML。

根据您的 YAML 的布局,您也可以成功地在文本的基础上合并它们。例如,对于这样的两个 YAML 文件:

first.yaml:

foo: bar
spam: egg

秒:yaml:

sausage: spam
baked: beans

您可以像这样合并它们,只需在每行添加缩进并将它们连接起来:

first:
  foo: bar
  spam: egg
second:
  sausage: spam
  baked: beans

您只需遍历行并添加缩进即可。这适用于任何格式正确的输入 YAML 文件只要它们没有明确的指令或文档结束标记---...)。

如果您想在同一级别合并 YAML 文件,您仍然可以尝试连接它们,这在我的示例中效果很好:

foo: bar
spam: egg
sausage: spam
baked: beans

您也可以将它们连接到一个包含多个文档的 YAML 文件中,尽管我不确定这是否是您想要的:

foo: bar
spam: egg
...
---
sausage: spam
baked: beans

保证按照 YAML 规范工作。

【讨论】:

  • 将在 ramuel 上观看。我的主要问题是我正在处理一个现有项目,所以我不能做很多事情,这个 YAML 就像一个复杂的对象,里面有很多对象。您可以通过一个小示例在线程中查看我的编辑,因为我使用的文件更大。
  • @flyx 实际上 ruamel.yaml 会给你一个 CommentedMaps 和 CommentedSeqs 的树,并且 cmets 的关联不一定与这些结构的顶级之一。因此,如果您的映射包含具有 EOL cmets 的键值对,并且这些映射是序列中的项目并重新排列这些映射,您的 cmets 会自动显示在新的键值位置。
【解决方案2】:

正如@flyx 所指出的,您应该查看ruamel.yaml 的往返功能(免责声明:我是该软件包的作者),即使没有内置的递归合并并且有一些注意事项。

首先,您应该引用您的 ? 值,否则您将收到不允许映射键的警告(因为普通的 ? 通常会引入明确定义的映射键)。

同样重要的是要知道ruamel.yaml 中的 cmets 的关联往往与评论之前的最后一个解析节点有关。因此,在您的 file2.yaml 中,# New Log File 注释与前面的键 db1 相关联,而不与下面的 logFile 相关联。

如果您愿意像这样输入file1.yaml

project:
    general:
        environment: '?'

    databases:
        # Main Database
        db1:
            host: localhost
            username: root
            password: root123
            dbname: project
            logFile: '?'

file2.yaml 喜欢:

project:
    general:
        environment: local

    databases:
        db1:
            logFile: project.log   # New Log File

那么这个程序:

import sys
from pathlib import Path
import ruamel.yaml


def update(d, n):
    if isinstance(n, ruamel.yaml.comments.CommentedMap):
        for k in n:
            d[k] = update(d[k], n[k]) if k in d else n[k]
            if k in n.ca._items and n.ca._items[k][2] and \
               n.ca._items[k][2].value.strip():
                d.ca._items[k] = n.ca._items[k]  # copy non-empty comment
    else:
        d = n
    return d

data1 = ruamel.yaml.round_trip_load(Path('file1.yaml').read_text())
update(data1, ruamel.yaml.round_trip_load(Path('file2.yaml').read_text()))
ruamel.yaml.round_trip_dump(data1, sys.stdout)

足以给你以下输出:

project:
  general:
    environment: local

  databases:
        # Main Database
    db1:
      host: localhost
      username: root
      password: root123
      dbname: project
      logFile: project.log         # New Log File

请注意logFile: '?' 不必在file1.txt 中,因为缺少的键将在映射末尾添加。

如果将# New Log File 移动到密钥后的位置是不可接受的,那么您必须对从file2.yaml 加载的数据进行预处理,在这种情况下这并不难。这样做基于例如取决于原始file2.yaml 中的缩进是可能的,但是需要更多的代码行才能正确并且有点脆弱:

import sys
from pathlib import Path
import ruamel.yaml

INDENT=4

def update(d, n):
    if isinstance(n, ruamel.yaml.comments.CommentedMap):
        for k in n:
            d[k] = update(d[k], n[k]) if k in d else n[k]
            if k in n.ca._items and \
               ((n.ca._items[k][2] and n.ca._items[k][2].value.strip()) or \
                n.ca._items[k][1]):
                d.ca._items[k] = n.ca._items[k]  # copy non-empty comment
    else:
        d = n
    return d


def move_comment(d, depth=0):
    # recursively adjust comment
    if isinstance(d, ruamel.yaml.comments.CommentedMap):
        for k in d:
            if isinstance(d[k], ruamel.yaml.comments.CommentedMap):
                if hasattr(d, 'ca'):
                    comment = d.ca.items.get(k)
                    if comment and comment[3] is not None:
                        # add to first key of the mapping that is the value
                        for k1 in d[k]:
                            d[k].yaml_set_comment_before_after_key(
                                k1,
                                before=comment[3][0].value.lstrip('#').strip(),
                                indent=INDENT*(depth+1))
                            break
            move_comment(d[k], depth+1)
    return d

data1 = ruamel.yaml.round_trip_load(Path('file1.yaml').read_text())
update(data1, move_comment(ruamel.yaml.round_trip_load(Path('file2.yaml').read_text())))
ruamel.yaml.round_trip_dump(data1, sys.stdout, indent=INDENT)

上面给出了完全你要求的输出,正确的 ('?') file1.yaml 和你原来的file2.yaml

【讨论】:

  • ramuel.yaml 给了我一个想法,用 cmets 创建一个对象,扩展现有的 NodeJS 库之一来转换 YAML。如果它不起作用,将使用这个调用 py 脚本。非常感谢您的帮助和在该项目中所做的工作!
猜你喜欢
  • 1970-01-01
  • 2010-10-08
  • 2013-07-10
  • 2013-12-31
  • 1970-01-01
  • 2020-08-18
  • 1970-01-01
  • 2016-12-15
  • 2011-10-02
相关资源
最近更新 更多