Return 嵌套字典中的所有键和值

Return all keys along with value in nested dictionary

我正在努力将存在于几个 .yaml 文件中的所有文本放入一个新的单一 YAML 文件中,该文件将包含英语翻译,然后有人可以将其翻译成西班牙语。

每个 YAML 文件都有很多嵌套文本。我想为 YAML 文件中的每个值打印完整的 'path',也就是所有键以及值。这是位于 myproject.section.more_information 文件中的 .yaml 文件的示例输入:

default: 
    heading: Here’s A Title
    learn_more:
        title: Title of Thing
        url: www.url.com
        description: description
        opens_new_window: true

这是所需的输出:

myproject.section.more_information.default.heading: Here’s a Title
myproject.section.more_information.default.learn_more.title: Title of Thing
mproject.section.more_information.default.learn_more.url: www.url.com
myproject.section.more_information.default.learn_more.description: description
myproject.section.more_information.default.learn_more.opens_new_window: true

这似乎是一个很好的递归候选者,所以我查看了

等示例

但是,我想保留导致给定值的所有键,而不仅仅是值中的最后一个键。我目前正在使用 PyYAML 来 read/write YAML。

关于如何保存每个键的任何提示,因为我继续检查项目是否是字典,然后 return 与每个值关联的所有键?

您要做的是展平嵌套词典。这将是一个很好的起点:Flatten nested Python dictionaries, compressing keys

事实上,如果您将 sep 参数更改为 .,我认为顶部答案中的代码片段对您有用。

编辑:

检查这个基于链接的 SO 答案的工作示例 http://ideone.com/Sx625B

import collections

some_dict = {
    'default': {
        'heading': 'Here’s A Title',
        'learn_more': {
            'title': 'Title of Thing',
            'url': 'www.url.com',
            'description': 'description',
            'opens_new_window': 'true'
        }
    }
}

def flatten(d, parent_key='', sep='_'):
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, collections.MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

results = flatten(some_dict, parent_key='', sep='.')
for item in results:
    print(item + ': ' + results[item])

如果你想要它按顺序排列,你将需要一个 OrderedDict。

保留一个简单的字符串列表,作为每个缩进深度的最新键。当您从一行前进到下一行而没有任何变化时,只需更改列表末尾的项目即可。当您 "out-dent" 时,将最后一项从列表中弹出。缩进时,追加到列表中。

然后,每敲一个冒号,对应的键项就是列表中字符串的拼接,比如:

'.'.join(key_list)

这会让您以光荣的速度前进吗?

遍历嵌套字典需要递归,并且通过将 "prefix" 交给 "path" 这可以防止您必须对路径段进行任何操作(如@Prune 所建议的那样)。

有几点需要牢记,使这个问题变得有趣:

  • 因为您使用多个文件可能会导致多个文件中的路径相同,您需要处理(至少抛出一个错误,否则您可能会丢失数据)。在我的示例中,我生成了一个值列表。
  • 处理特殊键(非字符串(转换?)、空字符串、包含 . 的键)。我的示例报告这些并退出。

示例代码使用 ruamel.yaml ¹:

import sys
import glob
import ruamel.yaml
from ruamel.yaml.comments import CommentedMap, CommentedSeq
from ruamel.yaml.compat import string_types, ordereddict

class Flatten:
    def __init__(self, base):
        self._result = ordereddict() # key to list of tuples of (value, comment)
        self._base = base

    def add(self, file_name):
        data = ruamel.yaml.round_trip_load(open(file_name))
        self.walk_tree(data, self._base)

    def walk_tree(self, data, prefix=None):
        """
        this is based on ruamel.yaml.scalarstring.walk_tree
        """
        if prefix is None:
            prefix = ""
        if isinstance(data, dict):
            for key in data:
                full_key = self.full_key(key, prefix)
                value = data[key]
                if isinstance(value, (dict, list)):
                    self.walk_tree(value, full_key)
                    continue
                # value is a scalar
                comment_token = data.ca.items.get(key)
                comment = comment_token[2].value if comment_token else None
                self._result.setdefault(full_key, []).append((value, comment))
        elif isinstance(base, list):
            print("don't know how to handle lists", prefix)
            sys.exit(1)

    def full_key(self, key, prefix):
        """
        check here for valid keys
        """
        if not isinstance(key, string_types):
            print('key has to be string', repr(key), prefix)
            sys.exit(1)
        if '.' in key:
            print('dot in key not allowed', repr(key), prefix)
            sys.exit(1)
        if key == '':
            print('empty key not allowed', repr(key), prefix)
            sys.exit(1)
        return prefix + '.' + key

    def dump(self, out):
        res = CommentedMap()
        for path in self._result:
            values = self._result[path]
            if len(values) == 1: # single value for path
                res[path] = values[0][0]
                if values[0][1]:
                    res.yaml_add_eol_comment(values[0][1], key=path)
                continue
            res[path] = seq = CommentedSeq()
            for index, value in enumerate(values):
                seq.append(value[0])
                if values[0][1]:
                    res.yaml_add_eol_comment(values[0][1], key=index)


        ruamel.yaml.round_trip_dump(res, out)


flatten = Flatten('myproject.section.more_information')
for file_name in glob.glob('*.yaml'):
    flatten.add(file_name)
flatten.dump(sys.stdout)

如果您有额外的输入文件:

default:
    learn_more:
        commented: value  # this value has a comment
        description: another description

那么结果是:

myproject.section.more_information.default.heading: Here’s A Title
myproject.section.more_information.default.learn_more.title: Title of Thing
myproject.section.more_information.default.learn_more.url: www.url.com
myproject.section.more_information.default.learn_more.description:
- description
- another description
myproject.section.more_information.default.learn_more.opens_new_window: true
myproject.section.more_information.default.learn_more.commented: value  # this value has a comment

当然,如果您的输入没有双路径,您的输出将不会有任何列表。

通过使用 ruamel.yaml 中的 string_typesordereddict 使 Python2 和 Python3 兼容(您没有指明您使用的是哪个版本) .

ordereddict保留了原来的key排序,当然这取决于文件的处理顺序。如果要对路径进行排序,只需将 dump() 更改为使用:

        for path in sorted(self._result):

另请注意,'commented' 词典条目上的注释已保留。


¹ ruamel.yaml 是一个 YAML 1.2 解析器,它在往返过程中保留注释和其他数据(PyYAML 完成 YAML 1.1 的大部分工作)。免责声明:我是 ruamel.yaml

的作者