在Python中,如何将YAML映射加载为OrderedDicts?


Answers:


147

更新:在python 3.6+中OrderedDict,由于新的dict实现已经在pypy中使用了一段时间(尽管现在考虑了CPython实现的细节),您可能根本不需要。

更新:在python 3.7+中,dict对象的插入顺序保留性质已声明为Python语言规范的正式组成部分,请参阅Python 3.7的新增功能

我喜欢@James的简单解决方案。但是,它更改了默认的全局yaml.Loader类,这可能导致麻烦的副作用。特别是在编写库代码时,这是一个坏主意。另外,它不能直接与使用yaml.safe_load()

幸运的是,无需付出太多努力即可改进该解决方案:

import yaml
from collections import OrderedDict

def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict):
    class OrderedLoader(Loader):
        pass
    def construct_mapping(loader, node):
        loader.flatten_mapping(node)
        return object_pairs_hook(loader.construct_pairs(node))
    OrderedLoader.add_constructor(
        yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
        construct_mapping)
    return yaml.load(stream, OrderedLoader)

# usage example:
ordered_load(stream, yaml.SafeLoader)

对于序列化,我不知道明显的概括,但是至少这应该没有任何副作用:

def ordered_dump(data, stream=None, Dumper=yaml.Dumper, **kwds):
    class OrderedDumper(Dumper):
        pass
    def _dict_representer(dumper, data):
        return dumper.represent_mapping(
            yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
            data.items())
    OrderedDumper.add_representer(OrderedDict, _dict_representer)
    return yaml.dump(data, stream, OrderedDumper, **kwds)

# usage:
ordered_dump(data, Dumper=yaml.SafeDumper)

3
+1-非常感谢您,这为我节省了很多麻烦。
Nobilis 2014年

2
此实现会破坏YAML合并标签BTW
Randy

1
@兰迪谢谢。我以前没有在那种情况下运行过,但是现在我也添加了一个修复程序来处理此问题(我希望如此)。
Coldfix

9
@ArneBabenhauserheide我不确定PyPI是否足够上游,但是如果您认为ruamel.yaml(我是它的作者),请看一下。
Anthon 2015年

1
@Anthon您的ruamel.yaml库运行很好。感谢那。
Jan Vlcinsky

56

yaml模块允许您指定自定义“表示器”以将Python对象转换为文本,并指定“构造器”以逆转该过程。

_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG

def dict_representer(dumper, data):
    return dumper.represent_dict(data.iteritems())

def dict_constructor(loader, node):
    return collections.OrderedDict(loader.construct_pairs(node))

yaml.add_representer(collections.OrderedDict, dict_representer)
yaml.add_constructor(_mapping_tag, dict_constructor)

5
这个答案有什么解释吗?
舒曼(Shuman)

1
甚至更好from six import iteritems,然后将其更改为iteritems(data)使其在Python 2和3中同样有效
。– Midnighter

5
这似乎正在使用PyYAML(represent_dictDEFAULT_MAPPING_TAG)的未记录功能。是因为文档不完整,还是不支持这些功能并且可能随时更改恕不另行通知?
aldel

3
请注意,dict_constructor您需要致电loader.flatten_mapping(node)否则您将无法加载<<: *...(合并语法)
Anthony Sottile

@ brice-m-dempsey您可以添加任何示例如何使用您的代码吗?在我看来,它似乎不起作用(Python 3.7)
schaffe

53

2018年选项:

oyamlPyYAML的直接替代品,保留了字典排序。同时支持Python 2和Python 3。只需pip install oyaml导入,如下所示:

import oyaml as yaml

在转储/加载时,您将不再为搞砸的映射而烦恼。

注意:我是oyaml的作者。


1
这次真是万分感谢!由于某些原因,即使使用Python 3.8,PyYaml也不遵守该顺序。oyaml立即为我解决了这个问题。
约翰·史密斯

26

2015(及更高版本)选项:

ruamel.yaml是PyYAML的替代品(免责声明:我是该软件包的作者)。保留映射的顺序是2015年在第一版(0.1)中添加的内容之一。它不仅保留字典的顺序,还保留注释,锚点名称,标签并支持YAML 1.2规范(2009年发布)

规范说,不能保证排序,但是YAML文件中当然有排序,并且适当的解析器可以仅保留该排序器,并透明地生成一个保持排序的对象。您只需要选择正确的解析器,加载器和转储器¹:

import sys
from ruamel.yaml import YAML

yaml_str = """\
3: abc
conf:
    10: def
    3: gij     # h is missing
more:
- what
- else
"""

yaml = YAML()
data = yaml.load(yaml_str)
data['conf'][10] = 'klm'
data['conf'][3] = 'jig'
yaml.dump(data, sys.stdout)

会给你:

3: abc
conf:
  10: klm
  3: jig       # h is missing
more:
- what
- else

dataCommentedMap类似dict 的类型,但具有额外的信息,这些信息会一直保留直到被转储(包括保留的注释!)


如果您已经有一个YAML文件,那很好,但是如何使用Python结构做到这一点呢?我尝试CommentedMap直接使用它,但是它不起作用,并且OrderedDict!!omap其放置在每个地方都不是很友好。
Holt

我不确定为什么CommentedMap对您不起作用。您可以使用(最小化的)代码发布问题并将其标记为ruamel.yaml吗?这样,我将收到通知并回答。
Anthon

抱歉,我认为这是因为我试图将CommentedMapwith 保存为safe=Truein YAML,但该方法不起作用(使用safe=False作品)。我也遇到CommentedMap无法修改的问题,但现在无法重现...如果再次遇到此问题,我将打开一个新问题。
Holt

您应该使用yaml = YAML(),您将获得往返解析器/自卸车,这是派生安全分析器/自卸车,它知道CommentedMap /序列等
安通

14

注意:有一个基于以下答案的库,该库还实现了CLoader和CDumpers:Phynix / yamlloader

我非常怀疑这是最好的方法,但这是我想出的方法,并且确实有效。也可作为要点

import yaml
import yaml.constructor

try:
    # included in standard lib from Python 2.7
    from collections import OrderedDict
except ImportError:
    # try importing the backported drop-in replacement
    # it's available on PyPI
    from ordereddict import OrderedDict

class OrderedDictYAMLLoader(yaml.Loader):
    """
    A YAML loader that loads mappings into ordered dictionaries.
    """

    def __init__(self, *args, **kwargs):
        yaml.Loader.__init__(self, *args, **kwargs)

        self.add_constructor(u'tag:yaml.org,2002:map', type(self).construct_yaml_map)
        self.add_constructor(u'tag:yaml.org,2002:omap', type(self).construct_yaml_map)

    def construct_yaml_map(self, node):
        data = OrderedDict()
        yield data
        value = self.construct_mapping(node)
        data.update(value)

    def construct_mapping(self, node, deep=False):
        if isinstance(node, yaml.MappingNode):
            self.flatten_mapping(node)
        else:
            raise yaml.constructor.ConstructorError(None, None,
                'expected a mapping node, but found %s' % node.id, node.start_mark)

        mapping = OrderedDict()
        for key_node, value_node in node.value:
            key = self.construct_object(key_node, deep=deep)
            try:
                hash(key)
            except TypeError, exc:
                raise yaml.constructor.ConstructorError('while constructing a mapping',
                    node.start_mark, 'found unacceptable key (%s)' % exc, key_node.start_mark)
            value = self.construct_object(value_node, deep=deep)
            mapping[key] = value
        return mapping

如果您想key_node.start_mark在错误消息中包含该属性,我看不出任何明显的方法可以简化您的中央构造循环。如果尝试利用OrderedDict构造函数将接受键,值对的可迭代项的事实,则在生成错误消息时,您将无法访问该详细信息。
ncoghlan 2011年

有人正确测试过此代码吗?我无法在我的应用程序中使用它!
theAlse

用法示例:ordered_dict = yaml.load('''b:1 a:2''',Loader = OrderedDictYAMLLoader)#ordereddict = OrderedDict([('b',1),('a',2)])不幸的是我对帖子的编辑被拒绝,因此请原谅缺少格式。
Panic Panic

此实现中断了有序映射类型的加载。为了解决这个问题,你可以删除第二个呼叫add_constructor你的__init__方法。
瑞安

10

更新:不赞成使用该库,而推荐使用yamlloader(它基于yamlordereddictloader)

我刚刚找到了一个Python库(https://pypi.python.org/pypi/yamlordereddictloader/0.1.1),该库是基于此问题的答案而创建的,使用起来非常简单:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)

我不知道是否是同一作者,但请查看yodlgithub。
B先生

3

在针对Python 2.7的For PyYaml安装中,我更新了__init __。py,constructor.py和loader.py。现在支持用于加载命令的object_pairs_hook选项。我所做的更改差异如下。

__init__.py

$ diff __init__.py Original
64c64
< def load(stream, Loader=Loader, **kwds):
---
> def load(stream, Loader=Loader):
69c69
<     loader = Loader(stream, **kwds)
---
>     loader = Loader(stream)
75c75
< def load_all(stream, Loader=Loader, **kwds):
---
> def load_all(stream, Loader=Loader):
80c80
<     loader = Loader(stream, **kwds)
---
>     loader = Loader(stream)

constructor.py

$ diff constructor.py Original
20,21c20
<     def __init__(self, object_pairs_hook=dict):
<         self.object_pairs_hook = object_pairs_hook
---
>     def __init__(self):
27,29d25
<     def create_object_hook(self):
<         return self.object_pairs_hook()
<
54,55c50,51
<         self.constructed_objects = self.create_object_hook()
<         self.recursive_objects = self.create_object_hook()
---
>         self.constructed_objects = {}
>         self.recursive_objects = {}
129c125
<         mapping = self.create_object_hook()
---
>         mapping = {}
400c396
<         data = self.create_object_hook()
---
>         data = {}
595c591
<             dictitems = self.create_object_hook()
---
>             dictitems = {}
602c598
<             dictitems = value.get('dictitems', self.create_object_hook())
---
>             dictitems = value.get('dictitems', {})

loader.py

$ diff loader.py Original
13c13
<     def __init__(self, stream, **constructKwds):
---
>     def __init__(self, stream):
18c18
<         BaseConstructor.__init__(self, **constructKwds)
---
>         BaseConstructor.__init__(self)
23c23
<     def __init__(self, stream, **constructKwds):
---
>     def __init__(self, stream):
28c28
<         SafeConstructor.__init__(self, **constructKwds)
---
>         SafeConstructor.__init__(self)
33c33
<     def __init__(self, stream, **constructKwds):
---
>     def __init__(self, stream):
38c38
<         Constructor.__init__(self, **constructKwds)
---
>         Constructor.__init__(self)

这实际上应该添加到上游。
Michael

1
Justed提出了您的更改的请求请求。github.com/yaml/pyyaml/pull/12让我们希望合并。
迈克尔

真的希望作者更加积极,最后一次提交是在4年前。这种变化对我来说是天赐之物。
Mark LeMoine

-1

这是一个简单的解决方案,还可以检查地图中是否有重复的顶级键。

import yaml
import re
from collections import OrderedDict

def yaml_load_od(fname):
    "load a yaml file as an OrderedDict"
    # detects any duped keys (fail on this) and preserves order of top level keys
    with open(fname, 'r') as f:
        lines = open(fname, "r").read().splitlines()
        top_keys = []
        duped_keys = []
        for line in lines:
            m = re.search(r'^([A-Za-z0-9_]+) *:', line)
            if m:
                if m.group(1) in top_keys:
                    duped_keys.append(m.group(1))
                else:
                    top_keys.append(m.group(1))
        if duped_keys:
            raise Exception('ERROR: duplicate keys: {}'.format(duped_keys))
    # 2nd pass to set up the OrderedDict
    with open(fname, 'r') as f:
        d_tmp = yaml.load(f)
    return OrderedDict([(key, d_tmp[key]) for key in top_keys])
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.