返回嵌套字典中的所有键和值

时间:2016-07-29 23:07:58

标签: python dictionary recursion pyyaml

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

每个YAML文件都有很多嵌套文本。我想为YAML文件中的每个值打印完整的路径',也就是所有键以及值。以下是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

这似乎是递归的一个很好的候选者,因此我查看了this answer

等示例

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

有关如何保存每个密钥的任何提示,我继续检查该项是否为字典,然后返回与每个值关联的所有密钥?

3 个答案:

答案 0 :(得分:1)

您想要做的是拼合嵌套词典。这将是一个很好的起点: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。

答案 1 :(得分:0)

保留一个简单的字符串列表,作为每个缩进深度的最新键。当您从一行进展到下一行而没有任何变化时,只需更改列表末尾的项目即可。当您“失败”时,弹出列表中的最后一项。缩进时,请附加到列表中。

然后,每次敲击冒号时,相应的键项是列表中字符串的串联,如:

'.'.join(key_list)

这会让你以一种光荣的速度前进吗?

答案 2 :(得分:0)

遍历嵌套字典需要递归,并通过将“前缀”交给“路径”,这可以防止您对路径的各个部分进行任何操作(如@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

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

使用string_types中的ordereddictruamel.yaml使得此Python2和Python3兼容(您没有指明您使用的是哪个版本)。

ordereddict保留原始密钥排序,但这当然取决于文件的处理顺序。如果您希望对路径进行排序,只需将dump()更改为使用:

        for path in sorted(self._result):

另请注意,“已评论”字典条目的注释会被保留。

¹ ruamel.yaml是一个YAML 1.2解析器,它保存有关往返的注释和其他数据(PyYAML可以处理YAML 1.1的大部分内容)。免责声明:我是ruamel.yaml的作者