Bagaimana saya bisa memasukkan file YAML ke dalam file lain?

288

Jadi saya memiliki dua file YAML, "A" dan "B" dan saya ingin konten A dimasukkan di dalam B, baik disambungkan ke struktur data yang ada, seperti array, atau sebagai anak elemen, seperti nilai untuk kunci hash tertentu.

Apakah ini mungkin? Bagaimana? Jika tidak, ada petunjuk ke referensi normatif?

kch
sumber
1
Saya baru-baru ini bertemu dengan HiYaPyCo untuk Python yang melakukan ini. Anda dapat menggabungkan berbagai file YAML. Is adalah modul Python yang sangat bagus yang perlu diketahui.
nowox

Jawaban:

326

Tidak, YAML tidak termasuk pernyataan "impor" atau "sertakan".

jameshfisher
sumber
8
Anda dapat membuat penangan! Include <filename>.
clarkevans
5
@clarkevans yakin, tetapi konstruk itu akan "di luar" bahasa YAML.
jameshfisher
2
Ini sekarang mungkin. Saya telah menambahkan jawaban di bawah ... semoga membantu.
daveaspinall
1
Jika Anda menggunakan Rails, Anda dapat memasukkan sintaks ERB <% = 'fdsa fdsa'%> dan itu akan berfungsi
gleenn
9
Saya pikir jawaban ini harus diulangi sebagai "Tidak, standar YAML tidak termasuk fungsi ini. Namun banyak implementasi menyediakan beberapa ekstensi untuk melakukannya."
Franklin Yu
112

Pertanyaan Anda tidak meminta solusi Python, tapi di sini ada satu yang menggunakan PyYAML .

PyYAML memungkinkan Anda untuk melampirkan konstruktor khusus (seperti !include) ke loader YAML. Saya telah memasukkan direktori root yang dapat diatur sehingga solusi ini mendukung referensi file relatif dan absolut.

Solusi Berbasis Kelas

Ini adalah solusi berbasis kelas, yang menghindari variabel root global dari respons awal saya.

Lihat inti ini untuk solusi Python 3 yang serupa dan lebih kuat yang menggunakan metaclass untuk mendaftarkan konstruktor kustom.

import yaml
import os

class Loader(yaml.SafeLoader):

    def __init__(self, stream):

        self._root = os.path.split(stream.name)[0]

        super(Loader, self).__init__(stream)

    def include(self, node):

        filename = os.path.join(self._root, self.construct_scalar(node))

        with open(filename, 'r') as f:
            return yaml.load(f, Loader)

Loader.add_constructor('!include', Loader.include)

Sebuah contoh:

foo.yaml

a: 1
b:
    - 1.43
    - 543.55
c: !include bar.yaml

bar.yaml

- 3.6
- [1, 2, 3]

Sekarang file dapat dimuat menggunakan:

>>> with open('foo.yaml', 'r') as f:
>>>    data = yaml.load(f, Loader)
>>> data
{'a': 1, 'b': [1.43, 543.55], 'c': [3.6, [1, 2, 3]]}
Josh Bode
sumber
Ini adalah fitur yang menarik, thanx. Tapi apa tujuan dari semua manipulasi ini dengan root / old_root? Saya kira kode includefungsi dapat disederhanakan: `def include (loader, node):" "" Sertakan file YAML lain. "" "Filename = loader.construct_scalar (node) data = yaml.load (buka (nama file))`
Aliaksei Ramanau
Root global ada di sana sehingga relatif mencakup pekerjaan pada kedalaman apa pun, misalnya ketika file yang dimasukkan duduk di direktori yang berbeda menyertakan file relatif ke direktori itu. Absolute termasuk harus bekerja juga. Mungkin ada cara yang lebih bersih untuk melakukan ini tanpa variabel global, mungkin menggunakan kelas yaml.Loader kustom.
Josh Bode
2
Apakah mungkin untuk memiliki sesuatu seperti ini: foo.yaml: a: bla bar.yaml: `! Sertakan foo.yaml b: blubb` Sehingga hasilnya adalah:` {'a': bla, 'b': blubb}
Martin
3
Ini harus menjadi jawaban yang diterima. Juga, nitpick keamanan, Anda harus menggunakan yaml.safeload alih-alih yaml.load, untuk menghindari yaml yang dibuat khusus agar tidak memiliki layanan Anda.
danielpops
1
@JoshBode ini bisa digunakan untuk Anda: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops
32

Jika Anda menggunakan YAML versi Symfony , ini dimungkinkan, seperti ini:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }
daveaspinall
sumber
34
Ini khusus untuk bagaimana Symfony menafsirkan YAML, bukan bagian dari YAML itu sendiri.
jameshfisher
9
Yap, itu sebabnya saya memposting tautan ke Symfony docs. Pertanyaannya adalah "Apakah ini mungkin? Bagaimana?" ... ini adalah bagaimana. Tidak melihat alasan untuk downvote.
daveaspinall
4
Saya tidak menurunkan suara Anda; Saya hanya menunjukkan bahwa ini khusus untuk Symfony YAML.
jameshfisher
9
Tidak ada "Symfony versi YAML" ... ini hanyalah pustaka yang kompatibel dengan YAML khusus vendor yang memiliki hal-hal tambahan yang bukan bagian dari YAML.
dreftymac
3
Tidak ada alasan untuk menurunkan jawaban ini jika jawaban "berbasis kelas" dibatalkan.
Mikhail
13

Termasuk tidak secara langsung didukung di YAML sejauh yang saya tahu, Anda harus menyediakan mekanisme sendiri, namun ini umumnya mudah dilakukan.

Saya telah menggunakan YAML sebagai bahasa konfigurasi di aplikasi python saya, dan dalam hal ini sering mendefinisikan konvensi seperti ini:

>>> main.yml <<<
includes: [ wibble.yml, wobble.yml]

Kemudian dalam kode (python) saya saya lakukan:

import yaml
cfg = yaml.load(open("main.yml"))
for inc in cfg.get("includes", []):
   cfg.update(yaml.load(open(inc)))

Satu-satunya sisi negatifnya adalah bahwa variabel dalam menyertakan akan selalu menimpa variabel di utama, dan tidak ada cara untuk mengubah prioritas itu dengan mengubah di mana "pernyataan: pernyataan muncul di file main.yml.

Pada titik yang sedikit berbeda, YAML tidak mendukung termasuk karena tidak benar-benar dirancang sebagai eksklusif sebagai mark up berbasis file. Apa yang dimaksud dengan menyertakan jika Anda mendapatkannya sebagai tanggapan terhadap permintaan AJAX?

clh
sumber
3
ini hanya berfungsi ketika file yaml tidak mengandung konfigurasi bersarang.
Freedom
10

Untuk pengguna Python, Anda dapat mencoba pyyaml-include .

Install

pip install pyyaml-include

Pemakaian

import yaml
from yamlinclude import YamlIncludeConstructor

YamlIncludeConstructor.add_to_loader_class(loader_class=yaml.FullLoader, base_dir='/your/conf/dir')

with open('0.yaml') as f:
    data = yaml.load(f, Loader=yaml.FullLoader)

print(data)

Anggap kami memiliki file YAML tersebut :

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml
  • 1.yaml konten:
name: "1"
  • 2.yaml konten:
name: "2"

Sertakan file dengan nama

  • Di tingkat atas:

    Jika 0.yamltadinya:

!include include.d/1.yaml

Kita akan mendapatkan:

{"name": "1"}
  • Dalam pemetaan:

    Jika 0.yamltadinya:

file1: !include include.d/1.yaml
file2: !include include.d/2.yaml

Kita akan mendapatkan:

  file1:
    name: "1"
  file2:
    name: "2"
  • Berurutan:

    Jika 0.yamltadinya:

files:
  - !include include.d/1.yaml
  - !include include.d/2.yaml

Kita akan mendapatkan:

files:
  - name: "1"
  - name: "2"

Catatan :

Nama file dapat berupa absolut (suka /usr/conf/1.5/Make.yml) atau relatif (suka ../../cfg/img.yml).

Sertakan file dengan wildcard

Nama file dapat berisi wildcard gaya-shell. Data yang diambil dari file yang ditemukan oleh wildcard akan diatur secara berurutan.

Jika 0.yamltadinya:

files: !include include.d/*.yaml

Kita akan mendapatkan:

files:
  - name: "1"
  - name: "2"

Catatan :

  • Sebab Python>=3.5, jika recursiveargumen dari tag !include YAML adalah true, polanya “**”akan cocok dengan semua file dan nol atau lebih direktori dan subdirektori.
  • Menggunakan “**”pola dalam pohon direktori besar dapat menghabiskan banyak waktu karena pencarian rekursif.

Untuk mengaktifkan recursiveargumen, kami akan menulis !includetag dalam Mappingatau Sequencemode:

  • Argumen dalam Sequencemode:
!include [tests/data/include.d/**/*.yaml, true]
  • Argumen dalam Mappingmode:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
xqliang
sumber
Ini sebenarnya tidak menjawab pertanyaan. Ini berkaitan dengan solusi Python, tidak ada yang menggunakan format YAML standar.
oligofren
@oligofren Penangan tag khusus adalah fitur YAML, yang memungkinkan parser untuk memperluas YAML untuk menentukan jenis dan menerapkan perilaku khusus seperti ini. Akan sangat panjang untuk spesifikasi YAML sendiri untuk menentukan bagaimana inklusi file harus bekerja dengan semua spesifikasi path OS yang berbeda, sistem file, dll.
Anton Strogonoff
@AntonStrogonoff Terima kasih atas perhatian saya. Bisakah Anda mengarahkan saya ke tempat seperti itu di RFC? Tidak disebutkan kata "kebiasaan". Ref yaml.org/spec/1.2/spec.html
oligofren
1
@oligofren Sama-sama. Cari tag "khusus aplikasi" .
Anton Strogonoff
8

Memperluas jawaban @ Josh_Bode, inilah solusi PyYAML saya sendiri, yang memiliki keuntungan menjadi subclass mandiri yaml.Loader. Itu tidak tergantung pada global tingkat modul, atau pada memodifikasi keadaan global yamlmodul.

import yaml, os

class IncludeLoader(yaml.Loader):                                                 
    """                                                                           
    yaml.Loader subclass handles "!include path/to/foo.yml" directives in config  
    files.  When constructed with a file object, the root path for includes       
    defaults to the directory containing the file, otherwise to the current       
    working directory. In either case, the root path can be overridden by the     
    `root` keyword argument.                                                      

    When an included file F contain its own !include directive, the path is       
    relative to F's location.                                                     

    Example:                                                                      
        YAML file /home/frodo/one-ring.yml:                                       
            ---                                                                   
            Name: The One Ring                                                    
            Specials:                                                             
                - resize-to-wearer                                                
            Effects: 
                - !include path/to/invisibility.yml                            

        YAML file /home/frodo/path/to/invisibility.yml:                           
            ---                                                                   
            Name: invisibility                                                    
            Message: Suddenly you disappear!                                      

        Loading:                                                                  
            data = IncludeLoader(open('/home/frodo/one-ring.yml', 'r')).get_data()

        Result:                                                                   
            {'Effects': [{'Message': 'Suddenly you disappear!', 'Name':            
                'invisibility'}], 'Name': 'The One Ring', 'Specials':              
                ['resize-to-wearer']}                                             
    """                                                                           
    def __init__(self, *args, **kwargs):                                          
        super(IncludeLoader, self).__init__(*args, **kwargs)                      
        self.add_constructor('!include', self._include)                           
        if 'root' in kwargs:                                                      
            self.root = kwargs['root']                                            
        elif isinstance(self.stream, file):                                       
            self.root = os.path.dirname(self.stream.name)                         
        else:                                                                     
            self.root = os.path.curdir                                            

    def _include(self, loader, node):                                    
        oldRoot = self.root                                              
        filename = os.path.join(self.root, loader.construct_scalar(node))
        self.root = os.path.dirname(filename)                           
        data = yaml.load(open(filename, 'r'))                            
        self.root = oldRoot                                              
        return data                                                      
Maxy-B
sumber
2
Akhirnya sempat menambahkan pendekatan berbasis kelas untuk jawaban saya, tetapi Anda mengalahkan saya ke pukulan :) Catatan: Jika Anda menggunakan di yaml.load(f, IncludeLoader)dalam _includeAnda dapat menghindari harus mengganti root. Selain itu, kecuali Anda melakukan ini, solusinya tidak akan bekerja lebih dari satu level karena data yang disertakan menggunakan yaml.Loaderkelas reguler .
Josh Bode
Aku harus menghapus kata kunci rootdari kwargssetelah pengaturan self.rootuntuk membuatnya bekerja dengan string. Saya memindahkan blok if-else di atas superpanggilan. Mungkin orang lain dapat mengkonfirmasi temuan saya atau menunjukkan kepada saya bagaimana menggunakan kelas dengan string dan rootparameter.
Woltan
1
Sayangnya, ini tidak berfungsi dengan referensi seperti `` `termasuk: & TERMASUK! Termasuk penggabungan inner.yaml: <<: * TERMASUK` `
antony
2

Saya membuat beberapa contoh untuk referensi Anda.

import yaml

main_yaml = """
Package:
 - !include _shape_yaml    
 - !include _path_yaml
"""

_shape_yaml = """
# Define
Rectangle: &id_Rectangle
    name: Rectangle
    width: &Rectangle_width 20
    height: &Rectangle_height 10
    area: !product [*Rectangle_width, *Rectangle_height]

Circle: &id_Circle
    name: Circle
    radius: &Circle_radius 5
    area: !product [*Circle_radius, *Circle_radius, pi]

# Setting
Shape:
    property: *id_Rectangle
    color: red
"""

_path_yaml = """
# Define
Root: &BASE /path/src/

Paths: 
    a: &id_path_a !join [*BASE, a]
    b: &id_path_b !join [*BASE, b]

# Setting
Path:
    input_file: *id_path_a
"""


# define custom tag handler
def yaml_import(loader, node):
    other_yaml_file = loader.construct_scalar(node)
    return yaml.load(eval(other_yaml_file), Loader=yaml.SafeLoader)


def yaml_product(loader, node):
    import math
    list_data = loader.construct_sequence(node)
    result = 1
    pi = math.pi
    for val in list_data:
        result *= eval(val) if isinstance(val, str) else val
    return result


def yaml_join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])


def yaml_ref(loader, node):
    ref = loader.construct_sequence(node)
    return ref[0]


def yaml_dict_ref(loader: yaml.loader.SafeLoader, node):
    dict_data, key, const_value = loader.construct_sequence(node)
    return dict_data[key] + str(const_value)


def main():
    # register the tag handler
    yaml.SafeLoader.add_constructor(tag='!include', constructor=yaml_import)
    yaml.SafeLoader.add_constructor(tag='!product', constructor=yaml_product)
    yaml.SafeLoader.add_constructor(tag='!join', constructor=yaml_join)
    yaml.SafeLoader.add_constructor(tag='!ref', constructor=yaml_ref)
    yaml.SafeLoader.add_constructor(tag='!dict_ref', constructor=yaml_dict_ref)

    config = yaml.load(main_yaml, Loader=yaml.SafeLoader)

    pk_shape, pk_path = config['Package']
    pk_shape, pk_path = pk_shape['Shape'], pk_path['Path']
    print(f"shape name: {pk_shape['property']['name']}")
    print(f"shape area: {pk_shape['property']['area']}")
    print(f"shape color: {pk_shape['color']}")

    print(f"input file: {pk_path['input_file']}")


if __name__ == '__main__':
    main()

keluaran

shape name: Rectangle
shape area: 200
shape color: red
input file: /path/src/a

Perbarui 2

dan Anda dapat menggabungkannya, seperti ini

# xxx.yaml
CREATE_FONT_PICTURE:
  PROJECTS:
    SUNG: &id_SUNG
      name: SUNG
      work_dir: SUNG
      output_dir: temp
      font_pixel: 24


  DEFINE: &id_define !ref [*id_SUNG]  # you can use config['CREATE_FONT_PICTURE']['DEFINE'][name, work_dir, ... font_pixel]
  AUTO_INIT:
    basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # SUNG30

# ↓ This is not correct.
# basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # It will build by Deep-level. id_define is Deep-level: 2. So you must put it after 2. otherwise, it can't refer to the correct value.
Carson
sumber
1

Sayangnya YAML tidak menyediakan ini dalam standarnya.

Tetapi jika Anda menggunakan Ruby, ada permata yang menyediakan fungsionalitas yang Anda minta dengan memperluas pustaka ruby ​​YAML: https://github.com/entwanderer/yaml_extend

pengguna8419486
sumber
1

Saya pikir solusi yang digunakan oleh @ maxy-B tampak hebat. Namun, itu tidak berhasil bagi saya dengan inklusi bersarang. Misalnya jika config_1.yaml menyertakan config_2.yaml, yang mencakup config_3.yaml ada masalah dengan loader. Namun, jika Anda cukup mengarahkan kelas loader baru ke dirinya sendiri pada beban, itu berhasil! Secara khusus, jika kita mengganti fungsi _include lama dengan versi yang sedikit dimodifikasi:

def _include(self, loader, node):                                    
     oldRoot = self.root                                              
     filename = os.path.join(self.root, loader.construct_scalar(node))
     self.root = os.path.dirname(filename)                           
     data = yaml.load(open(filename, 'r'), loader = IncludeLoader)                            
     self.root = oldRoot                                              
     return data

Pada refleksi saya setuju dengan komentar lain, bahwa pemuatan bersarang tidak sesuai untuk yaml secara umum karena aliran input mungkin bukan file, tetapi sangat berguna!

Padi
sumber
1

Standar YML tidak menentukan cara untuk melakukan ini. Dan masalah ini tidak membatasi dirinya ke YML. JSON memiliki keterbatasan yang sama.

Banyak aplikasi yang menggunakan konfigurasi berbasis YML atau JSON akhirnya mengalami masalah ini. Dan ketika itu terjadi, mereka membuat konvensi mereka sendiri .

misalnya untuk definisi API sombong:

$ref: 'file.yml'

misalnya untuk konfigurasi penulisan buruh pelabuhan:

services:
  app:
    extends:
      file: docker-compose.base.yml

Atau, jika Anda ingin membagi konten file yml dalam banyak file, seperti pohon konten, Anda dapat menentukan konvensi struktur folder Anda sendiri dan menggunakan skrip gabungan (yang sudah ada).

bvdb
sumber
0

Standar YAML 1.2 tidak termasuk fitur ini. Namun demikian banyak implementasi menyediakan beberapa ekstensi untuk melakukannya.

Saya menyajikan cara mencapainya dengan Java dan snakeyaml:1.24(perpustakaan Java untuk mem-parsing / memancarkan file YAML) yang memungkinkan membuat tag YAML khusus untuk mencapai tujuan berikut (Anda akan melihat saya menggunakannya untuk memuat suite pengujian yang ditentukan dalam beberapa file YAML dan saya membuatnya berfungsi sebagai daftar menyertakan untuk test:simpul target ):

# ... yaml prev stuff

tests: !include
  - '1.hello-test-suite.yaml'
  - '3.foo-test-suite.yaml'
  - '2.bar-test-suite.yaml'

# ... more yaml document

Berikut ini adalah Java satu kelas yang memungkinkan pemrosesan !includetag. File diambil dari classpath (direktori sumber Maven):

/**
 * Custom YAML loader. It adds support to the custom !include tag which allows splitting a YAML file across several
 * files for a better organization of YAML tests.
 */
@Slf4j   // <-- This is a Lombok annotation to auto-generate logger
public class MyYamlLoader {

    private static final Constructor CUSTOM_CONSTRUCTOR = new MyYamlConstructor();

    private MyYamlLoader() {
    }

    /**
     * Parse the only YAML document in a stream and produce the Java Map. It provides support for the custom !include
     * YAML tag to split YAML contents across several files.
     */
    public static Map<String, Object> load(InputStream inputStream) {
        return new Yaml(CUSTOM_CONSTRUCTOR)
                .load(inputStream);
    }


    /**
     * Custom SnakeYAML constructor that registers custom tags.
     */
    private static class MyYamlConstructor extends Constructor {

        private static final String TAG_INCLUDE = "!include";

        MyYamlConstructor() {
            // Register custom tags
            yamlConstructors.put(new Tag(TAG_INCLUDE), new IncludeConstruct());
        }

        /**
         * The actual include tag construct.
         */
        private static class IncludeConstruct implements Construct {

            @Override
            public Object construct(Node node) {
                List<Node> inclusions = castToSequenceNode(node);
                return parseInclusions(inclusions);
            }

            @Override
            public void construct2ndStep(Node node, Object object) {
                // do nothing
            }

            private List<Node> castToSequenceNode(Node node) {
                try {
                    return ((SequenceNode) node).getValue();

                } catch (ClassCastException e) {
                    throw new IllegalArgumentException(String.format("The !import value must be a sequence node, but " +
                            "'%s' found.", node));
                }
            }

            private Object parseInclusions(List<Node> inclusions) {

                List<InputStream> inputStreams = inputStreams(inclusions);

                try (final SequenceInputStream sequencedInputStream =
                             new SequenceInputStream(Collections.enumeration(inputStreams))) {

                    return new Yaml(CUSTOM_CONSTRUCTOR)
                            .load(sequencedInputStream);

                } catch (IOException e) {
                    log.error("Error closing the stream.", e);
                    return null;
                }
            }

            private List<InputStream> inputStreams(List<Node> scalarNodes) {
                return scalarNodes.stream()
                        .map(this::inputStream)
                        .collect(toList());
            }

            private InputStream inputStream(Node scalarNode) {
                String filePath = castToScalarNode(scalarNode).getValue();
                final InputStream is = getClass().getClassLoader().getResourceAsStream(filePath);
                Assert.notNull(is, String.format("Resource file %s not found.", filePath));
                return is;
            }

            private ScalarNode castToScalarNode(Node scalarNode) {
                try {
                    return ((ScalarNode) scalarNode);

                } catch (ClassCastException e) {
                    throw new IllegalArgumentException(String.format("The value must be a scalar node, but '%s' found" +
                            ".", scalarNode));
                }
            }
        }

    }

}
Gerard Bosch
sumber
0

Dengan Yglu , Anda dapat mengimpor file lain seperti ini:

A.yaml

foo: !? $import('B.yaml')

B.yaml

bar: Hello
$ yglu A.yaml
foo:
  bar: Hello

Seperti $importfungsi, Anda juga bisa memberikan ekspresi sebagai argumen:

  dep: !- b
  foo: !? $import($_.dep.toUpper() + '.yaml')

Ini akan memberikan hasil yang sama seperti di atas.

Penafian: Saya adalah penulis Yglu.

lbovet
sumber
-1

Dengan Symfony , penanganan yamlnya secara tidak langsung akan memungkinkan Anda untuk membuat sarang file yaml. Caranya adalah dengan memanfaatkan parametersopsi. misalnya:

common.yml

parameters:
    yaml_to_repeat:
        option: "value"
        foo:
            - "bar"
            - "baz"

config.yml

imports:
    - { resource: common.yml }
whatever:
    thing: "%yaml_to_repeat%"
    other_thing: "%yaml_to_repeat%"

Hasilnya akan sama dengan:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
jxmallett
sumber
-6

Mungkin itu tidak didukung ketika pertanyaan diajukan tetapi Anda dapat mengimpor file YAML lainnya menjadi satu:

imports: [/your_location_to_yaml_file/Util.area.yaml]

Meskipun saya tidak punya referensi online tetapi ini bekerja untuk saya.

Sankalp
sumber
4
Ini tidak termasuk apa pun. Itu menciptakan pemetaan dengan urutan yang terdiri dari string tunggal "/your_location_to_yaml_file/Util.area.yaml", sebagai nilai untuk kunci imports.
Anthon