Difference between revisions of "Introduction à YAML"
(→Tags) |
(→Usage avancé) |
||
Line 450: | Line 450: | ||
La première solution consiste à indiquer le type des objets avec des tags YAML. Ainsi, le fichier YAML suivant : | La première solution consiste à indiquer le type des objets avec des tags YAML. Ainsi, le fichier YAML suivant : | ||
− | + | <pre> | |
--- !jyaml.Commande id: test123 articles: | --- !jyaml.Commande id: test123 articles: | ||
Line 461: | Line 461: | ||
prix: 2.0 | prix: 2.0 | ||
quantite: 2 | quantite: 2 | ||
− | + | </pre> | |
Sera-t-il chargé en utilisant les classes suivantes : | Sera-t-il chargé en utilisant les classes suivantes : | ||
− | + | </pre> | |
package jyaml; | package jyaml; | ||
Line 500: | Line 500: | ||
} | } | ||
} | } | ||
+ | </pre> |
Revision as of 16:29, 29 August 2017
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 .
Contents
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 : </pre>
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(); } }
</pre>