【问题标题】:Return all keys along with value in nested dictionary返回嵌套字典中的所有键和值
【发布时间】:2016-07-29 23:07:58
【问题描述】:

我正在努力将多个 .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。

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

【问题讨论】:

    标签: python dictionary recursion pyyaml


    【解决方案1】:

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

    事实上,如果您将 sep 参数更改为 .,我认为最佳答案中的代码 sn-p 对您有用。

    编辑:

    根据链接的 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。

    【讨论】:

    • 超级感谢您的发送。我看了看,输出看起来很棒。不幸的是,我不得不离开我的电脑,但这个周末我会花更多的时间来浏览它。
    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会变得无效,甚至在指向其他 StackOverflow 答案的链接中也会发生这种情况。
    • 知道了@Anthon,我会修复它。
    【解决方案2】:

    遍历嵌套字典请求递归,并通过将“前缀”提交给“路径”,这可以防止您必须对路径的段进行任何操作(如 @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 保留了原始键顺序,但这当然取决于文件的处理顺序。如果您想要对路径进行排序,只需将 dump() 更改为使用:

            for path in sorted(self._result):
    

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


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

    【讨论】:

    • 这是一个经过深思熟虑的解决方案,可以解决我的输入可能遇到的许多边缘情况,谢谢。我特别喜欢它保留了 cmets。
    【解决方案3】:

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

    那么,每打一个冒号,对应的关键项就是列表中字符串的串联,类似于:

    '.'.join(key_list)
    

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

    【讨论】:

    • 智能设计。我必须离开电脑,所以现在无法尝试,但我喜欢这种方法,谢谢!
    • 使用字符串列表会使事情变得过于复杂。无论如何,您都必须使用递归来挖掘嵌套字典,因此只需输入当前路径“前缀”,递归就会处理您建议手动执行的追加/弹出操作。
    猜你喜欢
    • 2017-08-16
    • 2021-11-04
    • 2019-06-26
    • 1970-01-01
    • 2017-12-05
    • 2018-08-17
    • 1970-01-01
    • 1970-01-01
    • 2012-05-21
    相关资源
    最近更新 更多