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

我想让 PyYAML的加载程序将映射(和有序映射)加载到 Python 2.7 + 命令类型,而不是普通的 dict和它当前使用的对列表。

最好的办法是什么?

78840 次浏览

注意 : 有一个基于以下答案的库,它也实现了 CLoader 和 CDumpers: < a href = “ https://github.com/Phynix/yamlloader”rel = “ nofollow noReferrer”> 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

在安装 Python 2.7的 For PyYaml 时,我更新了 _ _ init _ _。Py,structor.py,and loader.py.现在支持用于加载命令的 object _ tables _ 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)

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)

Python > = 3.6

在 python 3.6 + 中,似乎默认情况下不使用特殊的字典类型就保留了 dict加载中顺序。另一方面,默认的 翻斗车按键对字典进行排序。从 pyyaml 5.1开始,您可以通过传递 sort_keys=False来关闭:

a = dict(zip("unsorted", "unsorted"))
s = yaml.safe_dump(a, sort_keys=False)
b = yaml.safe_load(s)


assert list(a.keys()) == list(b.keys())  # True

这可以工作,因为 新的命令行指令实现已经在使用了一段时间。虽然在 CPython 3.6中仍然被认为是一个实现细节,但是从3.7 + 开始,“ dicts 的插入顺序保持特性已经被声明为 Python 语言规范的正式部分”,参见 Python 3.7中的新特性

注意,在 PyYAML 方面仍然没有对此进行文档化,因此对于安全关键的应用程序,您不应该依赖于它。

原始答案(与所有已知版本兼容)

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

幸运的是,这种解决方案可以不费吹灰之力地得到改进:

import yaml
from collections import OrderedDict


def ordered_load(stream, Loader=yaml.SafeLoader, 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.SafeDumper, **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)

在每种情况下,您还可以将自定义子类设置为全局的,这样就不必在每次调用时重新创建它们。

2015年(及以后)方案:

Yaml 是 PyYAML 的替代品(免责声明: 我是那个包的作者)。保持映射的顺序是2015年第一个版本(0.1)中添加的内容之一。它不仅保留了字典的顺序,还保留了注释、锚名、标记,并且支持 YAML 1.2规范(2009年发布)

规范指出,排序是不能保证的,但是当然在 YAML 文件中存在排序,适当的解析器可以保持这种排序,并透明地生成一个对象来保持排序。你只需要选择正确的解析器、加载器和转储器1:

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

data的类型是 CommentedMap,它的功能类似于 dict,但是有额外的信息,这些信息一直保留到被转储(包括保留的注释!)

这里有一个简单的解决方案,也检查重复的顶级键在您的地图。

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])

更新 : 为了支持 炸弹(基于 yamlordereddicloader) ,不推荐使用该库

我刚刚发现了一个 Python 库(https://pypi.python.org/pypi/yamlordereddictloader/0.1.1) ,它是根据这个问题的答案创建的,使用起来非常简单:

import yaml
import yamlordereddictloader


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

2018年的选择:

oyaml PyYAML的插入式替代品,它保留了 dict 顺序。Python2和 Python3都受支持。只有 pip install oyaml和导入,如下所示:

import oyaml as yaml

在转储/加载时,您将不再会因为错误的映射而烦恼。

注: 我是 oyaml 的作者。