binaire – Sam & Max http://sametmax.com Du code, du cul Thu, 05 Sep 2019 08:22:03 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le type bytes n’est pas du texte http://sametmax.com/le-type-bytes-nest-pas-du-texte/ Fri, 11 Jan 2019 11:30:10 +0000 http://sametmax.com/?p=25125 je craque. Mais je me soigne, globalement j'ai récupéré plein de temps, et ça se voit sur mon quotidien. Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.]]> J’ai beau essayer très fort de ne pas répondre en ligne, des fois je craque. Mais je me soigne, globalement j’ai récupéré plein de temps, et ça se voit sur mon quotidien.

Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.

Mais c’est bien normal: Python 3 ne gère pas le texte de la même manière que la grande majorité des langages de programmation, justement à cause de la débâcle qu’on a eue en Python 2. Du coup, de nombreux programmeurs arrivent avec leur expérience d’ailleurs, et tentent de l’appliquer tel un utilisateur de SVN migrant sur git. En surface ça semble coller, malheuseuement à l’usage, ça fait faire des erreurs.

Donc un peu d’explications.

En informatique, tout est une histoire de convention. On dit que tel mot clé a tel effet. Que tel nom suppose telle chose. Que tel code de retour implique telle erreur. Que tel schéma XML représente tel type de document.

Essentiellement, tout cela est arbitraire: des gens ont décidé qu’il en serait ainsi. Impossible de deviner que ce que fait yield ou with si vous n’avez pas d’expérience similaire avant. Impossible de savoir que le code 0 en bash ou 200 en HTTP signifie tout va bien sans qu’on vous transmette l’information, ou faire de nombreux tests.

Quand je dis arbitrairement, évidemment je ne veux pas dire complètement sans raison. Il y a des raisons techniques, politiques, économiques, et parfois esthétiques à ces conventions. Cela n’en retire en rien l’aspect parfaitement artificiel de ces choix.

La convention la plus omniprésente, et pourtant aujourd’hui la plus masquée dans un monde où on utilise massivement des langages de haut niveau comme Javascript, Ruby, PHP et Python, est celle de l’organisation des octets.

Musique !

…je vois même plus le code : tout ce que je vois, c’est des blondes, des brunes, des rousses.

Tout ce qui passe par nos ordinateurs n’est qu’une suite de zéros et de uns, que nous avons groupés par paquets de 8:

Seulement la grande révélation, le “aaaaaaahhhhh okayyyyyyy” qui arrive un jour dans toute vie de dev, c’est que ces paquets de 8 ne veulent rien dire. Rien. C’est nous qui avons décidé, arbitrairement encore une fois, de leur signification.

Vous voyez ce moment dans les films et séries où un personnage arrive à “lire du binaire” ?

Evidement, "c'est une representation binaire ASCII de coordonnées WGS 84 Web Mercator" est plus dur à caser dans un dialogue

Evidement, “c’est une representation binaire ASCII de coordonnées WGS 84 Web Mercator” est plus dur à caser dans un dialogue

C’est de l’enculage de dauphin.

Le binaire n’est pas un langage, pas plus que les lettres “abcdefghijklmnopqrstuvwxyz”. Vous pouvez utiliser ces lettres pour représenter certains mots italiens, français, anglais, un nom propre (sans langue), le label d’un immeuble (sans langue encore) ou un chiffre latin.

Que veut dire “les gosses” ? Pour la même combinaisons de lettres, cela signifie “les enfants” avec la convention française européenne, et “les couilles” avec la convention québéquoise.

Pour le binaire c’est pareil, ce que veut dire un octet dépend de la convention que vous avez choisie.

Par exemple, que signifie cette suite d’octets ?

1100001 1100010 1100011 1100100

Bah rien. Mais on peut lui donner un sens en lui appliquant une convention.

Je peux lui appliquer la convention ASCII, et donc supposer que c’est un texte dans un certain format. Voici ce que ça donne en Python:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> print(data.decode('ascii'))     
abcd 
Les processeurs modernes ne comprenent pas nativement l'american apparel

Les processeurs modernes ne comprenent pas nativement l’american apparel

Ou je peux lui appliquer une autre convention, et decider de lire ces octets comme si ils étaient le dump d’une structure C. Interprettons en Python ces octets comme un entier non signé en big-endian:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> import struct     
>>> struct.unpack('>I', data)     
(1633837924,)

Même suite de bits, mais selon la convention choisie, elle veut dire les lettres “abcd” ou le nombre “1633837924”. Et oui, comme il n’y a pas une infinité de combinaisons de 0 et de 1 qui tiennent dans un espace mémoire limité, différentes conventions vont utiliser les mêmes octets mais décider que ça veut dire quelque chose de différent.

En fait, même des conventions pour le même type usage ne veulent pas forcément dire la même chose. Par exemple, prenez l’octet:

11101001

Un octet somme toute sympathique, de bonne famille. Il ne paie pas de mine, mais c’est un membre utile de la société.

Et maintenant, quelqu’un vous donne un indice, il vous dit que cet octet représente… du texte.

Super !

Oui, mais du texte avec quelle convention ? Car les pays du monde entier ont créé leur propre convention pour représenter du texte.

Avec la convention “latin-1”, utilisé par 0.7% de tous les sites Web du monde ?

 
>>> bytearray([0b11101001]).decode('latin-1') 
'é' 

Avec la convention “cp850”, utilisé par la console DOS ?

 
>>> bytearray([0b11101001]).decode('cp850')
'Ú'

Vous voulez rire ? Le premier à remplacé presque partout le second parce qu’ils contiennent les mêmes lettres. Elles ne sont juste pas représentées par la même combinaison d’octets.

Et cet octet, que veut-il dire avec la convention “utf8”, qui est aujourd’hui le standard international recommandé pour représenter du texte ?

 
>>> bytearray([0b11101001]).decode('utf8')
Traceback (most recent call last):
File "", line 1, in 
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 0: unexpected end of data 

Il n’a pas de correspondance. Cet octet n’est pas de l’utf8 valide.

Si vous voulez représenter ces lettres en utf8, il faut utiliser une convention différente, en utilisant non pas un seul octet, mais une séquence d’octets:

 
>>> list(map(bin, 'é'.encode('utf8')))
['0b11000011', '0b10101001']
>>> list(map(bin, 'Ú'.encode('utf8')))
['0b11000011', '0b10011010']

Vous pourriez croire que puisque le texte est particulièrement compliqué, c’est normal d’avoir des conventions qui divergent. Mais non, c’est juste la nature des conventions. Puisqu’elles sont arbitraires, l’une n’est pas plus “la vérité” qu’une autre. On retrouve la même chose avec les nombres:

>>> struct.unpack("h", bytearray([0b11101001, 0b11101001]))
(-5655,)
>>> struct.unpack("H", bytearray([0b11101001, 0b11101001])) 
(59881,)

La même suite d’octets peut représenter deux nombres totalement différents, selon que je décide de les lire comme des “short”, ou des “unsigned short”.

Et l’inverse est aussi vrai.

Ben oui, si quelque chose peut être interprété de plusieurs façons, on a aussi le fait que deux représentations différentes peuvent être interprétées … pour aboutir au même résultat.

Par exemple, le nombre des doigts de ma main peut être représenté de plein de façons différentes:

  • décimal: 5
  • français écrit: cinq
  • chiffre latin: V
  • anglais écrit: five
  • espagnol écrit: cinco
  • base deux: 101
  • structure C d’un signed short en little-endian avec Python: bytearray([0b101, 0b0])

Que de manières différentes, pour le même concept ! En plus, il y a confusion possible: V est une lettre également. cinq, five et cinco utilisent le même alphabet, mais pas les mêmes symboles spécifiques, pour représenter la même chose. Et le plus confusionant, 101 est une représentation binaire, mais bytearray([0b101, 0b0]) aussi.

Bref, voilà toute la complexité de la différence entre la donnée, un concept abstrait qui n’existe pas, et sa représentation, une convention humaine concrète qui nous permet de communiquer entre nous.

Donc, pour lire “du binaire”, ou faire n’importe quoi en informatique, il faut connaitre la convention utilisée. Mais pas juste en informatique: pour lire le journal, il faut connaitre la convention des symboles imprimés sur les pages, pour conduire sans se faire tuer, il faut connaitre la convention des panneaux, et pour parler, il faut connaitre la convention de la compression des molécules d’air émise par l’appareil buccal et respiratoire d’un individu qui vient rencontrer votre système auditif.

Vous êtes un être très conventionnel au fond.

Évidemment on trouve la même chose en Python. Par exemple vous pouvez utiliser plusieurs conventions pour demander à Python de créer le même nombre en mémoire:

>>> 245 # base 10
245
>>> 0xF5 # hexadecimal
245
>>> 0b11110101 # binaire
245
>>> 245 == 0xF5 == 0b11110101
True     
>>> type(245)     
     
>>> type(0xF5)     
     
>>> type(0b11110101)     
 

Inversement, "1" et 1 paraissent similaire, mais ils ont différents buts. Le premier est un outil destiné à l’affichage, qui matérialise le caractère représentant le chiffre arabe après le zéro. Il est stocké en interne avec une séquence d’octets similaire à:

>>> bin(ord("1"))
'0b110001'

Tandis que que le second est un outil fait pour faire des calculs avec la plus petite valeur positive entière non nulle. Il est stocké en interne avec une séquence d’octets similaire à:

>>> list(map(bin, struct.pack('l', 1)))
['0b1', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0']

Je simplifie bien entendu, en vérité la representation interne des nombres et du texte en Python est plus complexe que cela, et dépend de l’implémentation choisie, du type de processeur, de la taille de la donnée et de votre configuration.

Retour sur le type bytes

J’ai soigneusement évité d’utiliser le type bytes durant cette démonstration, le remplaçant techniquement inutilement (mais pédagogiquement brillamment, car je suis génial) par bytearray.

En effet, toute cette leçon est là pour arriver à la conclusion que bytes ne représente pas du texte, mais si je vous avais montré tout ça avec lui, voilà qui vous aurait interloqué:

     
>>> bytes([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
b'abcd' 

“Heu, mais c’est du texte !” me dirait alors un lecteur ayant diagonalisé l’article.

Mais bien entendu que non.

bytes ne présente pas du texte, c’est une structure de données dont le but est de permettre de manipuler une séquence d’octets ordonnée, et ce manuellement. N’importe laquelle.

Or, il se trouve que beaucoup de langages de programmation représentent le texte comme un array d’octets, et y attachent quelques opérations de manipulation. C’est le cas du C, ou de Python 2 par exemple. Les gens ayant eu cette expérience pensent donc que b'abcd' représente du texte, allant parfois jusqu’à aller lui donner l’appellation de “byte string”.

Il n’existe rien de tel en Python 3.

En Python 3, vous avez deux types pour manipuler des séquences d’octets: bytes et bytearray. Ils sont équivalents, à ceci près que bytes est non mutable (non modifiable) alors que bytearray est mutable (modifiable).

Ces types peuvent contenir n’importe quels octets, et nous avons vu ensemble qu’une même séquence d’octets pouvait être interprétée différemment selon la convention choisie pour la lire. Évidemment il est préférable de la lire avec la même convention qui a été utilisée pour la produire, sans quoi on ne comprendra pas ce que le producteur de la donnée à voulu dire.

Sauf que…

Beaucoup d’outils en informatique utilisent les conventions ASCII et hexadécimale pour symboliser les valeurs des octets. Si vous lancez Wireshark pour regarder les paquets d’un protocole réseau ou si vous ouvrez un PNG avec xxd, on va vous représenter le contenu avec un mélange de ces conventions.

Pour des raisons pratiques, Python fait donc la même chose, et permet ainsi de visualiser (ou produire) le type bytes à l’aide d’une notation ASCII:

    
>>> print(b'abcd'.decode('ascii'))     
abcd     
>>> struct.unpack('>I', b'abcd')     
(1633837924,)

Ou d’une notation héxa (ironiquement, l’héxa est representé par une combinaison de caractères ASCII \o/) si les valeurs ne tiennent pas dans la table ASCII:

     
>>> "é".encode('utf8')  # hexa C3 A9   
b'\xc3\xa9'     
>>> struct.unpack('h', b'\xc3\xa9')    
(-22077,)

Donc bytes, bien qu’il puisse contenir des octets interprétables comme du texte, n’est pas particulièrement fait pour manipuler du texte. Il peut contenir n’importe quoi. Mais pour des raisons pratiques, sa représentation dans le terminal est faite avec une convention familière. Après tout, il faut bien l’écrire en quelque chose pour l’affiquer à l’écran.

Si on veut manipuler du texte en Python 3, il faut utiliser le type str, qui est l’outil spécialisé dans la representation et la manipulation textuelle. Si vous savez qu’un type bytes contient des octets qui representent du texte, alors utilisez la méthode décode() avec la bonne convention (appelée “charset”), pour récupérer un str:

     
>>> print(b'P\xc3\xa8re No\xc3\xabl'.decode('utf8'))
Père Noël 

On a un très bon article sur l’encoding en Python sur le blog, d’ailleurs.

Toute cela n’était bien entendu pas vrai en Python 2. En Python 2, le type str était un array d’octets, rendant tout cela bien confus, et amenant à plein d’erreurs. L’introduction lors de la version 2.0 de l’objet unicode pour pallier le problème, bien que très utile, n’a fait que rajouter à l’incomprehension des nouveaux venus.

Or le monde extérieur, lui, n’a pas d’abstraction pour le texte. Faire des abstractions, c’est le rôle du langage de programmation. Si vous écrivez dans un terminal, ou lisez depuis un terminal, un nom de fichier, le contenu d’une base de données, une requête AJAX, etc., ce sont évidemment des octets qui sont échangés, et il vous faut la bonne convention pour faire partie de la discussion.

Le type bas niveau bytes est un outil qui sert donc à communiquer avec le monde extérieur, tandis que les types haut niveau (str, int, list, etc.) sont des outils qui font l’abstraction de ces conventions, pour vous permettre de manipuler confortablement un concept général (du texte, un nombre, une collection ordonnée) à l’interieur des murs de votre programme.

]]>
25125
Lire un format binaire en Python avec struct http://sametmax.com/lire-un-format-binaire-en-python-avec-struct/ http://sametmax.com/lire-un-format-binaire-en-python-avec-struct/#comments Fri, 26 Jun 2015 06:51:28 +0000 http://sametmax.com/?p=16503 Une suite de valeurs ne veut rien dire en soi, et même le sacro-saint binaire supposé être le socle de toute l’informatique n’a aucun sens si on ne connaît pas le format utilisé pour ce qu’il doit représenter.

Toujours la même opposition entre données et représentation.

Par exemple, le binaire peut représenter un chiffre en base 2 ou un texte encodé.

Pour autant, cela ne veut pas dire qu’il n’existe pas des formats prépondérant. En informatique, beaucoup de données binaires sont organisées pour correspondre aux structures de données du langage C, ces dernières étant une implémentation du standard IEEE 754 (en effet les strings sont des arrays d’int en C, donc le texte et les nombres sont des suites de chiffres).

Par exemple, si vous créez un array numpy contenant des nombres de 0 à 1000 stockés en int32 et sauvegardez son contenu dans un fichier :

>>> import numpy
>>> numpy.arange(0, 1000, dtype=np.int32).tofile('/tmp/data')

Le fichier va ici contenir une suite de 1 et de 0 représentant 1000 entiers, chacun comme un paquet de 4 octets organisés selon la sémantique que comprend le langage C.

Pour avoir une idée de l’organisation du contenu, on peut prendre un éditeur hexa qui vous affichera :

0000 0000 0100 0000 0200 0000 0300 0000 0400 0000 0500 0000 0600 0000 0700 0000 0800 0000 0900 0000 0a00 0000 0b00 0000 0c00 0000 0d00 0000 0e00 0000 0f00 0000 1000 0000 1100 0000 1200 0000 1300 0000

Ça se lit ainsi :

0000 0000 => 0
0100 0000 => 1
0200 0000 => 2
0300 0000 => 3
0400 0000 => 4
0500 0000 => 5
0600 0000 => 6
0700 0000 => 7
0800 0000 => 8
0900 0000 => 9
0a00 0000 => 10
0b00 0000 => 11
0c00 0000 => 12
0d00 0000 => 13
0e00 0000 => 14
0f00 0000 => 15
1000 0000 => 16
1100 0000 => 17
1200 0000 => 18
1300 0000 => 19
...

Numpy étant codé en C, cela semble plutôt logique qu’il dump tout ça dans ce format.

Mais c’est une représentation tellement courante que de nombreux formats standards l’utilisent. Par exemple, les archives et les images stockent souvent leurs données ainsi.

Prenez le format d’image PNG, la RFC indique que la taille de l’image est stockée dans le fichier sous la forme de deux entiers représentés par 4 octets chacun, ordonnés en big-endian, entre l’octet 16 et l’octet 24.

On peut donc récupérer ces informations en lisant son fichier image :

with open('image.png', 'rb') as f:
    taille = f.read(24)[16:24]

Le problème étant : comment lire cette info ? C’est un blob binaire qui ne veut rien dire pour Python :

print(taille)
b'\x00\x00\x07\x80\x00\x00\x048'

Le module struct est fait pour ça, on lui passe une donnée au format structure C, et il la convertit en type Python. Cela marche ansi, pardon, ainsi :

struct.unpack('motif_du_format_a_convertir', donnee)

Le format à convertir est une chaîne de caractères qui contient des symboles décrivant la structure de la donnée qu’on souhaite récupérer. Little-endian ou big-endian ? String, Int, Bool ?

Pour la taille de la photo, on sait qu’il y a deux entiers, non signés (une taille ne va pas être négative), en big-endian. D’après la doc de struct, on peut lui désigner un entier non signé avec ‘I’, et il faut les qualifier avec ‘>’ pour l’ordre big-endian. Du coup:

taille = struct.unpack('>II', taille)
print(taille)
(1920, 1080)

Il se trouve que mon image de test est un screenshot et que mon écran a une résolution de 1920×1080 :)

On peut faire l’opération inverse avec struct.pack, et bien entendu manipuler des formats plus complexes : il suffit de changer le motif qui représente le format à convertir.

]]>
http://sametmax.com/lire-un-format-binaire-en-python-avec-struct/feed/ 24 16503
La fin du mystère du binaire (nananère) http://sametmax.com/la-fin-du-mystere-du-binaire-nananere/ http://sametmax.com/la-fin-du-mystere-du-binaire-nananere/#comments Tue, 21 Jan 2014 15:01:56 +0000 http://sametmax.com/?p=8808 Quand mon voisin de table en cours de SVT a commencé à m’expliquer le binaire (il programmait en assembleur sur sa TI89), j’ai pas bien pigé. Des années plus tard, en cours d’archi, un prof nous a fait un cours magistral sur la question. Je n’ai définitivement rien pigé. J’ai regardé des tutos sur le net. RIEN P-I-G-É.

J’ai mis presque 10 ans (littéralement) avant que mon cerveau tilte. Non pas que je n’avais pas compris le principe, mais le détails, comment on faisait la conversion, ce qu’on pouvait faire avec, etc… Ca me passait au dessus de la tête.

Je râle beaucoup sur le manque de pédagogie d’autrui, et après tout, d’autres ont compris le truc à partir de ces explications. Mais je vais me répéter : leurs explications étaient merdiques.

Le binaire c’est simple. Tout le monde peut le comprendre.

Si vous ne pigez pas, c’est que le mec en face de vous est une grosse pine.

Par contre, il y a beaucoup de choses à savoir, alors à long article, loooooooooooongue musique :

Long article car j’ai vu des connards mélanger tout et n’importe quoi dans leurs explications du binaire, et je vais devoir démêler le sac de nœuds qu’ils ont fait dans votre tête.

Pourquoi apprendre le binaire ?

Pour le sport, essentiellement. La culture G. Parce qu’en toute honnêteté, aujourd’hui, ça ne sert plus à grand chose, tellement vous avez de couches d’abstraction entre les manipulations binaires et vous.

Il y a toujours quelques grincheux qui vous diront des trucs de grincheux comme “oui, mais pour comprendre les erreurs de flottant en arithmétique, il faut comprendre le binaire” comme mon père qui me disait qu’il fallait apprendre le latin au lycée. Meh.

D’abord, on a pas plus besoin d’apprendre le binaire pour comprendre les erreurs de flottant qu’on a besoin de comprendre la composition moléculaire de l’essence pour savoir pourquoi une voiture tombe en panne quand il y en a plus. Ensuite, vous n’avez même pas besoin de comprendre pourquoi il y a des erreurs de flottant, juste à savoir comment les éviter (ce que des tas de gens “qui les comprennent” ne sont pas foutu de faire).

Bref, c’est l’éternel bataille de l’arrière garde qui veut défendre le savoir aujourd’hui obsolète – qu’il a accumulé parce qu’a son époque c’était indispensable – à grand renfort de pédanterie. On voit ça dans tous les domaines. Généralement avec force exemples de niches et enculage de mouche.

Mais au delà de ça, le binaire est encore utilisé dans certains domaines, comme la programmation de micro-contrôleurs (si vous planifiez de bosser chez Intel, Nvidia ou la NASA, ça peut servir), la cryptographie ou la programmation de calculatrices en cours de bio. Ce dernier point étant semble-t-il, un sujet intemporel qui ne se démode jamais.

Néanmoins ça reste 0.00001 % de l’informatique actuelle. Vous n’aurez pas besoin du binaire pour vos tâches de Sys Admin, vos GUI, votre app mobile, votre site Web, votre algo de simulation, etc. Bref, les trucs pour lesquels les gens sont payés dans les années 2000.

Maintenant que j’ai bien été désagréable avec une partie du lectorat, précisons tout de même que savoir 2, 3 trucs sur le binaire, ça ne fait pas de mal. Vous êtes informaticiens, ça fait un peu partie du folklore.

Valeur et représentation

En informatique, il y a le problème sans fond de la dualité valeur / représentation. L’écriture des nombres est un exemple parfait de cela.

Par exemple, j’ai ce nombre d’étoiles :

* * * * *

Il y a la valeur du nombre d’étoiles. Cette valeur ne peut pas être manipulée, elle existe, c’est tout. On ne peut que constater le nombre d’étoiles en interprétant ce que l’on voit.

Si on veut manipuler cette valeur (parler d’une valeur, c’est déjà manipuler la valeur), il faut lui donner une représentation.

Les arabes nous ont donné une représentation : 5.

Les romains avaient une autre représentation : V.

La valeur derrière ces deux représentations est strictement la même.

Le binaire est juste une autre représentation. Il n’y a rien de magique derrière le binaire.

Par exemple, la valeur que l’on représentante par 5, se représente ainsi en binaire : 101.

C’est la même valeur de nombre d’étoiles. Juste écrit différemment. Cela n’a aucun impact sur la réalité, ne change rien sur la nature des étoiles ou la valeur manipulée. C’est une manière de transmettre une information.

A la base de la base

La raison pour laquelle on s’embrouille facilement avec le binaire, c’est que la plupart d’entre nous ont appris à lire les nombres par cœur. On lit un nombre comme un tout.

En vérité, un nombre est une notation qui obéit à des règles très strictes, et non seulement chaque chiffre donne une information, mais sa place dans le nombre donne une information. Ce qu’on appelle “centaine”, “millier”, etc, sont des notions qu’on manipule sans y penser, mais derrière, il y a en fait un système.

Le système des bases.

Quand on utilise les chiffres arabes pour écrire un nombre, on utilise une série de symboles, et on les aligne dans un ordre précis. Selon la base utilisée, l’ordre implique une valeur différente.

On ne s’en rend pas compte, mais on utilise tous les jours l’ordre de la base 10. On pense en base 10. On a tellement l’habitude de tout calculer en base 10 qu’on ne se rend pas compte qu’on suit une règle précise pour cela.

Que signifie le fait d’utiliser une base 10 ?

Deux choses.

La première, c’est que l’on a DIX symboles pour représenter DIX valeurs. On ne sait représenter directement que ces DIX valeurs là.

          0
*         1
**        2
***       3
****      4
*****     5
******    6
*******   7
********  8
********* 9

On a des valeurs (un nombre d’étoiles), et on fait correspondre complètement arbitrairement dix symboles pour ces valeurs. On connait ces symboles par cœur, ce sont des conventions. Ils ne signifient rien par eux-même, nous leur avons donné cette signification.

Ça c’est la première règle. La deuxième règle, c’est que quand on tombe à cours de symboles parceque la valeur est trop grande, on utilise la position des symboles pour dire combien de groupes de dizaines de symboles il y a.

Par exemple avec ce nombre :

10

10 veut dire (en lisant de droite à gauche, ce sont des chiffres arabes, je le rappelle), 0 unité, et 1 groupe d’une dizaine.

Si on a 587, on dit (toujours de droite à gauche) : il y a 7 unités et 8 groupes d’une dizaine et 5 groupes d’une dizaine de dizaines.

La position du chiffre dit si l’on parle d’un groupe de 1, de 10, de 10 groupes de 10 (cent), de 10 groupes de 10 groupes de 10 (1000), etc.

C’est pour ça qu’on parle de base 10. Tout est en groupe de 10. On fait des paquets de 10. Puis des paquets de 10 paquets de 10. Puis des paquets de 10 paquets de 10 paquets de 10…

Exemple avec le nombre 1982 :

[1][9][8][2]
 |  |  |  |
 |  |  |  2 unités
 |  |  8 groupes de 10
 |  9 groupes de 10 groupes de 10 (10 x 10 = 100)
1 groupe de 10 groupes de 10 groupes de 10 (10 x 10 x 10 = 1000)

Si on lit un chiffre comme un tableau en informatique (donc en commençant par 0), mais de droite à gauche, on peut utiliser les puissances pour noter ça de manière propre :

[1][9][8][2]
 |  |  |  |
 |  |  |  2 est à la place 0, sa "valeur" est 2 x 10^0 = 2 * 1 = 2
 |  |  8 est à la place 1, sa "valeur" est 8 x 10^1 = 8 * 10 = 80
 |  9 est à la place 2, sa "valeur" est 9 x 10^2 = 9 x 10 x 10 = 9 x 100 = 900
1 est à la place 3, sa "valeur" est 1 x 10^3 = 1 x 10 x 10 x 10 = 1 x 1000 = 1000

Ça vous parait évident sans avoir à faire le calcul ? C’est parce que vous êtes tellement habitué à compter en base 10 que c’est automatique pour vous. Pourtant, en base 2, c’est exactement la même chose.

On a DEUX symboles pour représenter DEUX valeurs. On ne sait représenter que ces DEUX valeurs là.

  0
* 1

Quand on tombe à cours de symbole parce qu’une valeur est trop grande, on utilise la position des symboles pour dire combien de groupes de deux symboles il y a.

Par exemple avec ce nombre :

10

10, qui ne se prononce PAS dix, veut dire (en lisant de droite à gauche), 0 unité, et 1 groupe d’une paire. Donc deux en base 10 :-).

Pour les chiffres plus longs, c’est kiff, kiff :

[1][1][1][0]
 |  |  |  |
 |  |  |  0 unité
 |  |  1 groupes de 2
 |  1 groupes de 2 groupes de 2 (2 x 2 = 4 en base 10)
1 groupe de 2 groupes de 2 groupes de 2 (2 x 2 x 2 = 8 en base 10)

Donc en base 10 : 8 + 4 + 2, soit 14.

Si on le représente en puissance de 2 :

[1][1][1][0]
 |  |  |  |
 |  |  |  0 est à la place 0, sa "valeur" est 0 x 2^0 = 0 * 1 = 0
 |  |  1 est à la place 1, sa "valeur" est 1 x 2^1 = 1 * 2 = 2
 |  1 est à la place 2, sa "valeur" est 1 x 2^2 = 1 x 2 x 2 = 1 x 4 = 4
1 est à la place 3, sa "valeur" est 1 x 2^3 = 1 x 2 x 2 x 2 = 1 x 8 = 8

En gros pour convertir du binaire en base 10, il suffit de réciter les puissances de deux dans sa tête :

2, 4, 8, 16, 32… (2 fois 2 = 4, 2 fois 4 = 8, 2 fois 8 = 16, 2 fois 16 = 32…)

Et les appliquer de droite à gauche :

[1][0][1][1][1][0] => [1 * 32] + [0 * 16] + [1 * 8] + [1 * 4] + [1 * 2] + [O * 1] => 46 en base 10

Une illustration graphique ?

Voici quarante-six étoiles :

**********************************************

Si je les représente en base 10, je noterais ça 46, soit 4 groupes de dizaines, et 6 unités :

    4          6

**********   ******
**********
**********
**********

Si je le représente en base 2, je le note 101110: soit 1 groupe de trente-deux, 0 groupe de seize, 1 groupe de huit, 1 groupe de quatre, 1 groupe de deux, et 0 unité.

                1                  0      1        1     1    0

********************************   -   ********   ****   **   -

On peut faire ça avec n’importe quelle base. On peut compter en base 3 (ternaire) ou en base 4, 5, 6… On utilise notamment parfois en informatique la base 8 (octale) et la base 16 (hexadécimale). Pour cette base, il y a 16 symboles : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, représentant chacun une valeur, puis, quand on a plus de symbole, on utilise la position pour signaler un paquet de 16.

Passons au code

En Python on peut écrire un nombre en binaire en le préfixant de ‘0b’:

>>> 0b101110
46

Mais par défaut Python représente toujours à l’affichage ce nombre en base 10. La valeur derrière est la même de toute façon (qui, de manière amusante, est sockée en binaire).

Si vous voulez forcer l’affichage du nombre en binaire, vous pouvez utilisez bin() ou format() :

>>> 'Le nombre {0} en binaire donne : {0:b}'.format(46)
u'Le nombre 46 en binaire donne : 101110'

>>> bin(46)
'0b101110'

A l’inverse, si vous avez une représentation textuelle d’un chiffre binaire, vous pouvez préciser la base via le second paramètre de int() :

>>> int('101110') # la base 10 est le réglage par défaut...
101110
>>> int('101110', 10) # ...donc on récupère cent un mille cent dix.
101110
>>> int('101110', 2) # si on précise la base 2, on récupère quarante-six.
46

Mais si on n’a pas codé sa petite fonction pour faire pareil à la main à 50 ans, on a raté sa vie. Donc, voici un exemple simple d’une fonction qui convertit une représentation textuelle d’un chiffre binaire en int Python :

def bin2dec(s):

    # on rejette les chaînes vides
    if not s:
        raise ValueError('The string cannot be empty')

    total = 0

    # on itère sur la chaîne, de droite à gauche
    reversed_number = s[::-1]
    for pos, num in enumerate(reversed_number):

        # on multiplie le chiffre en cours par la puissance de 2
        # qui correspond à sa position
        total += int(num) * (2 ** pos)

    return total

print(bin2dec('0'))
print(bin2dec('1'))
print(bin2dec('10'))
print(bin2dec('101'))
print(bin2dec('10011100'))

## 0
## 1
## 2
## 5
## 156

Et la fonction inverse :


from __future__ import division # pour être tous d'accord sur la division entière

def dec2bin(i):

    numbers = []

    if i == 0:
        return i

    # On divise (avec la division entière) par deux jusqu'à ce qu'il ne reste
    # plus rien à diviser (on fait des paquets de 2, quoi).
    # Avant chaque division par deux, on regarde si il y aura un reste
    # pour la prochaine division. Si oui, on met 1 pour le paquet de 2 actuel,
    # sinon on met 0.
    # C'est comme pour la multiplication à la main ("je met 2 et je retiens 1"),
    # mais à l'envers.
    while i != 0:
        numbers.append(str(i % 2))
        i = i // 2

    # Ensuite on inverse tout ça et on join() tout en une belle string
    return (''.join(numbers[::-1]))

print(dec2bin(0))
print(dec2bin(1))
print(dec2bin(2))
print(dec2bin(5))
print(dec2bin(156))

## 0
## 1
## 10
## 101
## 10011100

Et les opérateurs bitwise ?

Dans les tutos on voit souvent mélanger la notion de binaire, avec la représentation des nombres, du texte, et les opérateurs spécialisés dans la manipulation du binaire :

|, ^, ~, >>, <<, >>>, &, etc

C’est le bordel.

On les appelle ces opérateurs “bitwise”, et ils sont essentiellement de deux types : les shifts, et les opérateurs logiques.

Les shifts, souvent notés >>, <<, etc, décalent d’un bit. Un bit, c’est une case dans un nombre binaire, une d’un chiffre position, qui prend donc la valeur 0 ou 1.

Ex: 1010 est un nombre de 4 bits. 10100 est un nombre de 5 bits.

Par décaler, j’entends qu’ils poussent littéralement les bits dans un sens ou dans l’autre :

1010 >> 1 va donner 101 (les bits se sont décalés vers la droite d’un cran, c’est le rshift). Ca fait disparaitre un bit à droite.
1010 << 1 va donner 10100 (les bits se sont décalés vers la gauche d’un cran, c’est le lshift). Ca rajoute un bit à droite.

Si on met un plus grand nombre, on décale de plusieurs bits :

1010 << 3 va donner 1010000 (les bits se sont décalés vers la gauche de 3 crans)

C’est une opération très rapide, puisqu’il suffit de décaler les chiffres, sans faire de calculs complexes. Pourtant, c’est l’équivalent de certaines opérations mathématiques.

Par exemple, un lshift peut être équivalent à une multiplication par une puissance de 2.

Ainsi, si je prends le chiffre 10 en (binaire 1010), et que je lui applique un lshift de 3, c’est comme si je le multipliais par 2^3 (soit 8). Démo en Python :

>>> 0b1010
10
>>> 0b1010 << 3
80

Mais l'opération prend beaucoup moins de cycles CPU qu'une multiplication. C'est une astuce d'optimisation qui a été beaucoup utilisée au paléolithique et qui reste en vigueur chez les cryptonerds. Néanmoins ne l'utilisez pas en Python, les gains sont minimes. C'est surtout utile pour les codes C ou assembleur qui sont déjà sur optimisés et ont besoin de ce petit coup de pouce supplémentaire.

Ensuite il y a les opérateurs logiques. Ils appliquent tout simplement la logique booléenne à chaque bit d'un nombre avec chaque bit d'un autre nombre dont la position correspond. Par exemple :

Si je fais "1101 ET (représenté souvent par '&') 110", je vais obtenir :

[1][1][0][1]
 |  |  |  |
 &  &  &  &
 |  |  |  |
[ ][1][1][0]
 |  |  |  |
 v  v  v  v
 0  1  0  0

1 et rien => vrai ET faux ? => faux => 0
1 et 1 => vrai ET vrai ? => vrai => 1
0 et 1 => faux ET vrai ? => faux => 0
1 et 0 => faux ET vrai ? => faux => 0

C'est de la logique booléenne de base, vous pouvez la tester avec True, False, and et or dans votre shell si ça ne vous parle pas.

En code Python :

>>> bin(0b1101 & 0b100)
'0b100'

On l'utilise parfois pour faire ce qu'on appelle des "masques", c'est à dire représenter une série d'états sous forme de grille de bits, et changer un état de cette grille en appliquant une opération logique avec une autre grille.

Je sais, ça fait flou comme ça. Un exemple peut-être ?

Les permissions d'un fichiers Unix sont Read (lire), Write (écrire) et Execute (exécuter). Soit "rwx". Elles s'appliquent à User (utilisateur), Group (groupe), Other (autre), soit "ugo".

Cela est peut être représenté par 3 groupes de 3 bits, soit 9 bits :

    U         G         O
[r][w][x] [r][w][x] [r][w][x]

1 veut dire que la permission est donnée, 0 veut dire qu'elle n'est pas donnée.

Ainsi :

[1][1][1] [1][0][1] [1][0][1]

Veut dire que l'utilisateur à le droit de lire, écrire et exécuter le fichier. Le groupe et les autres n'ont que le droit de le lire et exécuter le fichier.

Si vous mettez ça en base 10, ça donne 7/5/5 (et NON sept-cent-cinquante-cinq car il y a 3 valeurs). Cela explique le fameux chmod 755 que vous voyez dans tous les tutos bash.

Si vous faites :

chmod g+w fichier

Le programme va rajouter la permission d'écrire au groupe. On peut représenter cette opération par l'application d'une opération "OU" (représentée par '|') :

En effet on a 101 pour les permissions du groupe et on veut obtenir 111. Il suffit d'appliquer le masque 010 (qui correspond pile poil à un 1 à la case de la permission voulue) :

>>> bin(0b101 | 0b010)
'0b111'

Représentation des nombres dans un ordinateur

Cette partie c'est vraiment pour vos beaux yeux hein, ne vous en faites pas si vous ne pigez pas tout. Je ne vais pas dans le détail car ça demanderait un autre article complet, plus un comparatif des archis, des notions, des systèmes de typages des langages, etc. Et j'ai vraiment pas envie de le faire.

Bon, déjà, la représentation des nombres dans un ordi, c'est une grande source de confusion : en effet, l’ordinateur ne stocke pas la représentation binaire directe d'un nombre.

On pourrait se dire : l’ordinateur manipule du binaire, on traduit nos nombres en binaire, on fout tout tel quel dedans, et yala.

Mais un ordinateur a des limitations techniques. Notamment, il possède des plages de mémoires d'un nombre de bits limité. Il faut donc faire tenir la représentation de la valeur sur cet espace limité.

Si votre espace de mémoire pour représenter une valeur est limité à 16 bits et que vous devez stocker un nombre entier dedans, il pourra être au maximum 2^16 - 1 = 65535.

C'est pour cela qu'en C on déclare le type d'une variable qui va contenir un nombre qu'on va manipuler. On dit clairement, "alloue moi cet espace mémoire, mon nombre ne sera jamais plus grand que ça". En Python, c'est automatiquement et transparent.

(C'est aussi pour ça qu'on parle de processeur 16bits, 32bits, 64bits, etc. C'est la taille des jeux d'instructions qu'ils sont capables de traiter en un cycle.)

Mais il se rajoute une autre complexité : un entier peut avoir un signe plus ou un signe moins. Il faut bien le représenter quelque part. On peut le faire en utilisant le premier bit, mais aussi avec un format appelé "le complément à deux". Comme cet article est déjà super long, je vais pas me lancer dans l'explication. J'ai mal aux doigts. Sachez juste qu'on utilise en quelque sorte "l'inverse" du nombre.

Encore une fois, en C, on déclare que l'on va utiliser un "signed int" ou un "unsigned int" à l'initialisation d'une variable.

Enfin, il y a les nombres à virgule, aussi appelés "flottants" (d'où le type "float" en Python).

On pourrait se dire qu'on utilise les puissances de deux négatives :

0.92 se lit en base 10 "0 x 1 + 9 x 10^-1 + 2 x 10^-2"

On garderait la logique des positions.

Donc :

0.11 se lit en base 2 "0 x 1 + 1 x 2^-1 + 1 x 2^-1 + 1 x 2^-2"

A priori on pourrait stocker le nombre en utilisant ce principe. Mais ça ne permet pas de présenter les nombres très grands (tendant vers l'infini) ou très petit (tendant vers zéro). Alors on utilise un autre principe : on représente un nombre avec la formule m x 2^n, très couramment sur 64 bits (ce que fait le type float Python, mais on peut avoir plus de choix de manoeuvre en utilisant numpy).

m est un nombre à virgule compris entre 1 et 2 (ce dernier étant exclus). On appelle ça la mantisse. Ce sera stocké sur 52 bits en utilisant une représentation binaire littérale.

n est l'exposant, c'est un nombre entier compris entre -1022 et 1023, stocké sur 11 bits.

On garde le premier bit pour le signe.

Et il y a des exceptions pour 0, l'infini et NaN.

(et je n'ai pas la moindre putain d'idée de comment sont représentés les complexes)

Si vous êtes en manque d'aspirine, tout ça est codifié dans la norme IEEE 754, que Python respecte scrupuleusement.

Mais ça vous fait une belle jambe pas vrai ?

Tout ça pour dire que même avec une belle précision, le nombre de chiffres stockés après la virgule est limité. Du coup, on a une valeur approximative stockée dans l'ordi, ce qui fait que les calculs se font sur des approximations :

>>> "{0:.35f}".format(0.9999999999999999999999999999999999999999999999999999999 + 0.000000000000000000001)
u'1.00000000000000000000000000000000000'

En Python on peut gagner en précision en utilisant le module decimal:

>>> from decimal import Decimal
>>> Decimal("0.9999999999999999999999999999999999999999999999999999999") + Decimal("0.000000000000000000001")
Decimal('1.000000000000000000001000000')

C'est plus lent, mais plus intuitif. Quand on manipule des sous, du temps, des unités de poids, de température, de longueur, etc, c'est utile.

Représentation du texte

Pour le texte, c'est encore autre chose. Et là je vous renvoie à l'article sur les encodings.

En résumé, le stockage du texte n'a rien à avoir avec l'arithmétique binaire, c'est juste qu'on fait correspondre un nombre (on montre souvent sa représentation binaire pour l'ASCII et sa représentation hexa pour l'unicode, mais ça reste un nombre, point) à un caractère. Il y a un grand tableau en mémoire qui fait cette correspondance de manière totalement arbitraire.

C'est une norme arbitraire de correspondance, c'est tout.

Donc quand on vous dit "ecrivez les mots 'glory hole' en binaire", ça ne veut rien dire.

Une bonne consigne serait de dire : "donnez moi la suite de nombres, représentés sous forme binaire, qui corresponde aux caractères qui compose les mots 'glory hole' selon l'encoding ASCII".

On peut obtenir la valeur numérique ASCII en utilisant ord() en Python :

>>> for x in 'glory hole':
    print(ord(x))
...
103
108
111
114
121
32
104
111
108
101

Après, c'est juste du formatage pour afficher le binaire :

>>> for x in 'glory hole':
...     print("{:b}".format(ord(x)))
...
1100111
1101100
1101111
1110010
1111001
100000
1101000
1101111
1101100
1100101
]]>
http://sametmax.com/la-fin-du-mystere-du-binaire-nananere/feed/ 52 8808
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