Données binaires ================ Introduction : chiffres et nombres ----------------------------------- Si je vous parle de ce que représente le texte: ``342`` vous pouvez le voir de deux façons: 1. C'est une **suite de chiffres**: ``3``, puis ``4``, puis ``2``. 1. C'est un **nombre** (quelque part entre 300 et 350) On se sert des *chiffres* de 0 à 9 pour *représenter* des *nombres* La base 10 ---------- Plus exactement, pour passer de la suite de chiffres ``342`` au nombre, on part de la fin, puis on ajoute chaque chiffre, multiplié par la puissance de 10 adéquate:: 2 * 1 + 4 * 10 + 2 * 10 * 10 soit:: 2 + 40 + 300 ce qui fait bien 342. La base 16 ---------- En informatique, on se sert souvent de la base 16. C'est le même principe: on se sert des "chiffres" de 0 à F (A vaut 10, B vaut 11, jusqu'à F qui vaut 15) Ainsi, la suite ``DA2`` peut être interprétée comme suit :: 2 * 1 + 10 * 16 + 13 * 16 * 16 soit :: 2 + 160 + 3328 soit 3746 On appelle aussi la base 16 la base *hexadécimale*, ou *hexa* en abrrégé. La base 2 ---------- La base 2 c'est pareil, mais avec deux "chiffres" - 0 et 1. Ainsi, la suite `110` peut être interprétée comme suit :: 0 * 1 + 1 * 2 + 1 * 2 * 2 soit :: 0 + 2 + 4 soit 6. Bits et octets -------------- * Un bit (*bit* en anglais) c'est la valeur 1 ou 0 * Un octet (*byte* en anglais) c'est une suite de 8 bits À retenir --------- **Ces paquets de 8 ne veulent rien dire en eux-mêmes**. Ils n'ont de sens que dans le cadre d'une *convention*. Par exemple, l'octet '10100100' peut être un nombre écrit en binaire (164 en l'occurrence), mais peut avoir une toute autre signification Le nombre de valeurs possible augmente *très* rapidement avec le nombre d'octets: * 1 octet, c'est 255 valeurs possibles (``2 ** 8``) * 2 octets, c'est 65.536 valeurs possibles (``2 ** 16``) * 4 octets, c'est 4.294.967.296 valeurs possibles (``2 ** 32``) Bases en Python --------------- On se sert des préfixes `0b` et `0x` pour noter les nombres en base binaire ou hexadécimale respectivement:: print(0b1110) # affiche: 6 print(0xDA2) # affiche: 3490 Poids des bits -------------- Regardez l'example suivant:: x = 0b0010010 # 18 y = 0b0010011 # 19 z = 0b1010010 # 82 Notez que le premier bit est plus "fort" que le dernier on dit qu'on est en "little endian". Manipuler des octets en Python ------------------------------ On peut construrie des listes d'octets en utilsant ``bytearray`` et une liste de nombres:: data = bytearray( [0b1100001, 0b1100010, 0b1100011 ] ) # equivalent: data = bytearray([97,98,99]) # equivalent aussi: data = bytearray([0x61, 0x62, 0x63] Texte ----- On peut aussi interpréter des octets comme du texte - c'est la table ASCII .. image:: ../img/ascii-table.png La table se lit ainsi: si on veut connaître la suite de 0 et de 1 qui correspond à `B`: on lit les 3 premiers bits de haut en bas sur la colonne: `100`, puis les 4 bits sur la ligne: `0010`. Du coup 'B' en s'écrit en 7 bits: `1000010`, soit 66 en décimal, et 42 en héxadécimal ASCII - remarques ----------------- * C'est *vieux* - 1960 * Le A est pour American * Ça sert à *envoyer* du texte sur des terminaux d'où les "caractères" non-imprimables dans la liste * Mais c'est une convention *très* utilisée * Un message de 4 lettres ASCII sera souvent envoyé comme 4 octets (même si seulement 7 bits sont nécessaires) Utiliser ASCII en Python ------------------------ Avec ``chr`` et ``ord``:: x = chr(0x42) print(x) # affiche: B x = ord('B') print(x) # affiche: 66 Affichage des bytearrays en Python ---------------------------------- Python utilise ASCII pour afficher les bytearrays si les caractères sont "imprimables":: data = bytearray([97,98,99]) print(data) # affiche: bytearray(b"abc") .. note:: Notez que Python rajoute quelque chose qui ressemble à un appel de fonction lorsqu'il affiche le bytearray: ce n'est pas un *vrai* appel de fonction. Et ``\x`` et le code hexa sinon:: data = bytearray([7, 69, 76, 70]) print(data) # affiche: bytearray(b"\x07ELF") Notez bien que ce qu'affiche Python n'est qu'une *interpétation* d'une séquence d'octets. Types ----- La variable `b"abc"` est une "chaîne d'octets", de même que `"abc"` est une "chaîne de caractères". Python apelle ces types `bytes` et `str`:: print(type("abc")) # affiche: str print(type(b"abc")) # affiche: bytes bytes et bytearray ------------------ De la même manière qu'on ne peut pas modifier un caractère à l'intérieur une string, on ne peut pas modifier un bit - ou un octet dans une variable de type `bytes`:: a = "foo" # a[0] = "f" => TypeError: 'str' object does not support item assignment b = b"foo" # b[0] = 1 => TypeError: 'bytes' object does not support item assignment Par contre on peut modifier un bytearray:: b = bytearray(b"foo") b[0] = 103 print(b) # affiche: bytearray(b"goo") Conversion bytes - texte ------------------------ Avec ``encode()`` et ``decode()``:: text = "chaise" encodé = text.encode("ascii") print(encodé) # affiche: b"chaise" bytes = b"table" décodé = bytes.decode("ascii") print(décodé) # affiche: b"table" Notez que dans le deuxième exemple, on est bien en train de "décoder" un paquet de 0 et de 1. Il peut s'écrire ainsi: bytes = b"\x74\x61\x62\x6c\x65" décodé = bytes.decode("ascii") print(décodé) # affiche: table Plus loin que l'ASCII --------------------- Vous avez sûrement remarquer qu'il n'y a pas de caractères accentués dans ASCII. Du coup, il existe d'autres *conventions* qu'on appelle "encodage". On peut spécifier l'encodage quand on appelle la méthode ``decode()``:: # latin-1: utilisé sur certains vieux sites data = bytearray([233]) lettre = data.decode('latin-1') print(lettre) # affiche: 'é' # cp850: dans l'invite de commande Windows data = bytearray([233]) lettre = data.decode('cp850') print(lettre) # affiche: 'Ú' Notez que la même suite d'octets a donné des résultats différents en fonction de l'encodage! Unicode -------- L'Unicode c'est deux choses: 1. Une **table** qui associe un un "codepoint" à chaque caractère 2. Un encodage particulier, l'UTF-8, qui permet de convertir une suite d'octets en suite de codepoint et donc de caractères UTF-8 en pratique ------------------ D'abord, UTF-8 est compatible avec ASCII:: encodé = "abc".encode("utf-8") print(encodé) # Affiche: b'abc' Ensuite, certains caractères (comme ``é``) sont représentés par 2 octets:: encodé = "café".encode("utf-8") print(encodé) # Affiche: b'caf\xc3\xa9" Enfin, certains caractères (comme les emojis) sont représentés par 3 voire plus octets. .. warning:: Toutes les séquences d'octets ne sont pas forcément valides quand on veut les décoder en UTF-8 Conséquences ------------- * On peut représenter *tout* type de texte avec UTF-8 (latin, chinois, coréen, langues disparues, ....) * On ne peut pas accéder à la n-ème lettre directement dans une chaîne encodée en UTF-8, il faut parcourir lettre par lettre (ce qui en pratique est rarement un problème).