Объединить YAML-файлы с переопределением значений в элементах списка - PullRequest
0 голосов
/ 22 октября 2019

Я хотел бы объединить два файла YAML, которые содержат элементы списка. (A) и (B) объединены в новый файл (C).

Я хотел бы переопределить существующие значения атрибутов записей списка в (A), если они также определены в (B).

Я хотел бы добавить новые атрибуты в записи списка, если они не определены в (A), но определены в (B).

Я также хотел бы добавить новые записи в списке (B) какхорошо, если нет в (A).

YAML-файл A:

list:
  - id: 1
    name: "name-from-A"
  - id: 2
    name: "name-from-A"

YAML-файл B:

list:
  - id: 1
    name: "name-from-B"
  - id: 2
    title: "title-from-B"
  - id: 3
    name: "name-from-B"
    title: "title-from-B"

Объединенный YAML-файл (C), Iхотел бы произвести:

list:
  - id: 1
    name: "name-from-B"
  - id: 2
    name: "name-from-A"
    title: "title-from-B"
  - id: 3
    name: "name-from-B"
    title: "title-from-B"

Мне нужна эта функциональность в скрипте Bash, но мне может потребоваться Python в среде.

Есть ли какой-либо автономный процессор YAML (например, yq), который может делатьthis?

Как мне реализовать что-то подобное в скрипте Python?

Ответы [ 3 ]

1 голос
/ 23 октября 2019

Вы можете объединить файлы yaml, переданные в командной строке:

import sys
import yaml

def merge_dict(m_list, s):
    for m in m_list:
        if m['id'] == s['id']:
            m.update(**s)
            return
    m_list.append(s)

merged_list = []
for f in sys.argv[1:]:
    with open(f) as s:
        for source in yaml.safe_load(s)['list']:
            merge_dict(merged_list, source)

print(yaml.dump({'list': merged_list}), end='')

Результаты:

list:
- id: 1
  name: name-from-B
- id: 2
  name: name-from-A
  title: title-from-B
- id: 3
  name: name-from-B
  title: title-from-B
1 голос
/ 22 октября 2019

Вы можете использовать пакет ruamel.yaml для этого.

если у вас уже установлен Python, введите в терминале следующую команду:

pip install ruamel.yaml

pythonкод адаптирован с здесь . (проверено и отлично работает) :

import ruamel.yaml
yaml = ruamel.yaml.YAML()

#Load the yaml files
with open('/test1.yaml') as fp:
    data = yaml.load(fp)
with open('/test2.yaml') as fp:
    data1 = yaml.load(fp)
# dict to contain merged ids
merged = dict()

#Add the 'list' from test1.yaml to test2.yaml 'list'
for i in data1['list']:
    for j in data['list']:
        # if same 'id'
        if i['id'] == j['id']:
            i.update(j)
            merged[i['id']] = True

# add new ids if there is some
for j in data['list']:
    if not merged.get(j['id'], False):
        data1['list'].append(j)

#create a new file with merged yaml
with open('/merged.yaml', 'w') as yaml_file:
    yaml.dump(data1, yaml_file)
0 голосов
/ 23 октября 2019

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

Он основан на Ruamel.

Он обрабатывает многоуровневые списки и управляет не только слиянием элементов списка по индексу, но и путем правильной идентификации элемента.

Это сложнеечем я надеялся (он пересекает дерево YAML).

Сценарий и основные методы:

import ruamel.yaml
from ruamel.yaml.comments import CommentedMap, CommentedSeq


#
# Merges a node from B with its pair in A
#
# If the node exists in both A and B, it will merge
# all children in sync
#
# If the node only exists in A, it will do nothing.
#
# If the node only exists in B, it will add it to A and stops
#
# attrPath DOES NOT include attrName
#
def mergeAttribute(parentNodeA, nodeA, nodeB, attrName, attrPath):

    # If both is None, there is nothing to merge
    if (nodeA is None) and (nodeB is None):
        return

    # If NodeA is None but NodeB has value, we simply set it in A
    if (nodeA is None) and (parentNodeA is not None):
        parentNodeA[attrName] = nodeB
        return

    if attrPath == '':
        attrPath = attrName
    else:
        attrPath = attrPath + '.' + attrName

    if isinstance(nodeB, CommentedSeq):

        # The attribute is a list, we need to merge specially
        mergeList(nodeA, nodeB, attrPath)

    elif isinstance(nodeB, CommentedMap):

        # A simple object to be merged
        mergeObject(nodeA, nodeB, attrPath)

    else:
        # Primitive type, simply overwrites
        parentNodeA[attrName] = nodeB


#
# Lists object attributes and merges the attribute values if possible
#
def mergeObject(nodeA, nodeB, attrPath):

    for attrName in nodeB:

        subNodeA = None
        if attrName in nodeA:
            subNodeA = nodeA[attrName]

        subNodeB = None
        if attrName in nodeB:
            subNodeB = nodeB[attrName]

        mergeAttribute(nodeA, subNodeA, subNodeB, attrName, attrPath)


#
# Merges two lists by properly identifying each item in both lists
# (using the merge-directives).
#
# If an item of listB is identified in listA, it will be merged onto the item
# of listA
#
def mergeList(listA, listB, attrPath):

    # Iterating the list from B
    for itemInB in listB:

        itemInA = findItemInList(listA, itemInB, attrPath)

        if itemInA is None:
            listA.append(itemInB)
            continue

        # Present in both, we need to merge them
        mergeObject(itemInA, itemInB, attrPath)


#
# Finds an item in the list by using the appropriate ID field defined for that
# attribute-path.
#
# If there is no id attribute defined for the list, it returns None
#
def findItemInList(listA, itemB, attrPath):

    if attrPath not in listsWithId:
        # No id field defined for the list, only "dumb" merging is possible
        return None

    # Finding out the name of the id attribute in the list items
    idAttrName = listsWithId[attrPath]

    idB = None
    if idAttrName is not None:
        idB = itemB[idAttrName]

    # Looking for the item by its ID
    for itemA in listA:

        idA = None
        if idAttrName is not None:
            idA = itemA[idAttrName]

        if idA == idB:
            return itemA

    return None

# ------------------------------------------------------------------------------


yaml = ruamel.yaml.YAML()

# Load the merge directives
with open('merge-directives.yaml') as fp:
    mergeDirectives = yaml.load(fp)

listsWithId = mergeDirectives['lists-with-id']

# Load the yaml files
with open('a.yaml') as fp:
    dataA = yaml.load(fp)

with open('b.yaml') as fp:
    dataB = yaml.load(fp)

mergeObject(dataA, dataB, '')

# create a new file with the merged yaml
yaml.dump(dataA, file('c.yaml', 'w'))

Файл конфигурации помощника (merge-directives.yaml), который инструктирует об идентификацииэлементы в (даже многоуровневых) списках.

Для структуры данных в исходном вопросе требуется только запись конфигурации «list:« id »», но я включил некоторые другие ключи, чтобы продемонстрировать использование.

#
# Lists that contain identifiable elements.
#
# Each sub-key is a property path denoting the list element in the YAML 
# data structure.
#
# The value is the name of the attribute in the list element that
# identifies the list element so that pairing can be made.
#
lists-with-id:
    list: "id"
    list.sub-list: "id"
    a.listAttrShared: "name"

Пока еще не проверено, но есть два тестовых файла, которые тестируют более полно, чем в исходном вопросе.

a.yaml:

a:
    attrShared: value-from-a
    listAttrShared:
        - name: a1
        - name: a2
    attrOfAOnly: value-from-a
list:
    - id: 1
      name: "name-from-A"
      sub-list:
          - id: s1
            name: "name-from-A"
            comments: "doesn't exist in B, so left untouched"
          - id: s2
            name: "name-from-A"
      sub-list-with-no-identification:
          - "comment 1"
          - "comment 2"
    - id: 2
      name: "name-from-A"

b.yaml:

a:
    attrShared: value-from-b
    listAttrShared:
        - name: b1
        - name: b2
    attrOfBOnly: value-from-b
list:
    - id: 1
      name: "name-from-B"
      sub-list:
          - id: s2
            name: "name-from-B"
            title: "title-from-B"
            comments: "overwrites name in A with name in B + adds title from B"
          - id: s3
            name: "name-from-B"
            comments: "only exists in B so added to A's list"
      sub-list-with-no-identification:
          - "comment 3"
          - "comment 4"
    - id: 2
      title: "title-from-B"
    - id: 3
      name: "name-from-B"
      title: "title-from-B"
...