Як я можу включити файл YAML всередину іншого?


288

Отже, у мене є два файли YAML, "A" і "B", і я хочу, щоб вміст A було вставлено всередину B, або зрощене в існуючу структуру даних, як масив, або як дочірня частина елемента, як значення для певного хеш-ключа.

Чи можливо це взагалі? Як? Якщо ні, то якісь покажчики на нормативну посилання?



1
Нещодавно я наткнувся на HiYaPyCo для Python, який робить саме це. Ви можете об'єднати різні файли YAML разом. Це дуже приємний модуль Python, який варто знати.
nowox

Відповіді:


326

Ні, YAML не включає будь-які заяви "імпорт" або "включити".


8
Ви можете створити обробник! Include <filename>.
clarkevans

5
@clarkevans впевнений, але ця конструкція була б "поза" мовою YAML.
jameshfisher

2
Це зараз можливо. Я додав відповідь нижче ... сподіваюся, що це допоможе.
daveaspinall

1
Якщо ви використовуєте Rails, ви можете вставити синтаксис <% = 'fdsa fdsa'%> ERB, і він спрацює
gleenn

9
Я думаю, що цю відповідь слід перефразувати як "Ні, стандартна YAML не включає цю функцію. Тим не менш, багато реалізацій передбачають деяке розширення для цього".
Франклін Ю.

113

У вашому запитанні не задається рішення Python, але ось таке, що використовує PyYAML .

PyYAML дозволяє приєднувати спеціальні конструктори (такі як !include) до навантажувача YAML. Я включив кореневий каталог, який можна встановити таким чином, щоб це рішення підтримувало відносні та абсолютні посилання на файли.

Рішення на основі класу

Ось рішення на основі класу, яке дозволяє уникнути глобальної кореневої змінної моєї оригінальної відповіді.

Дивіться цю суть щодо подібного, більш надійного рішення Python 3, яке використовує метаклас для реєстрації користувальницького конструктора.

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)

Приклад:

foo.yaml

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

bar.yaml

- 3.6
- [1, 2, 3]

Тепер файли можна завантажити, використовуючи:

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

Це цікава функція, ніж подяка. Але яка мета всіх цих маніпуляцій з root / old_root? Я припускаю, що код includeфункції може бути спрощений: `def включає (завантажувач, вузол):" "" Включити інший файл YAML. "" "Ім'я файлу = loader.construct_scalar (вузол) data = yaml.load (відкрити (ім'я файлу))` `
Аліаксей Раманау

Коренева глобальна система існує так, що відносно включає роботу на будь-якій глибині, наприклад, коли файли, що містяться в іншому каталозі, включають файл відносно цього каталогу. Абсолютний включення також повинен працювати. Можливо, існує більш чистий спосіб зробити це без глобальної змінної, можливо, використовуючи користувацький клас yaml.Loader.
Джош Боде

2
Чи можливо також мати щось подібне: foo.yaml: a: bla bar.yaml: `! Include foo.yaml b: blubb`, щоб результат був:` {'a': bla, 'b': blubb}
Мартін

3
Це має бути прийнятою відповіддю. Крім того, для безпеки програми, вам слід використовувати yaml.safeload замість yaml.load, щоб уникнути володіння вами послугою спеціально створеної ямли.
danielpops

1
@JoshBode це має працювати для вас: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops

32

Якщо ви використовуєте версію YAML Symfony , це можливо, наприклад:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }

34
Це специфічно для того, як Symfony інтерпретує YAML, а не частину самої YAML.
jameshfisher

9
Так, саме тому я розмістив посилання на документи Symfony. Питання задає "Чи можливо це взагалі? Як?" ... ось як. Не бачте причин для протистояння.
daveaspinall

4
Я не порушив тебе; Я просто вказую, що це характерно для Symfony YAML.
jameshfisher

9
Немає "Symfony-версії YAML" ... це просто спеціалізована для постачальника YAML-бібліотека, яка має додаткові матеріали, які не є частиною YAML.
dreftymac

3
Немає жодних причин спростовувати цю відповідь, якщо відповідь "на основі класу" є анульованою.
Михайло

13

Наскільки я знаю, включення не підтримуються безпосередньо в YAML, вам доведеться самостійно надати механізм, але це, як правило, легко зробити.

Я використовував YAML в якості мови конфігурації в своїх програмах python, і в цьому випадку часто визначаю такий конвенцію:

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

Потім у своєму (python) коді я роблю:

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

Єдиною нижньою стороною є те, що змінні в включенні завжди змінюватимуть основні змінні, і немає жодного способу змінити цей пріоритет, змінивши те, де у файлі main.yml з'являється оператор "включає:".

У дещо іншому пункті YAML не підтримує включення, оскільки його насправді не розроблено так, як виключно як розмітка файлів. Що означатиме включення, якщо ви отримали його у відповідь на запит AJAX?


3
це працює лише тоді, коли файл yaml не містить вкладеної конфігурації.
Свобода

10

Для користувачів Python ви можете спробувати pyyaml-include .

Встановити

pip install pyyaml-include

Використання

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)

Подумайте, у нас є такі файли YAML :

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml
  • 1.yaml зміст:
name: "1"
  • 2.yaml зміст:
name: "2"

Додайте файли по імені

  • На верхньому рівні:

    Якби 0.yaml:

!include include.d/1.yaml

Ми отримаємо:

{"name": "1"}
  • У картографуванні:

    Якби 0.yaml:

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

Ми отримаємо:

  file1:
    name: "1"
  file2:
    name: "2"
  • Послідовно:

    Якби 0.yaml:

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

Ми отримаємо:

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

Примітка :

Назва файлу може бути абсолютною (подобається /usr/conf/1.5/Make.yml) або відносною (як ../../cfg/img.yml).

Додайте файли за допомогою символів

Ім'я файлу може містити підстановочні символи у формі оболонки. Дані, завантажені з файлів (файлів), знайдених підстановними символами, встановлюються в послідовності.

Якби 0.yaml:

files: !include include.d/*.yaml

Ми отримаємо:

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

Примітка :

  • Тому що Python>=3.5, якщо recursiveаргументом тегу !include YAML є true, шаблон “**”буде відповідати будь-яким файлам і нульовим або більше каталогів і підкаталогів.
  • Використання “**”шаблону у великих деревах директорій може забирати непомірну кількість часу через рекурсивного пошуку.

Для того, щоб включити recursiveаргумент, ми запишемо !includeтег у Mappingабо Sequenceрежим:

  • Аргументи в Sequenceрежимі:
!include [tests/data/include.d/**/*.yaml, true]
  • Аргументи в Mappingрежимі:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}

Це фактично не відповідає на питання. Це стосується рішення Python, а не використовує стандартизований формат YAML.
олігофрен

@oligofren Спеціальні обробники тегів - це особливість YAML, що дозволяє парсерам розширювати YAML для визначення типів та реалізовувати власні поведінки, як це. Було б дуже тяжко, щоб сама специфікація YAML пішла настільки далеко, щоб прописати, як включення файлів має працювати з усіма розрізненими специфікаціями шляху ОС, файловими системами тощо
Антон Строгонов

@AntonStrogonoff Дякую, що донесли це до моєї уваги. Не могли б ви вказати мені на таке місце в RFC? У ньому немає згадки про слово "звичай". Ref yaml.org/spec/1.2/spec.html
oligofren

1
@oligofren Безкоштовно Шукайте теги "конкретні програми" .
Антон Строгонов

8

Розширюючись на відповідь @ Josh_Bode, ось моє власне рішення PyYAML, яке має перевагу бути самостійним підкласом yaml.Loader. Це не залежить від будь-яких глобальних рівнів модулів або від зміни глобального стану yamlмодуля.

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                                                      

2
Нарешті обійшов додавання підходу на основі класу до моєї відповіді, але ти переміг мене до удару :) Примітка: Якщо ви використовуєте yaml.load(f, IncludeLoader)всередині, _includeви можете уникнути необхідності заміни кореня. Крім того, якщо ви цього не зробите, рішення не буде працювати більш ніж на один рівень глибоко, оскільки включені дані використовують звичайний yaml.Loaderклас.
Джош Боде

Я повинен був видалити ключове слово rootз kwargsпісля установки , self.rootщоб отримати його роботу з рядками. Я перемістив блок if-else над superвикликом. Можливо, хтось інший може підтвердити мою знахідку або показати мені, як використовувати клас із рядками та rootпараметром.
Волтан

1
На жаль, це не працює з посиланнями на зразок `` `включено: & INCLUDED! Include Internal.yaml merge: <<: *
INCLEDED`

2

Я роблю кілька прикладів для вашої довідки.

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

вихід

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

Оновлення 2

і ви можете комбінувати його, як це

# 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.


1

Я думаю, що рішення, яке використовує @ maxy-B, виглядає чудово. Однак мені це не вдалося з вкладеними включеннями. Наприклад, якщо config_1.yaml включає config_2.yaml, який включає config_3.yaml, виникла проблема з завантажувачем. Однак якщо ви просто наведіть новий клас навантажувача на себе на навантаженні, він працює! Зокрема, якщо ми замінимо стару функцію _include на дуже дещо змінену версію:

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

Після роздуму я погоджуюся з іншими коментарями, що вкладене завантаження не підходить для yaml в цілому, оскільки вхідний потік може не бути файлом, але це дуже корисно!


1

Стандарт YML цього не робить визначає спосіб цього зробити. І ця проблема не обмежує себе YML. JSON має ті ж обмеження.

Багато програм, які використовують конфігурації на основі YML або JSON, з часом стикаються з цією проблемою. І коли це відбувається, вони складають власну умову .

наприклад, для визначень API-файлів:

$ref: 'file.yml'

наприклад для конфігурацій компонування докера:

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

Крім того, якщо ви хочете розділити вміст файлу yml на кілька файлів, як дерево вмісту, ви можете визначити власну конвенцію про структуру папок і використовувати (існуючий) сценарій злиття.



0

Стандартна YAML 1.2 не включає в себе цю функцію. Тим не менш, багато реалізацій передбачають деяке розширення для цього.

Я представляю спосіб її досягнення за допомогою Java та snakeyaml:1.24(бібліотека Java для розбору / видачі файлів YAML), що дозволяє створити спеціальний тег YAML для досягнення наступної мети (ви побачите, що я використовую його для завантаження тестових наборів, визначених у декількох файлах YAML і що я змусив його працювати як список включень для цільового test:вузла):

# ... yaml prev stuff

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

# ... more yaml document

Ось однокласна Java, яка дозволяє обробляти !includeтег. Файли завантажуються з classpath (каталог ресурсів 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));
                }
            }
        }

    }

}

0

За допомогою Yglu ви можете імпортувати такі файли, як цей:

A.yaml

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

Б.ямл

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

Як $importце функція, ви також можете передавати вираз як аргумент:

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

Це дало б такий самий результат, як і вище.

Відмова: Я є автором Yglu.


-1

Завдяки Symfony , обробка ямлу опосередковано дозволить вкладати файли yaml. Хитрість полягає у використанні цього parametersваріанту. наприклад:

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%"

Результат буде таким же, як:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"

-6

Можливо, він не підтримувався, коли задавали питання, але ви можете імпортувати інший файл YAML в один:

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

Хоча у мене немає жодної онлайн-довідки, але це працює для мене.


4
Це взагалі не робить ніякого включення. Він створює відображення з послідовністю, що складається з одного рядка "/your_location_to_yaml_file/Util.area.yaml", як значення для ключа imports.
Антон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.