Introduction à YAML

From DJSON
Jump to: navigation, search

Michel Casabianca casa@sweetohm.net

Cet article est une introduction à YAML, un langage permettant de représenter des données structurées, comme le ferait XML par exemple, mais de manière plus naturelle et moins verbeuse. On y verra une description de la syntaxe de YAML ainsi que des exemples en Java et Python.

On trouvera une archive ZIP avec cet article au format PDF ainsi que les exemples à l'adresse : http://www.sweetohm.net/arc/introduction-yaml.zip .

Qu'est ce que YAML ?

Le nom YAML veut dire "YAML Ain't Markup Language", soit "YAML n'est pas un langage de balises". Si cela met d'emblée des distances avec XML, cela ne nous dit pas ce qu'est YAML. YAML est, d'après sa spécification , un langage de sérialisation de données conçu pour être lisible par des humains et travaillant bien avec les langage de programmation modernes pour les tâches de tous les jours.

Concrètement, on pourrait noter la liste des ingrédients pour un petit déjeuner de la manière suivante :

- croissants
- chocolatines 
- jambon 
- oeufs

Ceci est un fichier YAML valide qui représente une liste de chaînes de caractères. Pour s'en convaincre, nous pouvons écrire le script Python suivant qui parse le fichier, dont le nom est passé sur la ligne de commande, et affiche le résultat :

 #!/usr/bin/env python
 # encoding: UTF-8

 import sys
 import yaml

 print yaml.load(open(sys.argv[1]))

Ce script produira le résultat suivant :

['croissants', 'chocolatines', 'jambon', 'oeufs']

Ce qui veut dire que le résultat de ce parsing est une liste Python contenant les chaînes de caractères appropriées ! Le parseur est donc capable de restituer des structures de données naturelles du langage utilisé pour le parsing.

Écrivons maintenant un compte à rebours :

- 3 
- 2 
- 1 
- 0

Nous obtenons le résultat suivant :

[3, 2, 1, 0]

C'est toujours une liste, mais le parseur a reconnu chaque élément comme étant un entier et non une simple chaîne de caractères comme dans l'exemple précédent. Le parseur est donc capable de distinguer des types de données tels que chaînes de caractères et entiers, nombres à virgule flottante et dates. On utilise pour ce faire une syntaxe naturelle : 3 est reconnu comme un entier alors que croissants ne l'est pas car ce dernier ne peut être converti ni en entier, ni en nombre à virgule flottante, ni en tout autre type reconnu par YAML. Pour forcer YAML à interpréter 3 comme une chaîne de caractères, on peut l'entourer de guillemets.

YAML peut aussi reconnaître des tableaux associatifs, ainsi on pourrait noter une commande de petit déjeuner de la manière suivante :

croissants: 30
chocolatines: 30   
jambon: 0
oeufs: 0

Qui sera chargé de la manière suivante :

{'chocolatines': 30, 'croissants': 30, 'jambon': 0, 'oeufs': 0}

En combinant les types de données de base dans les collections reconnues par YAML, on peut représenter quasiment toute structure de données. D'autre part, la représentation textuelle de ces données est très lisible et quasiment naturelle.

Il est aussi possible de réaliser l'opération inverse, à savoir sérialiser des structures de données en mémoire sous forme de texte. Dans l'exemple suivant, nous écrivons sur la sortie standard un Dictionnaire Python :

 #!/usr/bin/env python # encoding: UTF-8

 import yaml

 recette = {
     'nom': 'sushi',
     'ingredients': ['riz', 'vinaigre', 'sucre', 'sel', 'thon', 'saumon'], 
     'temps de cuisson': 10,
     'difficulte': 'difficile'
 } 
 print yaml.dump(recette)

Qui produira la sortie suivante :

difficulte: difficile
ingredients: [riz, vinaigre, sucre, sel, thon, saumon] 
nom: sushi
temps de cuisson: 10

Syntaxe de base

Après cette brève introduction, voici une description plus exhaustive de la syntaxe YAML.

Scalaires

Les scalaires sont l'ensemble des types YAML qui ne sont pas des collections (liste ou tableau associatif). Ils peuvent être représentés par une liste de caractères Unicode. Voici une liste des scalaires reconnus par les parseurs YAML :

Chaîne de caractères

Voici un exemple :

- Chaîne
- "3"
- Chaîne sur
  une ligne
- "Guillemets doubles\t"
- 'Guillemets simples\t'

Qui est parsé de la manière suivante :

[u'Cha\xeene', '3', u'Cha\xeene sur une ligne', 
  'Guillemets doubles\t', 'Guillemets simples\\t']

Le résultat de ce parsing nous amène aux commentaires suivants :

• Les caractères accentués sont gérés, en fait, l'Unicode est géré de manière plus générale. • Les retours à la ligne ne sont pas pris en compte dans les chaînes, ils sont gérés comme en HTML ou XML, à savoir qu'ils sont remplacés par des espaces. • Les guillemets doubles gèrent les caractères d'échappement, comme \t pour la tabulation par exemple. • Les guillemets simples ne gèrent pas les caractères d'échappement qui sont transcrits de manière littérale. • La liste des caractères d'échappement gérés par YAML comporte les valeurs classiques, mais aussi nombre d'autres que l'on pourra trouver dans la spécification YAML .

D'autre part, il est possible d'écrire des caractères Unicode à l'aide des notations suivantes :

• \xNN : pour écrire des caractères Unicode sur 8 bits, où NN est un nombre hexadécimal. • \uNNNN : pour des caractères Unicode sur 16 bits. • \UNNNNNNNN : pour des caractères Unicode sur 32 bits.

Entiers

Voici quelques exemples :

 canonique:      12345
 decimal:        +12_345 
 sexagesimal:    3:25:45
 octal:            030071
 hexadecimal:   0x3039

Qui est parsé de la manière suivante :

{'octal': 12345, 'hexadecimal': 12345, 'canonique': 12345,
 'decimal': 12345, 'sexagesimal': 12345}

Nous constatons que les notations les plus courantes des langages de programmation (comme l'octal ou l'hexadécimal) sont gérées. A noter que toutes ces notations seront reconnues comme identiques par un parseur YAML et par conséquent seront équivalentes comme clef d'un tableau associatif par exemple.

Nombres à virgule flottante

Voyons les différentes notations pour ces nombres :

canonique:         1.23015e+3
exponentielle:     12.3015e+02
sexagesimal:       20:30.15
fixe:              1_230.15
infini negatif:    -.inf
pas un nombre:     .NaN

Ce qui est parsé en :

{'pas un nombre': nan, 'sexagesimal': 1230.1500000000001,
 'exponentielle': 1230.1500000000001, 'fixe': 1230.1500000000001,
 'infini negatif': -inf, 'canonique': 1230.1500000000001}

Les notations classiques sont gérées ainsi que les infinis et les valeurs qui ne sont pas des nombres.

Dates

YAML reconnaît aussi des dates :

canonique:   2001-12-15T02:59:43.1Z
iso8601:     2001-12-14t21:59:43.10-05:00
espace:      2001-12-14 21:59:43.10 -5
date:        2002-12-14

Qui sont parsées de la manière suivante :

{'date': datetime.date(2002, 12, 14),
 'iso8601': datetime.datetime(2001, 12, 15, 2, 59, 43, 100000), 
 'canonique': datetime.datetime(2001, 12, 15, 2, 59, 43, 100000),
 'espace': datetime.datetime(2001, 12, 15, 2, 59, 43, 100000)}

Les types résultant du parsing dépendent du langage et du parseur, mais correspondent à des types naturels pour les temps considérés.

Divers

Il existe d'autres scalaires reconnus par les parseurs YAML :

nul: null
nul bis: ~
vrai: true
vrai bis: yes
vrai ter: on
faux: false
faux bis: no
faux ter: off

Qui sera parsé en :

{'faux bis': False, 'vrai ter': True, 'vrai bis': True,
 'faux ter': False, 'nul': None, 'faux': False,
 'nul bis': None, 'vrai': True}

Bien sûr, le type des valeurs parsées dépend du langage et d'autres valeurs spéciales peuvent être reconnues suivant les langages et les parseurs. Par exemple, le parseur Ruby reconnaît les symboles (notés :symbole par exemple) et les parse en symboles Ruby.

Collections

Il existe deux types de collections reconnues par YAML : les listes et les tableaux associatifs.

Listes

Ce sont des listes ordonnées, et qui peuvent contenir plusieurs éléments identiques (par opposition aux ensembles). Les éléments d'une liste sont identifiés par un tiret, comme suit :

- croissants au
  beurre
- chocolatines
- jambon
- oeufs

Les éléments de la liste sont distingués grâce à l'indentation : le premier élément est identé de manière à ce que sa deuxième ligne soit reconnue comme faisant partie du premier élément de la liste. Cette syntaxe peut être comparée à celle de Python, si ce n'est qu'en YAML, les caractères de tabulation sont strictement interdits pour l'indentation . Cette dernière règle est importante et source de nombreuses erreurs de parsing. Il est important de paramétrer son éditeur de manière à interdire les tabulations pour l'indentation des fichiers YAML.

Il existe une notation alternative pour les listes, semblable à celle des langages Python ou Ruby :

[croissants, chocolatines, jambon, oeufs]

Cette notation, dite en flux , est plus compacte et permet parfois de gagner en lisibilité ou compacité.

Tableaux associatifs

Appelés Map ou Dictionnaires dans certains langages, ils associent une valeur à une clef :

croissants: 30
chocolatines: 30
jambon: 0
oeufs: 0

La notation en flux est la suivante :

{ croissants: 2, chocolatines: 1, jambon: 0, oeufs: 2}

Qui est parsé de la même manière. Cette notation est identique à celle de Python ou Javascript et se rapproche de celle utilisée par Ruby. A noter qu'il est question que Ruby 2 utilise aussi cette notation.

Commentaires

Il est possible d'inclure des commentaires dans un document de la même manière que dans la plupart des langages de script :

#   commentaire
-   Du texte
#   autre commentaire
-   Autre texte

A noter que ces commentaires ne doivent (et ne peuvent) contenir d'information utile au parsing dans la mesure où ils ne sont pas accessibles, généralement, au code client du parser.

Documents multiples

Dans un même fichier ou flux, on peut insérer plusieurs documents YAML å la suite, en les faisant commencer par une ligne composée de trois tirets ( --- ) et en les termiant d'une ligne de trois points ( ... ) comme dans l'exemple ci-dessous :

 ---
 premier document
 ...
 ---
 deuxième document
 ...

A noter que par défaut, les parsers YAML attendent un document par fichier et peuvent émettre une erreur s'ils rencontrent plus d'un document. Il faut alors utiliser une fonction particulière capable de parser des documents multiples (comme yaml.load_all() pour PyYaml par exemple).

On peut alors extraire ces documents de manière séquentielle du flux.

Syntaxe avancée

Avec la section précédente, nous avons vu le minimum vital pour se débrouiller avec YAML. Nous allons maintenant aborder des notions plus avancées dont on peut souvent se passer dans un premier temps.

Références

Les références YAML sont semblables aux pointeurs des langages de programmation. Par exemple :

lundi:        &p 'des patates'
mardi:        *p 
mercredi:     *p 
jeudi:        *p
vendredi:     *p
samedi:       *p
dimanche:     *p

Donne, après parsing :

{'mardi': 'des patates', 'samedi': 'des patates', 
 'jeudi': 'des patates', 'lundi': 'des patates',
 'vendredi': 'des patates', 'dimanche': 'des patates',
 'mercredi': 'des patates'}

A noter qu'un alias, indiqué par une astérisque * , doit pointer vers une ancre valide, indiquée par une esperluette & , sans quoi il en résulte une erreur de parsing. Ainsi le fichier suivant doit provoquer une erreur lors du parsing :

*foo

Tags

Les tags sont les indicateurs du type de données. Par défaut, il n'est pas nécessaire d'indiquer le type des données qui est déduit de leur forme. Cependant, dans certains cas, il peut être nécessaire de forcer le type d'une donnée et YAML définit les types par défaut suivants :

 null:         !!null
 integer:      !!int       3
 float:        !!float     1.2
 string:       !!str       string
 boolean:      !!bool      true
 binary:       !!binary    dGVzdA==
 map:          !!map       { key: value }
 seq:          !!seq       [ element1, element2 ]
 ensemble:    !!set        { element1, element2 }
 omap:         !!omap      [ key: value ]

Les tags correspondants commencent par deux points d'exclamation. Lors du parsing, on obtient les type suivants en Python :

 {'binary': 'test', 'string': 'string',
  'seq': ['element1', 'element2'],
  'map': {'key': 'value'},
  'float': 1.2,
  'boolean': True,
  'omap': [('key', 'value')],
  None: None,
  'integer': 3,
  'ensemble': set(['element1', 'element2'])}

A noter les deux types supplémentaires :

• L'ensemble : il n'est pas ordonné et ne peut comporter de doublon. • Le tableau associatif ordonné : c'est un tableau associatif dont les entrées sont ordonnées.

L'utilité des tags pour ces types par défaut est limitée. La vrai puissance des tags réside dans la possibilité de définir ses propres tags pour ses propres types de données.

Par exemple, on pourrait définir son propre type pour les personnes, comportant deux champs : le nom et le prénom. On doit tout d'abord déclarer le tag au début du document, puis on peut l'utiliser dans la suite, comme dans cet exemple :

 %TAG !personne! tag:foo.org,2004:bar
 ---
 - !personne
     nom: Simpson
     prenom: Omer
 - !personne
     nom: Simpson
     prenom: Bart

Nous verrons plus loin comment utiliser les tags avec les APIs Java et Python pour désérialiser des structures YAML en types de donnés personnalisés.

Il est aussi possible de ne pas déclarer le tag et de l'expliciter dans le document, de la manière suivante :

- !<tag:foo.org,2004:bar>
    nom: Simpson
    prenom: Omer
- !<tag:foo.org,2004:bar>
    nom: Simpson
    prenom: Bart

Directives

Les directives donnent des instructions au parser. Il en existe deux :

TAG

Comme vu précédemment, déclare un tag dans le document.

YAML

Indique la version de YAML du document. Doit être en en-tête du document, comme dans l'exemple ci-dessous :

%YAML 1.1
---
test

Un parser doit refuser de traiter un document d'une version majeure supérieure. Par exemple, un parser 1.1 devrait refuser de parser un document en version YAML 2.0 . Il devrait émettre un warning si on lui demande de parser un document de version mineure supérieure, comme 1.2 par exemple. Il doit parser sans protester toutes les versions égales ou inférieures, telles que 1.1 et 1.0 .

Jeu de caractères et encodage

Un parser YAML doit accepter tout caractère Unicode, à l'exception de certains caractères spéciaux . Ces caractères peuvent être encodés en UTF-8 (encodage par défaut), UTF-16 ou UTF-32 . Les parsers YAML sont capables de déterminer l'encodage du texte en examinant le premier caractère. Il est donc impossible d'utiliser tout autre encodage dans un fichier YAML et en particulier ISO-8859-1.

APIs YAML

Nous allons maintenant jouer avec les principales APIs YAML.

JYaml

JYaml est une bibliothèque OpenSource pour manipuler les documents YAML en Java. Le projet est hébergé par SourceForge et on trouvera sa page à l'adresse http://jyaml.sourceforge.net/ . On trouvera sur ce site un tutoriel ainsi que les références de l'API .

Utilisation de base

JYaml effectue un mapping par défaut des structures YAML en objets Java standards : il associe une liste à une instance de java.util.ArrayList , un tableau associatif à une instance de java.util.HashMap et les types primitifs de YAML à leur contrepartie Java.

Ainsi, pour charger un fichier YAML dans un objet Java, on écrira le code suivant :

Object object = Yaml.load(new File("object.yml"));

Par exemple, le fichier suivant :

- Un
- 2
- { trois: 3.0, quatre: true }

Pourra être chargé en mémoire et affiché dans le terminal avec le source suivant :

 package jyaml;

 import java.io.File;
 import org.ho.yaml.Yaml;

 public class Load {

     public static void main(String[] args)
         throws Exception {
         String filename = "test/object.yml";
         if (args.length > 0) filename = args[0];
         System.out.println(Yaml.load(new File(filename)));
     }
 }

Cela affichera dans le terminal :

[Un, 2, {quatre=true, trois=3.0}]

Inversement, on peut sérialiser un objet Java dans un fichier YAML de la manière suivante :

Yaml.dump(object, new File("dump.yml"));

Ainsi, on pourra par exemple sérialiser une structure d'objets Java avec le code suivant :

 package jyaml;

 import   java.io.File;
 import   java.util.ArrayList;
 import   java.util.HashMap;
 import   java.util.List;
 import   java.util.Map;
 import   org.ho.yaml.Yaml;

 public class Dump {

     public static void main(String[] args)
         throws Exception {
         List<Object> object = new ArrayList<Object>();
		 object.add("Un");
		 object.add(2);
         Map<String, Object> map = new HashMap<String, Object>();
		 map.put("trois", 3.0);
		 map.put("quatre", true);
		 object.add(map);
         Yaml.dump(object, new File("test/dump.yml"));
     }
 }

Ce code produira le fichier suivant :

 ---

 - Un
 - 2
 - !java.util.HashMap
 quatre: true
 trois: !java.lang.Double 3.0

A noter que ce dump est un peu décevant dans la mesure où certains types standards de YAML (comme les tableaux associatifs et les nombres à virgule flottante) sont sérialisés en types Java (comme java.util.HashMap et java.lang.Double ). Un tel fichier ne sera pas chargé correctement en utilisant un autre langage de programmation (voire même une autre implémentation en Java).

Usage avancé

Nous pouvons aussi travailler avec des types qui ne sont pas génériques et ainsi charger des instances de classes Java à partir de fichiers YAML.

La première solution consiste à indiquer le type des objets avec des tags YAML. Ainsi, le fichier YAML suivant :

 --- !jyaml.Commande id: test123 articles:

 - !jyaml.Article 
   id: test456
   prix: 3.5
   quantite: 1
 - !jyaml.Article
   id: test567
   prix: 2.0
   quantite: 2

Sera-t-il chargé en utilisant les classes suivantes :

 package jyaml;

 public class Commande {

     private String id;
     private Article[] articles;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Article[] getArticles() {
        return articles;
    }

    public void setArticles(Article[] articles) {
        this.articles = articles;
    }
   
    public String toString() {
        StringBuffer buffer = new StringBuffer("[Commande id='")
            .append(id)
            .append("', articles='");
        for (int i=0; i<articles.length; i++) {
		Article article = articles[i];
        buffer.append(article.toString());
        if (i<articles.length-1) buffer.append(", "); 
		}
        buffer.append("]");
        return buffer.toString();
    }
 }