Как я могу включить файл YAML в другой?

289

Таким образом, у меня есть два файла YAML, «A» и «B», и я хочу, чтобы содержимое A было вставлено в B, либо встроено в существующую структуру данных, например, массив, или как дочерний элемент элемента, например, значение для определенного ключа хеша.

Это вообще возможно? Как? Если нет, какие-либо указатели на нормативную ссылку?

KCH
источник
1
Я недавно наткнулся на HiYaPyCo для Python, который делает именно это. Вы можете объединять разные файлы YAML вместе. Это очень хороший модуль Python, который стоит знать.
nowox
Смотрите также: stackoverflow.com/questions/41620674/use-placeholder-in-yaml
dreftymac

Ответы:

327

Нет, YAML не содержит каких-либо операторов «import» или «include».

jameshfisher
источник
8
Вы можете создать обработчик! Include <filename>.
кларкеванс
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 include (загрузчик, узел):" "" Включить другой файл YAML. "" "Filename = loader.construct_scalar (узел) data = yaml.load (open (filename))`
Алексей Раманау
Корневой глобал существует так, что относительный включает работу на любой глубине, например, когда включенные файлы, находящиеся в другом каталоге, включают файл относительно этого каталога. Абсолютные включения тоже должны работать. Вероятно, есть более чистый способ сделать это без глобальной переменной, возможно, с помощью пользовательского класса yaml.Loader.
Джош Боде
2
Также возможно иметь что-то вроде этого: foo.yaml: a: bla bar.yaml: `! Include foo.yaml b: blubb`, чтобы результат был:` {'a': bla, 'b': blubb}
Мартин
3
Это должен быть принятый ответ. Кроме того, для безопасности, вы должны использовать yaml.safeload вместо yaml.load, чтобы избежать специально созданного yaml от владения вашим сервисом.
danielpops
1
@JoshBode это должно работать для вас: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops
32

Если вы используете версию YAML от Symfony , это возможно, например так:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }
daveaspinall
источник
34
Это зависит от того, как Symfony интерпретирует YAML, а не часть самого YAML.
jameshfisher
9
Да, именно поэтому я разместил ссылку на документы Symfony. Вопрос спрашивает: «Это вообще возможно? Как?» ... вот как. Не вижу причин для понижения.
daveaspinall
4
Я не отрицал тебя; Я просто указываю, что это специфично для Symfony YAML.
jameshfisher
9
Не существует "версии YAML для Symfony" ... это просто библиотека, совместимая с 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)))

Единственным недостатком является то, что переменные во include всегда будут переопределять переменные в main, и нет никакого способа изменить этот приоритет, изменив положение, в котором в файле main.yml присутствует оператор "includes:".

С другой стороны, YAML не поддерживает include, поскольку он на самом деле не разработан так же эксклюзивно, как разметка на основе файлов. Что будет означать включение, если вы получите его в ответ на запрос AJAX?

CLH
источник
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}
xqliang
источник
Это на самом деле не отвечает на вопрос. Это относится к решению Python, а не к использованию стандартного формата YAML.
oligofren
@oligofren Обработчики пользовательских тегов - это функция YAML, позволяющая анализаторам расширять YAML для указания типов и реализовывать пользовательские поведения, подобные этим. Для самой спецификации YAML было бы
Антон Строгонов
@AntonStrogonoff Спасибо, что обратили на это мое внимание. Не могли бы вы указать мне на такое место в RFC? В нем нет упоминания слова «обычай». Ссылка 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параметром.
Woltan
1
К сожалению, это не работает с такими ссылками, как `` `includes: & INCLUDED! Include inner.yaml merge: <<: *
INCLUDED`
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.
Carson
источник
1

К сожалению, YAML не предоставляет этого в своем стандарте.

Но если вы используете Ruby, есть жемчужина, предоставляющая требуемую функциональность путем расширения библиотеки ruby ​​YAML: https://github.com/entwanderer/yaml_extend

user8419486
источник
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 в целом, поскольку входной поток может не быть файлом, но это очень полезно!

PaddyM
источник
1

Стандарт YML не определяет способ сделать это. И эта проблема не ограничивается YML. JSON имеет те же ограничения.

Многие приложения, использующие конфигурации на основе YML или JSON, в конечном итоге сталкиваются с этой проблемой. И когда это происходит, они составляют свое собственное соглашение .

например, для определения API Swagger:

$ref: 'file.yml'

например, для докера составьте конфигурации:

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

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

bvdb
источник
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')

B.yaml

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

Как $importи функция, вы также можете передать выражение в качестве аргумента:

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

Это даст тот же результат, что и выше.

Отказ от ответственности: я автор Yglu.

lbovet
источник
-1

С Symfony его обработка yaml косвенно позволит вам вкладывать файлы 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"
jxmallett
источник
-6

Возможно, это не было поддержано, когда был задан вопрос, но вы можете импортировать другой файл YAML в один:

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

Хотя у меня нет онлайн-ссылки, но это работает для меня.

Sankalp
источник
4
Это не делает ничего, включая вообще. Он создает отображение с последовательностью, состоящей из одной строки "/your_location_to_yaml_file/Util.area.yaml", в качестве значения для ключа imports.
Антон