Unicode et bytes demystifiés

ℙƴ☂ℌøἤ
𝖀𝖓𝖎𝖈𝖔𝖉𝖊 🅔🅣 ⒪⒞⒯⒠⒯⒮ DΣMYƧƬIFIΣƧ

Boris FELD - PyConFR, Toulouse - 2017

/moi

Unicode c'est ���!

Testons vos connaissances

1. Longueur Unicode

Quelle est la longueur de la chaîne Unicode suivante en Python 2?

len(u'😎')
  • 1
  • 2
  • 3
  • 4

Longueur Unicode

Ça dépends de votre version de Python, c'est soit 1:

DOCKER_IMAGE=quay.io/pypa/manylinux1_x86_64
$> docker run -t -i $DOCKER_IMAGE /opt/python/cp27-cp27mu/bin/python \
-c "print len(u'\U0001f60e')"
1

Soit 2:

DOCKER_IMAGE=quay.io/pypa/manylinux1_x86_64
$> docker run -t -i $DOCKER_IMAGE /opt/python/cp27-cp27m/bin/python \
-c "print len(u'\U0001f60e')"
2

2. UnicodeEncodeError

Dans quelle situation pouvez-vous rencontrer cette erreur ?

UnicodeEncodeError: 'ascii' codec can't encode character
  • En faisant .encode('ascii')
  • En faisant .decode('ascii')
  • En faisant .decode('utf-8')
  • Dans toutes ces situations

UnicodeEncodeError

Dans toutes ces situations!

>>> x = u'é'
>>> x.encode('ascii')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 0: ordinal not in range(128)
>>> x.decode('ascii')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 0: ordinal not in range(128)
>>> x.decode('utf-8')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 0: ordinal not in range(128)

3. Chr ou unichr

Quand devez-vous utiliser chr ou unichr ?

  • Vous devez toujours utiliser chr.
  • Vous devez toujours utiliser unichr.
  • Vous devez utiliser chr pour de l'ASCII et unichr pour de l'Unicode.

Chr ou unichr

  • Utilisez tout le temps unichr.

Skeptical dog is skeptical

We must go back!

Les années 60

Apollo 11

Woodstock

Quelque chose important

Quelque chose d'énorme

ASCII est né

La question à un millions de dollars

Dans les années 60, l'American Standards Association a voulu répondre à cette question:

Comment représenter du texte de manière numérique ?

Problème

Mais il y a un problème, les ordinateurs ne comprennents que le binaire. Comment transformer du texte en binaire ?

Une solution plutôt simple

On savait déjà convertier des entiers en binaire.

0   = 0000000
1   = 0000001
2   = 0000010
3   = 0000011
.............
127 = 1111111

Nous n'avons qu'à assigner à chaque lettre un entier de 0 à 127 nommé "code point".

ASCII avec Python 2

Qu'est-ce qu'une chaîne ?

Prenons la chaîne suivante:

"pyconfr"

Une chaîne est une succession de caractères:

assert list("pyconfr") == ['p', 'y', 'c', 'o', 'n', 'f', 'r']

Qu'est-ce qu'un caractère ?

assert type("pyparis"[0]) == <type 'str'>
assert len("pyparis"[0]) == 1

Un caractère peut tout représenter. Ça peut être une lettre, un chiffre ou même un emoji.

Code point en Python

Pour récupérer le code point ASCII d'un caractère, on peut utiliser ord:

assert ord("p") == 112

Pour l'inverse, on peut utiliser chr:

assert chr(112) == "p"

Code points

p y c o n f r
Code Point 112 121 99 111 110 102 114

ASCII encoding

p y p a r i s
Code Point 112 121 99 111 110 102 114
Binary 1110000 1111001 1100011 1101111 1101110 1100110 1110010
code point    🢧 encode 🢧    binaire
code point    🢤 decode 🢤    binaire

Encode vs Decode

  • encode est fait pour transformer une chaîne en binaire:

    string = 'abc'
    bytes = bytes.encode('ascii')
    assert hex(bytes) == '616263'
    
  • decode est fait pour transformed du binaire en une chaîne:

    bytes = unhex('616263')
    string = bytes.decode('ascii')
    assert string == 'abc'
    

Chaque de ces méthodes accepte un paramètre encoding pour spécifier l'algorithme de conversion à utiliser.

Everything is awesome...

... right?

Petit problème

L'anglais n'est pas universel 🌎 🌏

ASCII a résolu le problème pour les USA mais pas pour le reste du monde.

D'autres standards

ASCII utilise uniquement les 7 premiers bits d'un octet. 01100001

Sur la plupart des ordinateurs, un octet est composé de 8 bits, on peut donc représenter plus de caractères.

Et ainsi de nouveaux standards sont nés...

D'autres standards

Certains étaient basés sur ASCII et utilisaient le 8ème bit pour supporter les lettres accentuées par exemple, comme Latin1 qui définit la lettre É sur le code point 201.

Et d'autres standards, n'étaient pas du tout compatible avec ASCII; comme EBCDIC, utilisé sur les mainframes IBM, où le code point 75 1001011 représente le signe de ponctuation "." tandis qu'en ASCII il représente la lettre "A".

Et bien sûr ces standards n'étaient pas compatible entre eux...

C'était le bazar

Exemple

Texte initial a b ã é
Latin1 Code Point 97 98 227 233
Latin1 encoding 01100001 01100010 11100011 11101001
ASCII decoding a b ERROR ERROR
Mac OS Roman decoding a b È
EBCDIC decoding / ERROR T Z

Une solution miracle !

Unicode le sauveur

Un Standard pour les gouverner tous,

Un Standard pour les trouver,

Un Standard pour les amener tous,

et au nom du bien les lier

Unicode késako ?

Unicode est un standard informatique qui permet des échanges de textes dans différentes langues [...] qui vise au codage de texte écrit en donnant à tout caractère de n'importe quel système d'écriture un nom et un identifiant numérique, et ce de manière unifiée, quelle que soit la plate-forme informatique ou le logiciel utilisés.

Wikipedia

Unicode naquit en 1987-1988 grâce à la coordination entre Joe Becker de Xerox, mais aussi de Lee Collins et Mark Davis de Apple.

Les code points Unicode sont heureusement pour nous compatibles avec ASCII.


Taille Unicode

Le standard Unicode est constitué d'un répertoire de 128 172 caractères, couvrant 135 script modernes et historiques, ainsi que plusieurs ensembles de symboles.

Wikipedia

ASCII ne définit que 127 caractères, Unicode en définit 1000 fois plus !

Les caractères Unicode sont répartis en plusieurs blocs:

  • Latin basique: ab...XYZ
  • Grec, Araméen, Cherokee: ΔעᏗ
  • Scripts de droite à gauche, Cunéiforme, hiéroglyphes: 𐌸𓀀
  • Tuiles Mahjong, pièces de Domino, Cartes à jouer: 𝄞🁓🂱
  • Émoticônes, Notations musicales: 😺𝄞😎

Unicode contre ASCII

Vous vous rappellez la table ASCII ?

Python 2 et Unicode

Python 2 et Unicode

Prenons le caractère Unicode .

Tout d'abors, déclarez l'encoding de vos sources Python comme utf-8:

# -*- coding: utf-8 -*-

Ensuite vous pouvez l'écrire de cette manières:

u'€'

Ou:

u'\u20AC'

Son code point est 8364:

ord(u'€') == 8364

Problème

Essayons de convertir son code point en binaire:

Code Point 8364
Naive conversion 00100000 10101100

Multi-octets

Ça ne rentre plus dans 1 octet.

Les problèmes quand vous commencez à jouer avec des octets multiples:

  • Comment ordonner les octets, Big And Little Endian?
  • Comment reconnaître quel octet vous lisez dans un stream ou un fichier ?
  • Comment détecter et corriger des erreurs de transmission quand seulement quelques octets sont manquants ?

8364 en binaire prend 2 octets. Les code points Unicode vont bien au delà de 1 000 000, utilisant ainsi au moins 3 octets.

De multiples encoding

Comme ASCII était simple, convertir des code points ASCII en binaire était simplissime.

Mais le présence de code points Unicode utilisant plus d'un octet complexifie le processus. Il y a plusieurs manières de le faire, appelés encodings:

  • UTF-8
  • UTF-16
  • UTF-32

Choisir un encoding

  • Si vous n'êtes pas sûr, utilisez UTF-8, il est compatible avec tous les caractères, marche bien la plupart du temps et a résolu les problèmes d'octets multiple de manières élégante.

  • Si vous traitez plus de caractères asiatiques que de caractères latin, utilisez UTF-16 pour utiliser moins de place et de mémoire.

  • Si vous interagissez avec un autre programme, utilisez l'encoding de cet autre programme (qui n'a pas déjà traité du CSV ?)

Comparison of Unicode encodings - Wikipedia

Quelles sont les différences ?

A
Code Point 65 8364
Naive conversion 01000001 00100000 10101100
UTF-8 01000001 11100010 10000010 10101100
UTF-16 00000000 01000001 00100000 10101100
UTF-32 00000000 00000000 00000000 01000001 00000000 00000000 00100000 10101100

Encode contre Decode

Faisons le point sur quelque chose:

  • encode est fait pour transformer une chaîne unicode en binaire:

    hex(u'é'.encode('utf-8')) == 'c3a9'
    
  • decode est fait pour transformer du binaire en chaîne unicode:

    unhex('c3a9').decode('utf-8') == u'é'
    

Python 2 😭 🐍 😭 🐍 😭

1. Longueur d'une chaîne

Compter la longueur d'une chaîne ASCII est simple, il suffit de compter le nombre d'octets !

Mais c'est bien plus difficile avec les chaînes Unicode.

Python 2 essaye de vous donner une réponse correcte, mais n'y arrive pas toujours.


Prenons un caractères comme exemple: 😎. Son code point est 128526.

Des versions différents de Python 2

Python 2 est disponible en plusieurs versions dont 2 sont liés à la façon dont il gère l'Unicode. Cela peut être une version narrow ou une version wide. Cela change la façon dont Python stocke les chaînes.

  • Pour les code points < 65535, ça marche pareil, Python stocke chaque caractères séparement.

  • Pour les code points > 65535, ça change. Pour la version wide, la taille allouée pour les caractères est suffisante pour tous les code points Unicode. Mais avec la version narrow, le taille allouée n'est pas suffisante pour les code points > 65535, du coup Python stocke les code points comme une paire de caractères.

La version narrow utilise ainsi moins de mémoire mais celà explique pourquoi cette version retourne 2 pour len(u'😎'), c'est parce que Python 2 stocke deux caractères.

2. Encoding / Decoding en Python 2

Vous vous rappelez la signification de encode et decode ?

  • Encode transforme une chaîne Unicode en binaire.

  • Decode transforme du binaire en une chaîne Unicode.

Python 2 types

Python 2 as toujours eut un type string mais as introduit le type Unicode en Python 2.1.

Le type str en Python 2 est mal nommé parce que c'est simplement une successions d'octets. Quand vous essayez de l'afficher, Python va essayer de le décoder pour vous. Du coup pour les chaînes de caractères ASCII, encode et decode vont retourner la même chose:

x = 'abc'
assert x.encode('ascii') == x
assert x.decode('ascii') == x

Python 2 conversion de type

Python 2 est un langage fortement typé, ce qui signifie qu'il ne va pas convertir de types derrière votre dos:

'012' + 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

Mais il ne respecte pas cette propriété avec les strings. Vous vous souvenez que decode sert à convertir du binaire en une string Unicode en Python ?

x = u'é'
x.decode('utf-8')

Comme decode n'est pas appelé sur des octets, Python va d'abord essayer de convertir la chaîne en binaire et va en fait exécuter :

x = u'é'
x.encode('ascii').decode('utf-8')

C'est pourquoi vous pouvez voir une erreur UnicodeEncodeError quand vous essayez de décoder une chaîne Unicode en Python 2.

3. Python 2 chr contre unichr

Vous pouvez utiliser chr pour récupérer le caractère correspondant à un code point :

assert chr(65) == 'A'

Mais ça ne fonctionne qu'avec les code point ASCII !

chr(8364)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: chr() arg not in range(256)

Pour les code points Unicode, utilisez unichr:

assert unichr(8364) == u'€'

Python 3 ♥ 🐍 ♥ 🐍 ♥ 🐍 ♥

1. Version unique de Python 3

Python 3 stocke maintenant ses chaînes de la même manière et len retourne toujours la bonne réponse:

x = '😎'
assert len(x) == 1

2. Python 3 big change

Le plus gros changement dans Python 3 as été son système de type:

Binaire Chaînes de caractères Chaîne Unicode
Python 2
str
unicode
Python 3
bytes
str

2. Python 3 types cohérents

Maintenant que Python 3 as un type séparé pour le binaire et les chaînes, on ne peut plus se tromper entre encode et decode:

string = ''
string.decode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'decode'

Decoder une chaîne Unicode n'a jamais fais de sens.

bytes = b''
bytes.encode('utf-8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'bytes' object has no attribute 'encode'

2. Plus de préfixe u

Comme les chaînes Unicode sont maintenant la norme, Python 3 as supprimé le préfixe u pour les chaînes et l'a replacé par un préfixe b pour le binaire, vous pouvez directement écrire:

x = '😎'

Python 3.3 as réintroduit le préfix pour les bases de code qui doivent être compatibles avec Python 2 et 3, vous pouvez aussi écrire:

x = u'😎'

3. Python 3 chr

Python 3 n'a plus qu'une fonction chr qui replace et combine chr et unichr:

assert chr(65) == 'A'
assert chr(8364) == '€'

Doliprane

1. Unicode sandwich

Grâce au nouveau types de Python 3, il est maintenant plus facile d'identifier quelle partie du code doit encoder des chaînes et décoder du binaire.

binaire Monde extérieur
decode Librairie
unicode Business logic
unicode
encode Librairie
binaire Monde extérieur

Unicode sandwich

Software should only work with Unicode strings internally, decoding the input data as soon as possible and encoding the output only at the end.

Python doc on unicode

2. Utilisez l'Encoding déclaré

Vous ne pouvez pas deviner l'encoding d'une suite d'octets:

Content-Type: text/html; charset=ISO-8859-4

<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />

<?xml version="1.0" encoding="UTF-8" ?>

# -*- coding: iso8859-1 -*-

Si vous devez vraiment vraiment vraiment vraiment deviner l'encoding, vous pouvez utilisez chardet, mais rappelez vous que c'est sans garantie.

3. Gestion des erreurs

encode et decode acceptent un deuxième argument pour la gestion des erreurs. Par défaut la valeur est strict qui signifie de crasher:

x = u'abcé'
x.encode('ascii', errors='strict')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 3...

Vous pouvez aussi utiliser replace pour remplacer les caractères invalides par ?:

assert x.encode('ascii', errors='replace') == 'abc?'

Ou vous pouvez simplement les ignorer:

assert x.encode('ascii', errors='ignore') == 'abc'

Finalement, vous pouvez aussi les remplacer par leur code XML:

assert x.encode('ascii', errors='xmlcharrefreplace') == 'abc&#233;'

Conclusion

  • Utilisez Unicode dès que possible.
  • Utilisez Python 3.
  • Utilisez encode et decode en Python 2, celà réglera des bugs dans votre code et facilitera la conversion en Python 3.
  • Unicode sandwich.
  • N'essayez jamais de deviner un encoding!
  • Utilisez les possibilités de la gestion d'erreur.

Python fun

for c in range(0x1F410, 0x1F4f0):
    print (r"\U%08x"%c).decode("unicode-escape"),

🐐 🐑 🐒 🐓 🐔 🐕 🐖 🐗 🐘 🐙 🐚 🐛 🐜 🐝 🐞 🐟 🐠 🐡 🐢 🐣 🐤 🐥 🐦 🐧 🐨 🐩 🐪 🐫 🐬 🐭 🐮 🐯 🐰 🐱 🐲 🐳 🐴 🐵 🐶 🐷 🐸 🐹 🐺 🐻 🐼 🐽 🐾 🐿 👀 👁 👂 👃 👄 👅 👆 👇 👈 👉 👊 👋 👌 👍 👎 👏 👐 👑 👒 👓 👔 👕 👖 👗 👘 👙 👚 👛 👜 👝 👞 👟 👠 👡 👢 👣 👤 👥 👦 👧 👨 👩 👪 👫 👬 👭 👮 👯 👰 👱 👲 👳 👴 👵 👶 👷 👸 👹 👺 👻 👼 👽 👾 👿 💀 💁 💂 💃 💄 💅 💆 💇 💈 💉 💊 💋 💌 💍 💎 💏 💐 💑 💒 💓 💔 💕 💖 💗 💘 💙 💚 💛 💜 💝 💞 💟 💠 💡 💢 💣 💤 💥 💦 💧 💨 💩 💪 💫 💬 💭 💮 💯 💰 💱 💲 💳 💴 💵 💶 💷 💸 💹 💺 💻 💼 💽 💾 💿 📀 📁 📂 📃 📄 📅 📆 📇 📈 📉 📊 📋 📌 📍 📎 📏 📐 📑 📒 📓 📔 📕 📖 📗 📘 📙 📚 📛 📜 📝 📞 📟 📠 📡 📢 📣 📤 📥 📦 📧 📨 📩 📪 📫 📬 📭 📮 📯

Merci!