Jak scalić tablice YAML?

113

Chciałbym scalić tablice w YAML i załadować je przez ruby ​​-

some_stuff: &some_stuff
 - a
 - b
 - c

combined_stuff:
  <<: *some_stuff
  - d
  - e
  - f

Chciałbym mieć połączoną tablicę jako [a,b,c,d,e,f]

Otrzymuję błąd: nie znalazłem oczekiwanego klucza podczas analizowania mapowania bloku

Jak scalić tablice w YAML?

lfender6445
źródło
6
Dlaczego chcesz to zrobić w YAML, a nie w języku, w którym to analizujesz?
Patrick Collins
7
aby wysuszyć powielanie w bardzo dużym pliku yaml
lfender6445
4
To bardzo zła praktyka. Powinieneś czytać yamlów osobno, złożyć tablice razem w Rubim, a następnie zapisać je z powrotem w yaml.
sawa
74
Jak stara się być suchym złą praktyką?
krak3n
13
@PatrickCollins Znalazłem to pytanie, próbując zmniejszyć powielanie w moim pliku .gitlab-ci.yml i niestety nie mam kontroli nad parserem używanym przez GitLab CI :(
rink.attendant

Odpowiedzi:

42

Jeśli celem jest uruchomienie sekwencji poleceń powłoki, możesz to osiągnąć w następujący sposób:

# note: no dash before commands
some_stuff: &some_stuff |-
    a
    b
    c

combined_stuff:
  - *some_stuff
  - d
  - e
  - f

Jest to równoważne z:

some_stuff: "a\nb\nc"

combined_stuff:
  - "a\nb\nc"
  - d
  - e
  - f

gitlab-ci.ymlUżywałem tego na moim (aby odpowiedzieć @ rink.attendant.6 na komentarz do pytania).


Przykład roboczy, którego używamy do obsługi requirements.txtposiadania prywatnych repozytoriów z gitlab:

.pip_git: &pip_git
- git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com".insteadOf "ssh://[email protected]"
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts

test:
    image: python:3.7.3
    stage: test
    script:
        - *pip_git
        - pip install -q -r requirements_test.txt
        - python -m unittest discover tests

use the same `*pip_git` on e.g. build image...

gdzie requirements_test.txtzawiera np

-e git+ssh://[email protected]/example/[email protected]#egg=example

Jorge Leitao
źródło
3
Sprytny. Używam go teraz w naszym potoku Bitbucket. Dzięki
Dariop
* Końcowy myślnik nie jest tu wymagany, wystarczy rurka na końcu. * Jest to gorsze rozwiązanie, ponieważ gdy zadanie kończy się niepowodzeniem w przypadku bardzo długiej instrukcji wielowierszowej, nie jest jasne, które polecenie się nie powiodło.
Mina Luke
1
@MinaLuke, gorszy w porównaniu z czym? Żadna z aktualnych odpowiedzi nie daje sposobu na połączenie dwóch pozycji przy użyciu tylko yaml ... Ponadto w pytaniu nie ma nic, co by było powiedziane, że OP chce to wykorzystać w CI / CD. Wreszcie, gdy jest to używane w CI / CD, rejestrowanie zależy tylko od konkretnego używanego CI / CD, a nie od deklaracji yaml. Więc jeśli już, CI / CD, o którym mówisz, jest tym, który wykonuje złą robotę. Yaml w tej odpowiedzi jest ważny i rozwiązuje problem OP.
Jorge Leitao
@JorgeLeitao Myślę, że używasz go do łączenia reguł. Czy możesz podać działający przykład gitlabci? Próbowałem czegoś w oparciu o twoje rozwiązanie, ale zawsze otrzymuję błąd weryfikacji.
niels
@niels, dodałem przykład z działającym przykładem gitlabci. Zauważ, że niektóre środowiska IDE oznaczają ten yaml jako nieprawidłowy, mimo że tak nie jest.
Jorge Leitao
26

Aktualizacja: 01.07.2019 14:06:12

  • Uwaga : inna odpowiedź na to pytanie została znacznie zmieniona wraz z aktualizacją podejść alternatywnych .
    • Ta zaktualizowana odpowiedź wspomina o alternatywie dla obejścia w tej odpowiedzi. Został dodany do sekcji Zobacz też poniżej.

Kontekst

Ten post zakłada następujący kontekst:

  • python 2.7
  • parser YAML w pythonie

Problem

lfender6445 chce scalić dwie lub więcej list w pliku YAML i sprawić, by te scalone listy pojawiały się jako jedna lista podczas analizowania.

Rozwiązanie (obejście)

Można to uzyskać po prostu przypisując kotwice YAML do mapowań, gdzie żądane listy pojawiają się jako elementy podrzędne mapowań. Istnieją jednak zastrzeżenia (patrz „Pułapki” poniżej).

W poniższym przykładzie mamy trzy mapowania ( list_one, list_two, list_three) oraz trzy kotwice i aliasy, które odwołują się do tych mapowań tam, gdzie jest to stosowne.

Kiedy plik YAML jest ładowany do programu, otrzymujemy listę, którą chcemy, ale może ona wymagać niewielkiej modyfikacji po załadowaniu (patrz pułapki poniżej).

Przykład

Oryginalny plik YAML

  list_one: & id001
   - a
   - b
   - c

  list_two: & id002
   - e
   - f
   - g

  list_three: & id003
   - godz
   - ja
   - j

  list_combined:
      - * id001
      - * id002
      - * id003

Wynik po YAML.safe_load

## list_combined
  [
    [
      "za",
      "b",
      "do"
    ],
    [
      "mi",
      "fa",
      "sol"
    ],
    [
      "h",
      "ja",
      "jot"
    ]
  ]

Pułapki

  • to podejście tworzy zagnieżdżoną listę list, która może nie być dokładnie pożądanym wyjściem, ale można ją przetworzyć końcowo metodą spłaszczenia
  • to zwykłe zastrzeżenia do kotew YAML i aliasów ubiegać się o wyjątkowości i porządku deklaracji

Wniosek

Takie podejście umożliwia tworzenie scalonych list przy użyciu aliasu i funkcji kotwicy YAML.

Chociaż wynikiem wyjściowym jest zagnieżdżona lista list, można ją łatwo przekształcić za pomocą tej flattenmetody.

Zobacz też

Zaktualizowano alternatywne podejście autorstwa @Anthon

Przykłady flattenmetody

dreftymac
źródło
21

To nie zadziała:

  1. scalanie jest obsługiwane tylko przez specyfikacje YAML dla mapowań, a nie dla sekwencji

  2. całkowicie mieszasz rzeczy, mając klucz scalania, << po którym następuje separator klucz / wartość :i wartość, która jest odwołaniem, a następnie kontynuujesz pracę z listą na tym samym poziomie wcięcia

To nie jest poprawne YAML:

combine_stuff:
  x: 1
  - a
  - b

Więc twoja przykładowa składnia nie miałaby nawet sensu jako propozycja rozszerzenia YAML.

Jeśli chcesz zrobić coś takiego, jak scalanie wielu tablic, możesz rozważyć składnię taką jak:

combined_stuff:
  - <<: *s1, *s2
  - <<: *s3
  - d
  - e
  - f

gdzie s1, s2, s3są kotwice na sekwencji (nie pokazane), które chcesz połączyć w nowej sekwencji, a następnie mieć d, ea f załączone do tego. Ale YAML najpierw rozwiązuje ten rodzaj głębi struktur, więc podczas przetwarzania klucza scalającego nie jest dostępny prawdziwy kontekst. Nie ma dostępnej tablicy / listy, do której można by dołączyć przetworzoną wartość (zakotwiczoną sekwencję).

Możesz przyjąć podejście zaproponowane przez @dreftymac, ale ma to ogromną wadę, że w jakiś sposób musisz wiedzieć, które zagnieżdżone sekwencje spłaszczyć (tj. Znając "ścieżkę" od korzenia załadowanej struktury danych do sekwencji nadrzędnej), lub że rekurencyjnie przechodzisz po załadowanej strukturze danych, szukając zagnieżdżonych tablic / list i bezkrytycznie spłaszczasz je wszystkie.

Lepszym rozwiązaniem IMO byłoby użycie tagów do ładowania struktur danych, które wykonują spłaszczanie za Ciebie. Pozwala to na wyraźne oznaczenie tego, co należy spłaszczyć, a co nie oraz daje pełną kontrolę nad tym, czy spłaszczanie to odbywa się podczas ładowania, czy podczas dostępu. To, który z nich wybrać, to kwestia łatwości wdrożenia oraz wydajności w zakresie czasu i przestrzeni magazynowej. Jest to ten sam kompromis, który musi być wykonany za wdrożenie seryjnej kluczową cechę i nie ma jednego rozwiązania, które jest zawsze najlepszy.

Np. Moja ruamel.yamlbiblioteka używa dyktowania typu brute force merge-dicts podczas ładowania, kiedy używa swojego bezpiecznego programu ładującego, co skutkuje połączonymi słownikami, które są normalnymi dyktami Pythona. To scalanie musi być wykonane z góry i powiela dane (nieefektywne pod względem miejsca), ale jest szybkie w wyszukiwaniu wartości. Korzystając z modułu ładującego w obie strony, chcesz mieć możliwość zrzucania połączeń nierozłącznych, więc należy je przechowywać oddzielnie. Dykt, podobnie jak struktura danych załadowana w wyniku ładowania w obie strony, zajmuje mało miejsca, ale jest wolniejszy w dostępie, ponieważ musi spróbować wyszukać klucz, którego nie znaleziono w samym dyktowaniu w połączeniach (i to nie jest buforowane, więc za każdym razem). Oczywiście takie względy nie są bardzo ważne w przypadku stosunkowo małych plików konfiguracyjnych.


Poniższy schemat implementuje schemat scalania dla list w Pythonie przy użyciu obiektów ze znacznikami, flatten które w locie powracają do elementów, które są listami i oznaczane toflatten. Używając tych dwóch tagów możesz mieć plik YAML:

l1: &x1 !toflatten
  - 1 
  - 2
l2: &x2
  - 3 
  - 4
m1: !flatten
  - *x1
  - *x2
  - [5, 6]
  - !toflatten [7, 8]

(użycie sekwencji typu flow vs block jest całkowicie dowolne i nie ma wpływu na ładowany wynik).

Podczas iteracji po elementach, które są wartością klucza, m1to „powraca” do sekwencji oznaczonych tagiem toflatten, ale wyświetla inne listy (z aliasami lub bez) jako pojedynczy element.

Jednym z możliwych sposobów osiągnięcia tego celu za pomocą kodu Pythona jest:

import sys
from pathlib import Path
import ruamel.yaml

yaml = ruamel.yaml.YAML()


@yaml.register_class
class Flatten(list):
   yaml_tag = u'!flatten'
   def __init__(self, *args):
      self.items = args

   @classmethod
   def from_yaml(cls, constructor, node):
       x = cls(*constructor.construct_sequence(node, deep=True))
       return x

   def __iter__(self):
       for item in self.items:
           if isinstance(item, ToFlatten):
               for nested_item in item:
                   yield nested_item
           else:
               yield item


@yaml.register_class
class ToFlatten(list):
   yaml_tag = u'!toflatten'

   @classmethod
   def from_yaml(cls, constructor, node):
       x = cls(constructor.construct_sequence(node, deep=True))
       return x



data = yaml.load(Path('input.yaml'))
for item in data['m1']:
    print(item)

które wyjścia:

1
2
[3, 4]
[5, 6]
7
8

Jak widać, w sekwencji, która wymaga spłaszczenia, możesz użyć aliasu do sekwencji ze znacznikiem lub możesz użyć sekwencji ze znacznikiem. YAML nie pozwala na:

- !flatten *x2

, tj. oznaczyć zakotwiczoną sekwencję, ponieważ zasadniczo uczyniłoby to inną strukturę danych.

Używanie znaczników jawnych jest lepsze w IMO niż magia, jak w przypadku kluczy scalających YAML <<. Jeśli nic innego, musisz teraz przechodzić przez obręcze, jeśli masz plik YAML z mapowaniem, który ma klucz <<, którego nie chcesz zachowywać się jak klucz scalający, np. Kiedy wykonujesz mapowanie operatorów C do ich opisów w języku angielskim (lub innym języku naturalnym).

Anthon
źródło
9

Jeśli chcesz scalić tylko jedną pozycję na liście, możesz to zrobić

fruit:
  - &banana
    name: banana
    colour: yellow

food:
  - *banana
  - name: carrot
    colour: orange

która daje

fruit:
  - name: banana
    colour: yellow

food:
  - name: banana
    colour: yellow
  - name: carrot
    colour: orange
Tamlyn
źródło
-4

Możesz scalić mapowania, a następnie przekonwertować ich klucze na listę, pod następującymi warunkami:

  • jeśli używasz szablonów jinja2 i
  • jeśli kolejność przedmiotów nie jest ważna
some_stuff: &some_stuff
 a:
 b:
 c:

combined_stuff:
  <<: *some_stuff
  d:
  e:
  f:

{{ combined_stuff | list }}
sm4rk0
źródło
Co jest nie tak z tą odpowiedzią? Nie mam nic przeciwko negatywnym głosom, jeśli są argumentowane. Zachowam odpowiedź dla ludzi, którzy mogą z niej skorzystać.
sm4rk0
3
Prawdopodobnie dlatego, że ta odpowiedź opiera się na szablonie jinja2, gdy pytanie prosi o zrobienie tego w yml. jinja2 wymaga środowiska Python, które przynosi efekt przeciwny do zamierzonego, jeśli OP próbuje OSUSZAĆ. Ponadto wiele narzędzi CI / CD nie akceptuje etapu tworzenia szablonu.
Jorge Leitao
Dzięki @JorgeLeitao. To ma sens. Nauczyłem się YAML i Jinja2 razem podczas opracowywania podręczników i szablonów Ansible i nie mogę myśleć o jednym bez drugiego
sm4rk0