Dengan Python, bagaimana Anda bisa memuat pemetaan YAML sebagai OrderedDicts?

128

Saya ingin mendapatkan loader PyYAML untuk memuat pemetaan (dan memesan pemetaan) ke dalam tipe Python 2.7+ OrderedDict , bukan vanilla dictdan daftar pasangan yang saat ini digunakan.

Apa cara terbaik untuk melakukan itu?

Eric Naeseth
sumber

Jawaban:

147

Pembaruan: Dalam python 3.6+ Anda mungkin tidak perlu OrderedDictsama sekali karena implementasi dikt baru yang telah digunakan dalam pypy untuk beberapa waktu (meskipun dianggap detail implementasi CPython untuk saat ini).

Pembaruan: Dalam python 3.7+, sifat pelestarian sisipan urutan objek didik telah dinyatakan sebagai bagian resmi dari spesifikasi bahasa Python , lihat What's New In Python 3.7 .

Saya suka solusi @James karena kesederhanaannya. Namun, itu mengubah yaml.Loaderkelas global default , yang dapat menyebabkan efek samping yang merepotkan. Terutama, ketika menulis kode perpustakaan ini adalah ide yang buruk. Juga, itu tidak langsung bekerja dengannya yaml.safe_load().

Untungnya, solusinya dapat ditingkatkan tanpa banyak usaha:

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)

Untuk serialisasi, saya tidak tahu generalisasi yang jelas, tetapi setidaknya ini seharusnya tidak memiliki efek samping:

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)
perbaikan dingin
sumber
3
+1 - terima kasih banyak untuk ini, ini telah menyelamatkan saya dari banyak masalah.
Nobilis
2
Implementasi ini memecah tag gabungan YAML, BTW
Randy
1
@Randy Terima kasih. Saya tidak menjalankan skenario itu sebelumnya, tapi sekarang saya menambahkan perbaikan untuk menangani ini juga (saya harap).
coldfix
9
@ArneBabenhauserheide Saya tidak yakin apakah PyPI cukup hulu, tetapi lihat ruamel.yaml (saya penulisnya) jika menurut Anda itu benar.
Anthon
1
@Anthon Perpustakaan ruamel.yaml Anda bekerja dengan sangat baik. Terima kasih untuk itu.
Jan Vlcinsky
56

Modul yaml memungkinkan Anda menentukan 'perwakilan' khusus untuk mengonversi objek Python menjadi teks dan 'konstruktor' untuk membalikkan proses.

_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)
Brice M. Dempsey
sumber
5
ada penjelasan untuk jawaban ini?
Shuman
1
Atau bahkan lebih baik from six import iteritemsdan kemudian mengubahnya iteritems(data)agar berfungsi sama baiknya di Python 2 & 3.
Midnighter
5
Ini tampaknya menggunakan fitur PyYAML ( represent_dictdan DEFAULT_MAPPING_TAG) yang tidak berdokumen . Apakah ini karena dokumentasinya tidak lengkap, atau apakah fitur-fitur ini tidak didukung dan dapat berubah tanpa pemberitahuan?
aldel
3
Perhatikan bahwa untuk dict_constructorAnda harus menelepon loader.flatten_mapping(node)atau Anda tidak akan dapat memuat <<: *...(menggabungkan sintaksis)
Anthony Sottile
@ brice-m-dempsey dapatkah Anda menambahkan contoh bagaimana menggunakan kode Anda? Tampaknya tidak berfungsi dalam kasus saya (Python 3.7)
schaffe
53

Opsi 2018:

oyamladalah pengganti drop-in untuk PyYAML yang mempertahankan pemesanan dict. Kedua Python 2 dan Python 3 didukung. Cukup pip install oyaml, dan impor seperti yang ditunjukkan di bawah ini:

import oyaml as yaml

Anda tidak akan lagi terganggu oleh pemetaan yang kacau saat membuang / memuat.

Catatan: Saya penulis oyaml.

wim
sumber
1
Terima kasih untuk ini! Untuk beberapa alasan, bahkan dengan Python 3.8 urutannya tidak dihormati dengan PyYaml. oyaml segera menyelesaikan ini untukku.
John Smith Opsional
26

Opsi 2015 (dan lebih baru):

ruamel.yaml adalah setetes untuk PyYAML (penafian: Saya pembuat paket itu). Mempertahankan urutan pemetaan adalah salah satu hal yang ditambahkan dalam versi pertama (0,1) kembali pada tahun 2015. Tidak hanya mempertahankan urutan kamus Anda, itu juga akan mempertahankan komentar, nama jangkar, tag dan tidak mendukung YAML 1.2 spesifikasi (dirilis 2009)

Spesifikasi mengatakan bahwa pemesanan tidak dijamin, tetapi tentu saja ada pemesanan dalam file YAML dan parser yang sesuai hanya bisa berpegang pada itu dan secara transparan menghasilkan objek yang menjaga pemesanan. Anda hanya perlu memilih parser, loader, dan dumper¹ yang tepat:

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)

akan memberimu:

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

dataadalah tipe CommentedMapyang berfungsi seperti dict, tetapi memiliki informasi tambahan yang disimpan hingga dibuang (termasuk komentar yang disimpan!)

Anthon
sumber
Itu cukup bagus jika Anda sudah memiliki file YAML, tetapi bagaimana Anda melakukannya dengan menggunakan struktur Python? Saya mencoba menggunakan CommentedMapsecara langsung tetapi tidak berhasil, dan OrderedDictmenempatkan di !!omapmana-mana yang tidak ramah pengguna.
Holt
Saya tidak yakin mengapa CommentedMap tidak bekerja untuk Anda. Bisakah Anda memposting pertanyaan dengan kode Anda (diminimalkan) dan menandainya ruamel.yaml? Dengan begitu saya akan diberitahu dan menjawab.
Anthon
Maaf, saya pikir itu karena saya mencoba untuk menyimpan CommentedMapdengan safe=Truedi YAML, yang tidak bekerja (menggunakan safe=Falsekarya). Saya juga memiliki masalah dengan CommentedMaptidak dapat dimodifikasi, tetapi saya tidak dapat mereproduksinya sekarang ... Saya akan membuka pertanyaan baru jika saya menemukan masalah ini lagi.
Holt
Anda harus menggunakan yaml = YAML(), Anda mendapatkan parser / dumper pulang-pergi dan itu adalah turunan dari parser / dumper aman yang tahu tentang CommentedMap / Seq dll.
Anthon
14

Catatan : ada pustaka, berdasarkan jawaban berikut, yang mengimplementasikan CLoader dan CDumper: Phynix / yamlloader

Saya sangat ragu bahwa ini adalah cara terbaik untuk melakukannya, tetapi ini adalah cara saya datang dengan, dan itu berhasil. Juga tersedia sebagai intisari .

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
Eric Naeseth
sumber
Jika Anda ingin menyertakan key_node.start_markatribut dalam pesan kesalahan Anda, saya tidak melihat cara yang jelas untuk menyederhanakan loop konstruksi pusat Anda. Jika Anda mencoba memanfaatkan fakta bahwa OrderedDictkonstruktor akan menerima iterable pasangan kunci, nilai, Anda kehilangan akses ke detail itu ketika membuat pesan kesalahan.
ncoghlan
Adakah yang sudah menguji kode ini dengan benar? Saya tidak bisa menjalankannya di aplikasi saya!
theAlse
Contoh Penggunaan: ordered_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # ordered_dict = OrderedDict ([('b', 1), ('a', 2)]) Sayangnya hasil edit saya untuk posting ditolak, jadi mohon alasan tidak memformat.
Kolonel Panic
Implementasi ini memutus pemuatan tipe pemetaan yang dipesan . Untuk memperbaiki ini, Anda bisa menghapus panggilan kedua ke add_constructordalam __init__metode Anda .
Ryan
10

Pembaruan : perpustakaan tidak digunakan lagi karena yamlloader (yang didasarkan pada yamlordereddictloader)

Saya baru saja menemukan perpustakaan Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ) yang dibuat berdasarkan jawaban atas pertanyaan ini dan cukup mudah digunakan:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
Alex Chekunkov
sumber
Saya tidak tahu apakah ini penulis yang sama atau tidak, tetapi periksa yodldi github.
Tn. B
3

Di instalasi PyYaml saya untuk Python 2.7 saya memperbarui __init__.py, constructor.py, dan loader.py. Sekarang mendukung opsi object_pairs_hook untuk memuat perintah. Perbedaan perubahan yang saya buat adalah di bawah ini.

__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)
EricGreg
sumber
Ini harus ditambahkan hulu sebenarnya.
Michael
1
Justed mengajukan permintaan tarik dengan perubahan Anda. github.com/yaml/pyyaml/pull/12 Mari berharap untuk penggabungan.
Michael
Sangat berharap penulis lebih aktif, komit terakhir adalah 4 tahun yang lalu. Perubahan ini akan menjadi berkah bagi saya.
Mark LeMoine
-1

inilah solusi sederhana yang juga memeriksa kunci tingkat atas yang digandakan di peta Anda.

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])
Adam Murphy
sumber