json – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Accepter un ID mais retourner un objet pour les liens de Django Rest Framework http://sametmax.com/accepter-un-id-mais-retourner-un-objet-pour-les-liens-de-django-rest-framework/ http://sametmax.com/accepter-un-id-mais-retourner-un-objet-pour-les-liens-de-django-rest-framework/#comments Thu, 08 Jun 2017 07:50:00 +0000 http://sametmax.com/?p=23385 DRF est une des perles de Django. De Python même. Comme marshmallow, requests, jupyter, pandas, SQLAlchemy ou l’admin Django. Python a tellement d’outils extraordinaires.

Mais aucune n’est parfaite, et une chose qui m’a toujours emmerdé avec celle-ci, c’est que si j’ai un modèle du genre:

class Foo(models.Model):
    name = models.CharField(max_length=64)
    bar = models.ForeignKey(Bar)

Et le serializer:

class FooSerialize(serilizers.ModelSerializer):

    class Meta:
        model = Foo

J’ai le choix entre soit avoir que des ID…

En lecture (chiant) :

GET /api/foos/1/

{
    name: "toto",
    bar: 2
}

Et en écriture (pratique) :

POST /api/foos/
{
    name: "tata",
    bar: 2
}

Soit avoir que des objets.

En lecture (pratique):

GET /api/foos/1/

{
    name: "toto",
    bar: {
       // tout l'objet bar disponible en lecture
    }
}
Et en écriture (chiant) :

POST /api/foos/
{
    name: "tata",
    bar: {
       // tout l'objet bar à se taper à écrire
    }
}

Il y a aussi la version hypermedia où l’id est remplacé par une URL. Mais vous voyez le genre : mon API REST est soit pratique en lecture mais relou à écrire, soit pratique en écriture (je fournis juste une référence), mais relou en lecture, puisque je dois ensuite fetcher chaque référence.

GraphQL répond particulièrement bien à ce problème, mais bon, la techno est encore jeune, et il y a encore plein d’API REST à coder pour les années à venir.

Comment donc résoudre ce casse-tête, Oh Sam! – sauveur de la pythonitude ?

Solution 1, utiliser un serializer à la place du field

class FooSerializer(serilizers.ModelSerializer):

    bar = BarSerializer()

    class Meta:
        model = Foo

Et là j’ai bien l’objet complet qui m’est retourné. Mais je suis en lecture seule, et il faut que je fasse l’écriture à la main. Youpi.

Pas la bonne solution donc.

Solution 2, écrire deux serializers

Ben ça marche mais il faut 2 routings, ça duplique l’API, la doc, les tests. Moche. Next.

Solution 3, un petit hack

En lisant le code source de DRF (ouais j’ai conscience que tout le monde à pas la foi de faire ça), j’ai noté que ModelSerializer génère automatiquement pour les relations un PrimaryKeyRelatedField, qui lui même fait le lien via l’ID. On a des classes similaires pour la version full de l’objet et celle avec l’hyperlien.

En héritant de cette classe, on peut créer une variante qui fait ce qu’on veut:

from collections import OrderedDict

from rest_framework import serializers


class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):

    # en lecture, je veux l'objet complet, pas juste l'id
    def to_representation(self, value):
        # le self.serializer_class.serializer_class est redondant
        # mais obligatoire
        return self.serializer_class.serializer_class(value).data

    # petite astuce perso et pas obligatoire pour permettre de taper moins 
    # de code: lui faire prendre le queryset du model du serializer 
    # automatiquement. Je suis lazy
    def get_queryset(self):
        if self.queryset:
            return self.queryset
        return self.serializer_class.serializer_class.Meta.model.objects.all()

    # Get choices est utilisé par l'autodoc DRF et s'attend à ce que 
    # to_representation() retourne un ID ce qui fait tout planter. On 
    # réécrit le truc pour utiliser item.pk au lieu de to_representation()
    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        return OrderedDict([
            (
                item.pk,
                self.display_value(item)
            )
            for item in queryset
        ])

    # DRF saute certaines validations quand il n'y a que l'id, et comme ce 
    # n'est pas le cas ici, tout plante. On désactive ça.
    def use_pk_only_optimization(self):
        return False

    # Un petit constructeur pour générer le field depuis un serializer. lazy,
    # lazy, lazy...
    @classmethod
    def from_serializer(cls, serializer, name=None, args=(), kwargs={}):
        if name is None:
            name = f"{serializer.__class__.__name__}AsymetricAutoField"

        return type(name, (cls,), {"serializer_class": serializer})(*args, **kwargs)

Et du coup:

class FooSerializer(serializers.ModelSerializer):

    bar = AsymetricRelatedField.from_serializer(BarSerializer)

    class Meta:
        model = Foo

Et voilà, on peut maintenant faire:

GET /api/foos/1/

{
    name: "toto",
    bar: {
       // tout l'objet bar disponible en lecture
    }
}

POST /api/foos/
{
    name: "tata",
    bar: 2
}

Elle est pas belle la vie ?

Ca serait bien cool que ce soit rajouté officiellement dans DRF tout ça. Je crois que je vais ouvrir un ticket

]]>
http://sametmax.com/accepter-un-id-mais-retourner-un-objet-pour-les-liens-de-django-rest-framework/feed/ 13 23385
Faire manger du datetime à JSON en Python http://sametmax.com/faire-manger-du-datetime-a-json-en-python/ http://sametmax.com/faire-manger-du-datetime-a-json-en-python/#comments Sun, 09 Mar 2014 20:32:12 +0000 http://sametmax.com/?p=9731 La guerre de la sérialisation a plus ou moins été gagnée par JSON. Le XML est relégué aux documents très riches et aux systèmes legacy. Le YML est cantonné a des niches (et en plus souvent des niches en Ruby, c’est dire !). Et les formats binaires, sont gardés pour les besoins de perf. Le reste, c’est du JSON, du JSON et du JSON.

Seulement le JSON ne permet pas de sauvegarder des dates, seulement des chaînes, des entiers, des booléens et null. Heureusement on peut créer son propre dialecte au dessus de JSON pour y remédier, mais il faut avoir un un parseur qui le gère.

En Python on peut créer sa propre classe d’encodeur et décodeur de JSON et donc techniquement ajouter n’importe quel type.

Voici une recette pour en créer un qui gère le type datetime de manière transparente :

import re
import json

from datetime import datetime

# On hérite simplement de l'encodeur de base pour faire son propre encodeur
class JSONEncoder(json.JSONEncoder):

    # Cette méthode est appelée pour serialiser les objets en JSON
    def default(self, obj):
        # Si l'objet est de type datetime, on retourne une chaîne formatée
        # représentant l'instant de manière classique
        # ex: "2014-03-09 19:51:32.7689"
        if isinstance(obj, datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S.%f')
        return json.JSONEncoder.default(self, obj)


# On fait l'opération exactement inverse pour le décodeur
class JSONDecoder(json.JSONDecoder):


    # On écrase la méthode qui permet de décoder les paires clé / valeur
    # du format JSON afin que chaque valeur passe par notre moulinette
    def object_pairs_hook(self, obj):
        return dict((k, self.decode_on_match(v)) for k, v in obj)


    # notre moulinette
    def decode_on_match(self, obj):

        # une petite regex permet de savoir si la chaine est une date
        # sérialisée selon notre format précédent
        match = re.search(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}', unicode(obj))
        # si oui, on parse et on retourne le datetime
        if match:
            return datetime.strptime(match.string, self.datetime_format)

        # sinon on retourne l'objet tel quel
        return obj

# On se fait des raccourcis pour loader et dumper le json

def json_dumps(data):
    return JSONEncoder().encode(data)


def json_loads(string):
    return JSONDecoder().decode(string)

Usage :

>>> res = json_dumps({'test': datetime(2000, 1, 1, 1, 1, 1), 'autre': [True, 1]})
>>> print(type(res), res)
(, '{"test": "2000-01-01 01:01:01.000000", "autre": [true, 1]}')
>>> res = json_loads(res)
>>> print(type(res), res)
(, {u'test': u'2000-01-01 01:01:01.000000', u'autre': [True, 1]})

Minibelt contient une version un peu plus élaborée de ce code qui prend en compte les types date, time et timedelta ainsi que pas mal d’options de configuration.

]]>
http://sametmax.com/faire-manger-du-datetime-a-json-en-python/feed/ 6 9731
plist, pickle, hdf5, protocol buffers… : les formats binaires http://sametmax.com/plist-pickle-hdf5-protocol-buffer-les-formats-binaires/ http://sametmax.com/plist-pickle-hdf5-protocol-buffer-les-formats-binaires/#comments Mon, 15 Jul 2013 19:24:18 +0000 http://sametmax.com/?p=6654 il est probable que ça ne parle pas aux non informaticiens.]]> Dans un article précédent, on avait fait un petit tour des formats texte, et j’avais promis qu’on verrait les formats binaires.

Contrairement à cette fois là, je vais faire un peu plus technique, et donc il est probable que ça ne parle pas aux non informaticiens.

Avant toute chose, il faut faire un peu de ménage. En effet, tous les formats de données sont des formats binaires, même les formats texte. Quand bien même on retire les formats texte par convention, tout le reste sont des formats binaires.

tar.gz, zip, 7zip, rar, iso, dmg et compagnie sont des formats binaires. Il servent à l’archivage.

doc, xls, ppt, pps, etc. sont des formats binaires. Ils servent à sauvegarder un document édité sous une suite Microsoft Office.

jpg, tiff, png, gif ou webp sont des formats binaires. Ils servent à représenter des images.

wav, mp3, ogg, acc, opus et monkey sont des formats binaires. Ils servent à stocker des données sonores.

mkv, avi, mov, mp4, ogm, webm… sont des formats binaires. Ils servent à contenir des informations vidéos.

Bref, tout fichier est un format binaire, toute donnée transmise d’un système informatique à un autre est un format binaire.

Alors qu’est-ce qu’on entend ici par “format binaire” ?

Principalement, format de sérialisation binaire.

En effet JSON, XML ou CSV sont avant tout, bien que pas uniquement, des formats de sérialisation, et nous allons donc voir des équivalents dans le monde du binaire. Attention cependant, il existe de centaines de formats, et beaucoup sont très utilisés même si je n’en ai jamais entendu parler. Les formats de sérialisation binaires sont en effet moins universels, c’est à dire qu’on les retrouve plus souvent liés à un usage ou un corps de métier. Les scientifiques ont les leurs, les industriels les leurs, les concepteurs d’OS les leurs, les constructeur de matériel les leurs, etc. Le fait que je ne les connaisse pas ne veut pas du tout dire qu’ils ne sont pas massivement utilisés. Cela veut juste dire que je ne les ai jamais croisés dans mon activité.

Par ailleurs je ne présenterai pas tous ceux que j’ai effectivement croisés. Voyez l’article comme une base de travail qui va vous permettre d’évaluer les autres formats binaires plutôt qu’un listing exhaustif.

En théorie, on distingue des données binaires, et des données encodées en binaire. En pratique, on s’en branle.

Séria-quoi ?

A la conception d’un programme se pose la question de savoir comment stocker ses données dans un fichier ou les transmettre par le réseau. Vous avez vos données sous forme de code, par exemple en Python une collections d’instances de vos propres classes, des dictionnaires, des listes, des entiers, des chaînes, etc. Ces objets, il va falloir les transformer en quelque chose qui puisse sauvegardé dans un fichier. Ou envoyé sur le réseau.

Cette opération de transformation, c’est ce qu’on appelle la sérialisation.

Quand on lit le fichier ou que l’on récupère la donnée via un réseau, on doit la transformer pour obtenir des objets manipulables sous forme de code : les collections d’instances de vos propres classes, des dictionnaires, des listes, des entiers, des chaînes qui étaient là à l’origine.

Cette opération de transformation, c’est ce qu’on appelle la dé-sérialisation.

Prenons un exemple en Python. J’ai une classe Personne() :

>>> class Personne(object):
...    def __init__(nom, age):
...         self.nom = nom
...         self.age = age

Et j’ai un calendrier qui liste les personnes présentes selon les jours de la semaine :

>>> gertrude = Personne("Gertrude", 18)
>>> monique = Personne("Monique", 12)
>>> jenifer = Personne("Jenifer", 97)
>>> cal = {
"lundi": [gertrude],
"mardi": [gertrude, monique],
"mercredi": [],
"jeudi": [monique],
"vendredi": [gertrude, jenifer],
"samedi": [gertrude, monique, jenifer],
"dimanche": [gertrude]
}

On a donc un format riche ici, avec plusieurs types imbriqués : du dico, de la liste, de l’instance de classe perso, de l’entier et des strings. On a donc des primitives, des données associatives, des séquences ordonnées et un structure complexe.

Pour sauvegarder ça dans un fichier ou le faire passer sur un réseau, il va falloir écrire un sacré bout de code. Par exemple si vous voulez le transformer en XML ou en JSON, il n’y a pas de type “Personne” dans ces formats. Il va donc falloir vous mettre d’accord sur une convention, écrire le code qui génère les données formatées selon cette convention, et également le code qui permet de lire ces données formatées et recréer les bons objets derrière. Sans parler du fait que la techno qui écrit ne va peut être pas être celle qui lit. C’est ça, la problématique de la sérialisation.

Les formats binaires se prêtent bien au jeu de la sérialisation, bien qu’ils puissent, eux aussi, servir à bien d’autre chose. Il sont compacts, et non limités par un besoin de lisibilité, ils contiennent souvent des moyens de contenir des données au format complexe. Ils sont aussi en général rapides à traiter, et prennent peu de place.

Pickle

Pickle est un format de sérialisation spécialisé pour Python. Seul un programme Python peut écrire et lire du Pickle, même si des projets existent pour faire le pont avec d’autres langages.

Voilà ce que ça donne à l’usage, en reprenant notre calendrier précédent :

>>> import pickle
>>> pickle.dumps(cal)
"(dp0\nVmardi\np1\n(lp2\nccopy_reg\n_reconstructor\np3\n(c__main__\nPersonne\np4\nc__builtin__\nobject\np5\nNtp6\nRp7\n(dp8\nS'nom'\np9\nVGertrude\np10\nsS'age'\np11\nI18\nsbag3\n(g4\ng5\nNtp12\nRp13\n(dp14\ng9\nVMonique\np15\nsg11\nI12\nsbasVsamedi\np16\n(lp17\ng7\nag13\nag3\n(g4\ng5\nNtp18\nRp19\n(dp20\ng9\nVJenifer\np21\nsg11\nI97\nsbasVvendredi\np22\n(lp23\ng7\nag19\nasVjeudi\np24\n(lp25\ng13\nasVlundi\np26\n(lp27\ng7\nasVdimanche\np28\n(lp29\ng7\nasVmercredi\np30\n(lp31\ns."

Ce blougi blouga est une représentation sérialisée de notre calendrier. Si vous le sauvegardez dans un fichier ou que vous l’envoyez à un autre programme Python, il peut récupérer les objets initiaux :

>>> cal2 = pickle.loads("(dp0\nVmardi\np1\n(lp2\nccopy_reg\n_reconstructor\np3\n(c__main__\nPersonne\np4\nc__builtin__\nobject\np5\nNtp6\nRp7\n(dp8\nS'nom'\np9\nVGertrude\np10\nsS'age'\np11\nI18\nsbag3\n(g4\ng5\nNtp12\nRp13\n(dp14\ng9\nVMonique\np15\nsg11\nI12\nsbasVsamedi\np16\n(lp17\ng7\nag13\nag3\n(g4\ng5\nNtp18\nRp19\n(dp20\ng9\nVJenifer\np21\nsg11\nI97\nsbasVvendredi\np22\n(lp23\ng7\nag19\nasVjeudi\np24\n(lp25\ng13\nasVlundi\np26\n(lp27\ng7\nasVdimanche\np28\n(lp29\ng7\nasVmercredi\np30\n(lp31\ns.")
>>> type(cal2)

>>> for jour, personnes in cal2.items():
...     print(jour)
...     for personne in personnes:
...         print("\t- {}".format(personne.nom))
...
mardi
    - Gertrude
    - Monique
samedi
    - Gertrude
    - Monique
    - Jenifer
vendredi
    - Gertrude
    - Jenifer
jeudi
    - Monique
lundi
    - Gertrude
dimanche
    - Gertrude
mercredi

On utilisera Pickle essentiellement par fainéantise, quand on veut sauvegarder des objets Python et qu’on souhaite les récupérer plus tard, mais qu’on ne veut pas coder un code de sérialisation. Il existe des formes hybrides de cette approche, comme cette lib qui essaye de mélanger JSON et une forme de sérialisation d’objets complexes.

Quel que soit l’approche choisit, restaurer des objets complets, et non juste des primitives, comporte sont lot de risques de sécurité. En effet, un fichier Pickle malicieux sera exécuté comme code Python valide sans aucune vérification.

A noter que Python vient avec un autre format de sérialisation : marshall. Il est utilisé par Python en interne pour les fichiers .pyc et n’est pas recommandé pour un usage de persistance de données car le format évolue avec les versions de Python.

plist

Il existe de nombreux formats binaires qu’utilisent les OS comme .DS_store ou Thumbs.db. plist est l’un deux, et on va le voir parce qu’il est relativement simple à comprendre par rapport aux autres. Le principe est le même pour tous : on a des données, on les stock dans le fichier.

plist est un format qui existe aujourd’hui en XML, preuve que le même rôle peut très bien être rempli par deux formats différents. Il sert à stocker les réglages qu’on effectue dans le finder de Mac OS X, et ceux pour chaque dossier. Il sait représenter les types suivant : string, nombre, boolean, date, array, dictionnaire et des données arbitraires en base64 (un encodage binaire représentable sous forme de texte. Qu’est-ce qu’on se marre ^^).

Ce qui signifie par exemple, qu’il n’est pas capable de représenter un objet Personne() tel quel. Par contre il a des équivalents des types list, int, str, etc, ce qui en fait un format facile à manipuler en Python, surtout étant donné que la lib standard contient un module pour ça :

gertrude = ("Gertrude", 18)
monique = ("Monique", 12)
jenifer = ("Jenifer", 97)
cal = {
"lundi": [gertrude],
"mardi": [gertrude, monique],
"mercredi": [],
"jeudi": [monique],
"vendredi": [gertrude, jenifer],
"samedi": [gertrude, monique, jenifer],
"dimanche": [gertrude]
}

>>> gertrude = ("Gertrude", 18)
>>> monique = ("Monique", 12)
>>> jenifer = ("Jenifer", 97)
>>> cal = {
... "lundi": [gertrude],
... "mardi": [gertrude, monique],
... "mercredi": [],
... "jeudi": [monique],
... "vendredi": [gertrude, jenifer],
... "samedi": [gertrude, monique, jenifer],
... "dimanche": [gertrude]
... }
>>> plistlib.writePlistToString(cal)
'\n\n\n\n\tdimanche\n\t\n\t\t\n\t\t\tGertrude\n\t\t\t18\n\t\t\n\t\n\tjeudi\n\t\n\t\t\n\t\t\tMonique\n\t\t\t12\n\t\t\n\t\n\tlundi\n\t\n\t\t\n\t\t\tGertrude\n\t\t\t18\n\t\t\n\t\n\tmardi\n\t\n\t\t\n\t\t\tGertrude\n\t\t\t18\n\t\t\n\t\t\n\t\t\tMonique\n\t\t\t12\n\t\t\n\t\n\tmercredi\n\t\n\t\n\tsamedi\n\t\n\t\t\n\t\t\tGertrude\n\t\t\t18\n\t\t\n\t\t\n\t\t\tMonique\n\t\t\t12\n\t\t\n\t\t\n\t\t\tJenifer\n\t\t\t97\n\t\t\n\t\n\tvendredi\n\t\n\t\t\n\t\t\tGertrude\n\t\t\t18\n\t\t\n\t\t\n\t\t\tJenifer\n\t\t\t97\n\t\t\n\t\n\n\n'

Bon, là j’ai un peu foiré mon exemple parce que la lib standard, elle pond la version XML (puisque la version binaire est obsolète), pas la version binaire de plist, et maintenant que j’ai écris tout ça, ça me fait chier de tout refaire. Heureusement j’ai trouvé une lib sur le net qui va sauver mon honneur :

>>> biplist.writePlistToString(cal)
'bplist00bybiplist1.0\x00\xd7\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0ee\x00m\x00a\x00r\x00d\x00if\x00s\x00a\x00m\x00e\x00d\x00ih\x00v\x00e\x00n\x00d\x00r\x00e\x00d\x00ie\x00j\x00e\x00u\x00d\x00ie\x00l\x00u\x00n\x00d\x00ih\x00d\x00i\x00m\x00a\x00n\x00c\x00h\x00eh\x00m\x00e\x00r\x00c\x00r\x00e\x00d\x00i\xa2\x0f\x10\xa2\x11\x12h\x00G\x00e\x00r\x00t\x00r\x00u\x00d\x00e\x10\x12\xa2\x13\x14g\x00M\x00o\x00n\x00i\x00q\x00u\x00e\x10\x0c\xa3\x15\x16\x17\xa2\x11\x12\xa2\x13\x14\xa2\x18\x19g\x00J\x00e\x00n\x00i\x00f\x00e\x00r\x10a\xa2\x1a\x1b\xa2\x11\x12\xa2\x18\x19\xa1\x1c\xa2\x13\x14\xa1\x1d\xa2\x11\x12\xa1\x1e\xa2\x11\x12\xa0\x15$/>> biplist.readPlistFromString(r'bplist00bybiplist1.0\x00\xd7\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0ee\x00m\x00a\x00r\x00d\x00if\x00s\x00a\x00m\x00e\x00d\x00ih\x00v\x00e\x00n\x00d\x00r\x00e\x00d\x00ie\x00j\x00e\x00u\x00d\x00ie\x00l\x00u\x00n\x00d\x00ih\x00d\x00i\x00m\x00a\x00n\x00c\x00h\x00eh\x00m\x00e\x00r\x00c\x00r\x00e\x00d\x00i\xa2\x0f\x10\xa2\x11\x12h\x00G\x00e\x00r\x00t\x00r\x00u\x00d\x00e\x10\x12\xa2\x13\x14g\x00M\x00o\x00n\x00i\x00q\x00u\x00e\x10\x0c\xa3\x15\x16\x17\xa2\x11\x12\xa2\x13\x14\xa2\x18\x19g\x00J\x00e\x00n\x00i\x00f\x00e\x00r\x10a\xa2\x1a\x1b\xa2\x11\x12\xa2\x18\x19\xa1\x1c\xa2\x13\x14\xa1\x1d\xa2\x11\x12\xa1\x1e\xa2\x11\x12\xa0\x15$/

Pourquoi utiliser plist ? A part quand on est en Objectif-C où c'est le format le plus simple à parser ou si on veut communiquer avec finder, il n'y a pas vraiment de raison. C'est le cas typique d'un format qui a été créé parce qu'à l'époque il n'y avait rien d'aussi bien, les parsers XML étaient alors trop lents pour scanner toutes les plist de tous les dossiers récursivement.

hdf5

hdf5 est très intéressant, c'est le cas typique d'un format qui existe pour un usage très très particulier, et que des formats ordinaires ne comblent pas, ne peuvent pas par nature combler. C'est un format cross-plateforme qui peut contenir de très grosses quantités de données numériques (un fichier peut avoir une taille virtuellement illimitée), et les manipuler pour faire des calculs complexes. Cela ressemble à un système de fichiers... qui tient dans un fichier. En effet, il peut contenir une arborescence de données, et gère la compression transparente, mais les données sont essentiellement des arrays à plusieurs dimensions, appelés ici datasets.

On peut y mettre des arrays, des labels, des attributs, organiser tout ça par groupe et même avoir des références vers des données extérieures. L'avantage c'est qu'on peut bosser dessus presque de manière transparente, comme si c'était en RAM. Tout ce qui est array est stocké tel quel, et donc très rapide d'accès (bien plus qu'une colonne de base SQL), pour le reste, c'est indexé avec arbre binaire, donc facilement triable.

Pour manipuler ce format avec Python, on va utiliser la lib h5py :

sudo apt-get install libhdf5-serial-dev python-dev # sur ubuntu en tout cas
pip install numpy
pip install h5py

La normalement, ça compile à mort pendant 10 minutes.

Et pif paf pouf :

>>> import numpy # hdf5 s'utilise beaucoup avec les libs scientifiques type numpy
>>> import h5py
>>> array = numpy.ones((1000000000, 1000000000)) # une grosse matrice
>>> f = h5py.File('data.hdf5')
>>> dset = f.create_dataset("Nom du dataset", data=array)
>>> dset

>>> f.close()

Et voilà, on vient de créer array contenant 1000000000 lignes de 1000000000 de 1000000000 de int ayant pour valeur "1", et stocké tout ça dans un fichier au format hdf5. Ca prend quelques secondes, et le fichier fait quand même 800 Mo !

On le voit ici, hdf5 est entre le format de sérialisation et la base de données, et il est très orienté chiffre. Il existe tout un tas de formats binaires spécialisés pour un usage en particulier comme hdf5, à votre charge, donc, de chercher si il en existe un pour le votre. Ou même si vous en avez besoin d'un.

Des libs de haut niveau ont été construite en utilisant hdf5, telles que pytables, qui permettent de traiter très facilement d'énormes jeux de données tabulaires.

Protocol Buffers

Aussi appelé protobuf par ses amis, c'est un format de sérialisation inventé par Google qu'il utilise pour communiquer entre ses machines. On a donc vu un format de sérialisation orienté persistance avec Pickle, un orienté configuration, un orienté "grosse quantité de données" et voilà un dernier orienté communication réseau.

Protocol Buffers est un espèce d'hybride, puisqu'il utilise une description du schéma pour générer du code qui va sérialiser les donner en binaire. Vous suivez ? Non ?

Attendez ça va devenir plus clair.

Reprenons notre bonne vielle personne. Pour utiliser protobuf, vous allez décrire à quoi ressemble votre personne, dans un format texte spécialement conçu :

message Personne {
  required string nom = 1;
  required int32 age = 2;
}

Vous constatez qu'on décrit ici un message, qui va devoir contenir au minimum un nom et un age, de type string et entier. Les chiffres représentent des identifiants uniques de champs qui seront utilisés dans le message binaire.

Ceci n'est pas du code d'un langage particulier, c'est la syntaxe de modèle de protobuf.

On sauvegarde tout ça dans un fichier personne.proto, et on utilise la commande protoc pour transformer cette description en code dans le langage de son choix. C++ et Java sont supportés, nous on va utiliser Python :

protoc personne.proto --python_out=.

Et il va nous pondre un fichier personne_pb2.py, qui est un module Python valide qui va contenir une classe Personne :

>>> from personne_pb2 import Personne
>>> p = Personne(nom="Gertrude", age=12)
>>> p.SerializeToString()
'\n\x08Gertrude\x10\x0c'

Il vous suffit d'envoyer ça par un socket, et de l'autre côté, une machine qui possède le même fichier .proto peut le lire et récupérer la donnée sous forme d'un objet Python, Java ou C++. Il a donc l'avantage d'un pickle, multi langages.

Parmi les bénéfices de protobuf, il y a que sa sortie est assez courte :

>>> json.dumps({"nom":"Gertrude", "age":12})
'{"nom": "Gertrude", "age": 12}'
>>> pickle.dumps({"nom":"Gertrude", "age":12})
'(dp0\nVnom\np1\nVGertrude\np2\nsVage\np3\nI12\ns.'

Ca fait moins de données à envoyer par le réseau.

Et en prime on a la validation des données :

>>> p.age = "12"
Traceback (most recent call last):
  File "", line 1, in 
    p.age = "12"
  File "/usr/lib/python2.7/dist-packages/google/protobuf/internal/python_message.py", line 435, in setter
    type_checker.CheckValue(new_value)
  File "/usr/lib/python2.7/dist-packages/google/protobuf/internal/type_checkers.py", line 104, in CheckValue
    raise TypeError(message)
TypeError: u'12' has type , but expected one of: (, )

Du coup on peut utiliser protobuf en lieu et place d'un XML + DTD, en tout cas pour les cas simples.

Normalement, c'est aussi un format très rapide à parser.

Bref, Google a voulu le format pour les utilisations industrielles : c'est un peu chiant à mettre en place, mais c'est performant, robuste et ça marche avec les 3 langages qu'ils utilisent en interne.

Néanmoins ce n'est pas le seul à avoir pensé à ça : msgpack est une sorte de JSON binaire plus rapide à parser et qui prend moins de place. Il est assez utilisé avec les outils de file d'attente genre celery ou de communication type ZeroMq. Mais il perd un intérêt fort du JSON : sa transparence pour javascript, et n'a pas la vérification des données comme protobuf. BSON existe aussi dans le même genre, et sert de format de stockage pour mongodb, en supportant nativement des types avancées comme les dates.

Comme je vous le disais, des formats binaire, il y en a une bonne chiée.

La prochaine et dernière session, on se fera un petit tour des bases de données SQL et NoSQL.

]]> http://sametmax.com/plist-pickle-hdf5-protocol-buffer-les-formats-binaires/feed/ 9 6654 YAML, XML, JSON, CSV, INI… Qu’est-ce que c’est et à quoi ça sert ? http://sametmax.com/yaml-xml-json-csv-ini-quest-ce-que-cest-et-a-quoi-ca-sert/ http://sametmax.com/yaml-xml-json-csv-ini-quest-ce-que-cest-et-a-quoi-ca-sert/#comments Sat, 06 Jul 2013 05:23:09 +0000 http://sametmax.com/?p=6576 évident pour tout le monde...]]> Que voilà de jolis acronymes !

Quand j’ai débuté la programmation, je les rencontrais partout sur le net. On en parlait comme si on parlait d’acheter du pain. Apparemment c’était évident pour tout le monde.

Ça m’a énervé, mais ça m’a énervé !

Et puis j’ai oublié. C’est devenu tellement le quotidien pour moi, tellement banal… Jusqu’à ce que je reçoive ce mail :

Je viens vous quémander un article

Il y a une question que je me pose assez souvent avant de coder quelque chose et bien que j’ai pu me faire quelques opinions au fil du temps, je n’ai jamais trouvé un article ou une conf ou que sais-je qui explique ça clairement.

Ma question concerne le format d’écriture des données.
Entre les fichiers textes genre ini, json, xml, yaml (que j’aime bien), les formats binaires (pickle, voir hdf5 pour les scientifiques et j’en ignore peut être d’autre…), les bases de données mysql, postgre, sqlite.

Je n’ai pas les idées claires sur :
* cas typique d’utilisation
* les plus et les moins
* la corruptibilité
* la sécurité (point fourre-tout)

En gros, mon raisonnement de béotien dit : binaire plus performant que texte mais texte lisible par l’éditeur et ça, ça rassure.

A prendre ou à jeter

Aujourd’hui, je vais donc parler des formats texte, et je ferai un article sur les formats binaires plus tard.

Formats

YAML, XML, JSON, CSV, INI sont des noms de formats de données texte. Il y a plusieurs choses à comprendre ici:

Donnés :
Ca peut être n’importe quoi que vous vouliez sauvegarder ou transmettre : carnet d’adresses, configuration d’un logiciel, contenu d’une base de données, nom/prenom/age/mensurations, etc. Bref, tout groupe d’informations que vous souhaitez pourvoir communiquer, ou relire plus tard.
Format :
Comment sont organisées ces données. Comme on manipule les données avec un ordinateur, il est nécessaire de les ranger données d’une certaine façons. En les rangeant de cette façon, l’ordinateur, si il connait le “format”, est capable d’analyser les données qu’il reçoit. Sinon pour lui elles ne veulent rien dire.
Texte :
En opposition à “binaire” (ce qui est un abus de langage, puisque du texte en informatique, c’est du binaire). Cela signifie que votre format est organisé autour d’un texte lisible par les humains. C’est ce texte qui va dire “ceci est l’age”, “ceci est le nom”, “ceci est la taille de ses ganglions”, etc.

En résumé, un format de données texte, c’est une convention textuelle pour que des ordinateurs puissent échanger des données entre eux et que celui qui reçoit puisse retrouver la même chose que ce que l’on lui a envoyé.

Exemples

Le format XML est une convention qui dit qu’on va mettre les données (les informations qu’on transmet) entre des balises.

Les balises ont la forme suivante : < nomDeBalise >.

Une balise peut contenir une données, ou d’autres balises.

Le choix des balises est laissé à la personne qui créer le XML. Celui qui reçoit le XML doit connaître ces balises pour récupérer les données.

Imaginez un carnet d’adresses avec chaque personne ayant un nom et un numéro de téléphone. Il existe de nombreux moyens de représenter ce carnet d’adresses. L’UN des moyens de possibles, est d’écrire un fichier XML. Par exemple :



    
        Sam
        555-555-555
    
    
        Max
        1234567890
    
    
        Bob
        666
    

Pour vous en tant qu’humain, ça n’apporte rien.

Mais quand vous avez besoin de créer un programme qui peut sauvegarder / lire ces données ou les transmettre à un autre programme (car oui, les programmes doivent pourvoir se parler entre eux, comment vous croyez que cet article arrive sur votre ordinateur ?), il va falloir choisir un format pour ces données.

Chaque format et différent, et possède des avantages et des inconvénients. On peut très bien représenter les mêmes données avec deux formats différents. Ainsi, voici le même carnet d’adresses, mais au format CSV :

"nom";"numero"
"Sam";"555-555-555"
"Max";"1234567890"
"Bob";"666" 

Quel format est le meilleur ?

Il n’existe pas de “meilleur” format.

Chaque format a ses caractéristiques, et il existe de nombreux formats. En fait, il existe même des formats dans les formats, des XML avec des balises qui ont des noms standardisés par exemple. Pire, vous pouvez inventer vos propres formats. Mais à moins d’être très bon et de combler un besoin qui ne l’est pas encore, je ne vous le recommande pas.

Aussi il va vous falloir choisir un format selon votre situation. En général, on choisira parmi ces caractéristiques :

  • Facilité de manipulation : votre code va traiter ce format, donc si c’est galère à manipuler, vous allez vous faire du travail en plus.
  • Expressivité : certains formats permettent de “dire” plus de de choses que d’autres. Selon la complexité de vos données, certains seront trop simples ou trop complexes.
  • Pérennité : combien de temps ce format doit être lisible ? Et-il un standard reconnu ? Est-il beaucoup utilisé ? Est-il ouvert ? Avez-vous le droit d’utiliser ce format (car oui, il y a des questions légales) ?
  • Performance : le temps de traitement de ce format convient-il à votre usage ? La place qu’il prend sur le disque ? En RAM ? Le temps de transfert par un réseau ?

Dans notre cas, nous allons étudier des formats textes ouverts qui sont tous des standards, et lisibles par un humain. Aussi la pérennité de vos données est le moindre de vos soucis, si tant est que le créateur du format est compétent et bienveillant.

Dans cet article, nous allons en effet voir uniquement les formats texte. Je garderai les formats binaires pou un article suivant, et un dernier pour les bases de données.

Au passage, les formats textes ont tous ces caractéristiques en commun:

  • Ils sont lisibles et modifiables facilement et avec peu de moyen par les humains.
  • On peut debugger, et même bidouiller un format texte sans trop de souci, même si on a pas de lib faite pour ça.
  • Ils sont plus lents et prennent plus de place que les formats binaires.
  • On les utilisent souvent pour les exports / imports de données, les fichiers de configurations, et dans la transmission d’informations sur le Web entre deux machines.

A noter qu’au passage la sécurité et la corruptibilité ne sont pas des questions liées au format, mais à la méthode de manipulation de ces formats. Donc le choix du format ne prend pas en compte ces questions.

Le format CSV

L’acronyme CSV signifie Coma Separated Values, littéralement valeurs séparées par des virgules. C’est un des formats les plus simples que l’on puisse trouver.

Dans sa version la plus basique, c’est un format ligne à ligne, et chaque ligne représente une entrée (par exemple une personne dans un carnet d’adresses, un objet dans un catalogue, des groupes d’ingrédients dans une liste de recettes, etc).

Pour chaque entrée, il y a une série de valeur, chaque valeur est séparée par des virgules. Reprenons l’exemple du carnet d’adresse :

sam,555-555-555,5 rue des lilas
max,1234567890,thailande
bob,666,7eme cercle 

Mais il peut se complexifier dès qu’on veut rajouter des valeurs plus complexes. Par exemple si elles contiennent des virgules, alors il est d’usage d’entourer les valeurs de quotes :

"sam","555-555-555","5, rue des lilas"
"max,"1234567890","Patong, thailande"
"bob","666","7eme cercle" 

Le séparateur (la virgule) et les quotes peuvent être aussi un point-virgule et un quote simple, et on peut avoir n’importe quelle combinaison :

'sam','555-555-555','5, rue des lilas'
'max,'1234567890','Patong, thailande'
'bob','666','7eme cercle' 
'sam';'555-555-555';'5, rue des lilas'
'max;'1234567890';'Patong, thailande'
'bob';'666';'7eme cercle' 
sam;555-555-555;5, rue des lilas
max;1234567890;Patong, thailande
bob;666;7eme cercle 

Il existe aussi des caractères d’échappement, des exceptions de retour à la ligne, et des programmes qui produisent des lignes avec un séparateur, puis des lignes avec un autre. Autant dire que d’un format simple, on arrive parfois à quelque chose de complexe. C’est d’ailleurs pour ça que je recommande de ne pas traiter le CSV à la main, mais d’utiliser des modules spécialisés comme csv en Python.

Sachez néanmoins que le format le plus courant est celui supporté par les tableurs (Excel, LibreOffice Calc…) utilisant des guillemets doubles et des point-virgules :

"Sam";"555-555-555";"5, rue des lilas"
"Max";"1234567890";"Patong, thailande"
"Bob";"666";"7eme cercle"

C’est donc ce format que je recommande car il est du coup très facile à lire et à modifier. Il est aussi possible de donner un nom à chaque colonne sur la première ligne :

"nom";"numero";"adresse"
"Sam";"555-555-555";"5, rue des lilas"
"Max";"1234567890";"Patong, thailande"
"Bob";"666";"7eme cercle"

Vous voudrez utiliser ce format quand :

  • Vos données sont simples et facile à représentées sous forme de tableau.
  • Vous avez pas envie de vous faire chier (TRES bonne raison).
  • Il n’y a pas d’imbrication dans vos données.
  • Vos données sont statiques (pas de vérification, pas de calculs, pas de transtypage). Bref, on les lis tel quel.
  • Vous voulez que ce soit lisible dans un tableur. Pratique pour les néophytes.

Le format INI

Le format INI, utilisé pour l’INItialisation, est surtout un format pour stocker des configurations de logiciel. On le trouve souvent dans des fichiers avec des paramètres, portant l’extension .ini, .cfg, .conf ou .txt.

Il est constitué de deux types d’élément :

  • Les en-têtes, ou sections, qui s’écrivent [nom_de_l_en_tete]. Ils servent à nommer et délimiter des groupes de valeurs.
  • Les valeurs, qui s’écrivent nom=valeur.

Les fichiers INI ne sont pas fait pour stocker des données répétitives comme des personnes d’un carnet d’adresses. Généralement, chaque entrée est unique, et représente un paramètre du programme :

[utilisateur]
nom=Sam
dossier=/home/sam

[dernier_acces]
jour=2013-07-06
fichier='le_plus_dur_est_derriere_toi.avi'

C’est un format simple à manipuler (et il existe un module Python pour ça) et généralement on le lit, on modifie une valeur, et on la sauvegarde.

Vous voudrez utiliser ce format quand :

  • Vous voulez sauvegarder l’état ou la configuration de votre programme.
  • Vos données sont très simples.
  • Vous êtes sous une vieille machine Windows.
  • Vous voulez tricher à Baldur’s Gate.

Le format JSON

A la base JSON, qui signifie JavaScript Object Notation, n’était pas un format destiné à être échangé, mais seulement la représentation textuelle des objets Javascript.

Il se trouve qu’avec l’age d’or du Web, et l’utilisation massive d’AJAX, il a été utilisé pour communiquer entre le navigateur et le serveur, et les gens se sont apperçu qu’il était en fait très très très pratique.

Aujourd’hui, JSON est utilisé un peu pour tout, et si vous ne savez pas trop quoi choisir comme format, choisissez JSON, il y a peu de chance de se planter.

Voilà à quoi ressemble le carnet d’adresses en JSON:

[
    {
        'nom': 'Sam',
        'numero': '555-555-555',
        'adresse': '5, rue des lilas'
    },
    {
        'nom': 'Max',
        'numero': '1234567890',
        'adresse': 'Patong, thailande'
    },
    {
        'nom': 'Bob',
        'numero': '666',
        'adresse': '7eme cercle'
    }
]

JSON est extrêmement facile à manipuler de nos jours, et l’immense majorité des langages ont un module pour ça. Python n’échappe pas à la règle.

Il est rare que JSON soit une mauvaise idée, donc je vais plutôt faire une liste de quand vous ne voulez PAS utiliser JSON :

  • Vos données vont être lues souvent pas des humains sans connaissance techniques (le CSV est plus adapté).
  • La plupart des systèmes qui vont lire ces données ont de mauvaises bibliothèques pour lire le JSON, mais de grosses facilités pour d’autres formats.
  • Le reste du système communique avec un autre format (pas la peine de cumuler 40000 formats).
  • Vous avez besoin de performances extrêmes (dans ce cas cherchez du côté des formats binaires).
  • Vous voulez stocker de grosses quantités de données ou faire des analyses complexes dessus (dans ces cas cherchez du côté des bases de données).
  • Il existe un standard qui correspond à votre usage qui n’est pas en JSON (ex: notifications : Utilisez RSS, qui est du XML, ou les emails, carnet d’adresses : utilisez ldif ou vcard, etc.)
  • Vos données sont très très complexes et demande un format plus riche, des vérifications automatiques, un typage avancé, etc. Préférez XML.

Sinon, allez-y, prenez du JSON. On peut l’utiliser pour les fichiers de configuration (comme le fait Sublime Text), comme API pour son service (ce que font presque tous les grands services du monde), pour communiquer entre plusieurs sites Web (JSONP), pour exporter / importer ses données (fixtures Django)…

Le format YAML

YAML, l’humoristique “Yet Another Markup Language” a des buts et qualités similaires au JSON, mais avec un format différent.

Voici le fichier INI, traduit en YAML (les espaces sont significatifs) :

---
utilisateur:
    nom: Sam
    dossier: /home/sam

dernier_acces:
    jour: 2013-07-06
    fichier: 'le_plus_dur_est_derriere_toi.avi'
...

Le format YAML est néanmoins plus riche que le JSON:

  • Il permet d’inclure plusieurs document dans un seul fichier en les séparant par ---
  • Il contient des types avancés comme les dates.
  • Il possède plusieurs manières d’écrire les textes multi-lignes

Globalement le YAML a été créé pour être facilement lisible et éditable par un humain, et pour cette raison la communauté Ruby l’a adopté pour ses fichiers de configuration. On retrouve donc YAML dans RubyOnRail.

Utilisez YAML quand :

  • Votre fichier est destiné à être aussi souvent lu par une machine qu’un humain et contient des types complexes.
  • Vous êtes dans un environnement Ruby (à Rome…).

Contrairement à JSON, on utilisera donc plus YAML pour la configuration que l’envoie de données.

Personnellement je ne suis pas un aficionado de YAML. Mon expérience est que sa syntaxe complexe (j’admet que l’exemple ne le laisse pas paraitre) amène souvent des grattements de tête suite à une édition malheureuse. Frustrant pour un simple fichier de config.. De plus, il faut souvent installer une lib additionnelle pour le lire. Par ailleurs, JSON – qui est en fait un subset de YAML – fait généralement très bien le boulot.

Le format XML

La fameux eXtensible Markup Language. Je ne vais pas vous faire un cours complet sur XML, car on pourrait y passer des mois, au sens propre. Ce format, aux apparences simples, a été utilisé pour des choses extrêmement complexes.

Revenons à notre exemple :



    
        Sam
        555-555-555
    
    
        Max
        1234567890
    
    
        Bob
        666
    

< ?xml version="1.0" encoding="utf8"? > est l’en-tête du fichier, il annonce quel format de XML on va utiliser, le reste est le contenu.

Ici nous n’avons que quelques balises, mais bien entendu, les balises peuvent contenir des balises qui peuvent contenir des balises… En prime, XML autorise des attributs (nom=”valeur”), c’est à dire des valeurs sur les balises qui modifient la signification de celle-ci. Par exemple :

...

    Bob
    666

...

Enfin, XML permet ce qu’on appelle des namespaces, afin de dire que les balises correspondent à un dialecte et pas un autre.

Exemple, j’ai deux fois nom dans ce XML :



    
        Sam
        555-555-555
        
            Metro peau lisse
            3.14,6.56
        
    
    
        Max
        1234567890
        
            Patong
            4.2,6.9
        
    
    
        Bob
        666
        
            Sodome
            -12,-13
        
    

Comment savoir pour ma machine ce que signifie, “nom” ?

Et bien je peux les namespacer, c’est à dire les lier à une URL qui pointe vers la documentation qui dit ce que signifie chaque balise, ou au moins le site de l’auteur du XML.



    
        Sam
        555-555-555
        
            Metro peau lisse
            3.14,6.56
        
    
    
        Max
        1234567890
        
            Patong
            4.2,6.9
        
    
    
        Bob
        666
        
            Sodome
            -12,-13
        
    

Et si on veut se marrer un peu plus, on peut utiliser ce qu’on appelle des DTD (ou le format plus moderne XSD) : un XML, qui définit comment doit être formé un autre XML et qui permet de vérifier cela automatiquement. Il existe également un format appelé XSLT, qui permet de définir, en XML, comment transformer un autre document XML, en un document dans un troisième format de votre choix.

Bref, le XML peut devenir très très compliqué. Si compliqué en fait, que des formats en XML, même avec leurs spécifications, deviennent difficiles à lire.

XML a été historiquement le premier format texte à être souple, extensible et interopérable. Aussi a-t-il été longtemps un format de choix pour communiquer entre machines et pour sauvegarder les configurations complexes. Aujourd’hui, sa verbosité et sa complexité ont amener les gens à se tourner massivement vers JSON, et XML n’est maintenant plus utilisé que pour les données très riches ou des raisons historiques (RSS, SOAP, etc).

Il reste un terrain où XML brille, c’est la validation de données. En effet, grâce au DTD / XSD, on peut publier un document qui permet à tout langage avec la bibliothèque appropriée, de vérifier si un XML est valide, comme un formulaire. On publie les besoins de validation une fois, et plein de langages peuvent en faire usage. C’est un vrai plus.

Je recommande rarement d’utiliser XML, c’est lourd, difficile à manipuler (malgré un très bon support général de la plupart des langages), verbeux… Difficile à un débutant de mettre les mains dans votre système quand la courbe d’apprentissage est aussi hardos.

Utilisez XML si :

  • Un des dialectes XML correspond à un standard pour votre usage (RSS / Atom).
  • Vous visez des systèmes qui utilisent historiquement XML : serveurs SOAP, environnement Java…
  • La validation des données est importante et elles seront lues par de multiples langages.

Maintenance, versioning, documentation

Ce n’est pas le tout de choisir un format, il faut maintenant s’assurer qu’on puisse l’utiliser.

Car mettre des labels à ses données, c’est bien beau, mais si le mec qui reçoit vos données ne sait pas ce que signifie ces labels, il ne peut rien en faire.

Il va donc falloir écrire une documentation pour cela. Et oui, on ne documente pas seulement son code, mais aussi ses formats !

Je vous invite aussi fortement à versionner vos formats, c’est à dire à toujours accompagner vos données (dans le fichier, via le protocole d’échange, ou dans le changelog de votre application) d’un numéro de version, ceci afin que les utilisateurs / développeurs / admin système ne se retrouvent pas couillonnés quand vous décider de modifier un peu votre format.

Sur le long terme, comme les commentaires de code, cela va vous aider vous. Mais cela rendra aussi votre projet plus accessible et attirant pour les gens de l’extérieur, ou tout simplement vos collègues.

En écrivant cela je viens de m’apercevoir que l’on a rien fait de tout cela pour 0bin. La honte…

Prochainement, les formats binaires.

]]>
http://sametmax.com/yaml-xml-json-csv-ini-quest-ce-que-cest-et-a-quoi-ca-sert/feed/ 29 6576