# Unicode et bytes demystifiés
.fx: home
ℙƴ☂ℌøἤ
𝖀𝖓𝖎𝖈𝖔𝖉𝖊 🅔🅣 ⒪⒞⒯⒠⒯⒮ DΣMYƧƬIFIΣƧ
Boris FELD - PyConFR, Toulouse - 2017
---
# /moi
.fx: bigbullet
* Boris FELD
* Dévelopeur Python
* Consultant Mercurial et Python chez [Octobus](https://octobus.net)
* [https://lothiraldan.github.io/](https://lothiraldan.github.io/)
* [@lothiraldan](https://twitter.com/lothiraldan)
---
# Unicode c'est ���!
---
# Testons vos connaissances
.fx: fullimage
![](images/test-it.jpg)
---
# 1. Longueur Unicode
.fx: bigbullet
Quelle est la longueur de la chaîne Unicode suivante en Python 2?
!python
len(u'😎')
* 1
* 2
* 3
* 4
---
# Longueur Unicode
Ça dépends de votre version de Python, c'est soit 1:
!bash
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:
!bash
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
.fx: bigbullet
Dans quelle situation pouvez-vous rencontrer cette erreur ?
!python
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!
!pycon
>>> x = u'é'
>>> x.encode('ascii')
Traceback (most recent call last):
File "", line 1, in
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 "", line 1, in
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 "", line 1, in
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
.fx: bigbullet
* Utilisez tout le temps `unichr`.
---
# Skeptical dog is skeptical
.fx: fullimage
---
# We must go back!
.fx: title
---
# Les années 60
.fx: fullimage
---
# Apollo 11
.fx: fullimage
---
# Woodstock
.fx: fullimage
![](images/woodstock.jpg)
---
# Quelque chose important
.fx: fullimage
![](images/stand_by.jpg)
---
# Quelque chose d'énorme
.fx: fullimage
![](images/huge.jpg)
---
# ASCII est né
.fx: fullimage
![](images/ascii.gif)
---
# La question à un millions de dollars
.fx: fullimage
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 ?
![](images/ascii_association.jpg)
---
# Problème
.fx: fullimage
Mais il y a un problème, les ordinateurs ne comprennents que le binaire. Comment transformer du texte en binaire ?
![](images/binary.jpg)
---
# 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"`.
---
#
.fx: home
---
# ASCII avec Python 2
.fx: title
---
# Qu'est-ce qu'une chaîne ?
Prenons la chaîne suivante:
!python
"pyconfr"
Une chaîne est une succession de caractères:
!python
assert list("pyconfr") == ['p', 'y', 'c', 'o', 'n', 'f', 'r']
---
# Qu'est-ce qu'un caractère ?
!python
assert type("pyparis"[0]) ==
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
.fx: fullimage
Pour récupérer le code point ASCII d'un caractère, on peut utiliser `ord`:
!python
assert ord("p") == 112
Pour l'inverse, on peut utiliser `chr`:
!python
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 |
!text
code point 🢧 encode 🢧 binaire
code point 🢤 decode 🢤 binaire
---
# Encode vs Decode
* `encode` est fait pour transformer une chaîne en binaire:
!python
string = 'abc'
bytes = bytes.encode('ascii')
assert hex(bytes) == '616263'
* `decode` est fait pour transformed du binaire en une chaîne:
!python
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...
.fx: fullimage
---
# ... right?
.fx: fullimage
![](images/facepalm-ascii.jpg)
---
# Petit problème
.fx: fullimage
![](images/i18n_large.png)
---
# L'anglais n'est pas universel 🌎 🌏
.fx: fullimage
ASCII a résolu le problème pour les USA mais pas pour le reste du monde.
![](images/David.jpg)
---
# 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
.fx: fullimage
![](images/encoding_mess.png)
---
# 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 !
.fx: fullimage
![](images/savior.jpg)
---
# Unicode le sauveur
.fx: fullimage
> 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 ?
.fx: fullimage
> 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
.fx: fullimage
Vous vous rappellez la table ASCII ?
![](images/unichart-printed.jpg)
---
# Python 2 et Unicode
.fx: title
---
# Python 2 et Unicode
Prenons le caractère Unicode `€`.
Tout d'abors, déclarez l'encoding de vos sources Python comme `utf-8`:
!python
# -*- coding: utf-8 -*-
Ensuite vous pouvez l'écrire de cette manières:
!python
u'€'
Ou:
!python
u'\u20AC'
Son code point est 8364:
!python
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
.fx: bigbullet
* 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
---
# UTF-8 partout
.fx: fullimage
![](images/utf_8_everywhere.jpg)
UTF-8 Everywhere Manifest
---
# 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:
!python
hex(u'é'.encode('utf-8')) == 'c3a9'
* `decode` est fait pour transformer du binaire en chaîne **unicode**:
!python
unhex('c3a9').decode('utf-8') == u'é'
---
# Python 2 😭 🐍 😭 🐍 😭
.fx: title
---
# 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:
!python
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:
!python
'012' + 3
Traceback (most recent call last):
File "", line 1, in
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 ?
!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 :
!python
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 :
!python
assert chr(65) == 'A'
Mais ça ne fonctionne qu'avec les code point ASCII !
!python
chr(8364)
Traceback (most recent call last):
File "", line 1, in
ValueError: chr() arg not in range(256)
Pour les code points Unicode, utilisez `unichr`:
!python
assert unichr(8364) == u'€'
---
# Python 3 ♥ 🐍 ♥ 🐍 ♥ 🐍 ♥
.fx: title
---
# 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:
!python
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`:
!python
string = ''
string.decode('ascii')
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'str' object has no attribute 'decode'
Decoder une chaîne Unicode n'a jamais fais de sens.
!python
bytes = b''
bytes.encode('utf-8')
Traceback (most recent call last):
File "", line 1, in
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:
!python
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:
!python
x = u'😎'
---
# 3. Python 3 chr
Python 3 n'a plus qu'une fonction `chr` qui replace et combine `chr` et `unichr`:
!python
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:
!text
Content-Type: text/html; charset=ISO-8859-4
# -*- coding: iso8859-1 -*-
Si vous **devez vraiment vraiment vraiment vraiment** deviner l'encoding, vous pouvez utilisez [chardet](https://github.com/chardet/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:
!python
x = u'abcé'
x.encode('ascii', errors='strict')
Traceback (most recent call last):
File "", line 1, in
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 3...
Vous pouvez aussi utiliser `replace` pour remplacer les caractères invalides par `?`:
!python
assert x.encode('ascii', errors='replace') == 'abc?'
Ou vous pouvez simplement les ignorer:
!python
assert x.encode('ascii', errors='ignore') == 'abc'
Finalement, vous pouvez aussi les remplacer par leur code XML:
!python
assert x.encode('ascii', errors='xmlcharrefreplace') == 'abcé'
---
# Conclusion
.fx: bigbullet
* 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
!python
for c in range(0x1F410, 0x1F4f0):
print (r"\U%08x"%c).decode("unicode-escape"),
🐐 🐑 🐒 🐓 🐔 🐕 🐖 🐗 🐘 🐙 🐚 🐛 🐜 🐝 🐞 🐟 🐠 🐡 🐢 🐣 🐤 🐥 🐦 🐧 🐨 🐩 🐪 🐫 🐬 🐭 🐮 🐯 🐰 🐱 🐲 🐳 🐴 🐵 🐶 🐷 🐸 🐹 🐺 🐻 🐼 🐽 🐾 🐿 👀 👁 👂 👃 👄 👅 👆 👇 👈 👉 👊 👋 👌 👍 👎 👏 👐 👑 👒 👓 👔 👕 👖 👗 👘 👙 👚 👛 👜 👝 👞 👟 👠 👡 👢 👣 👤 👥 👦 👧 👨 👩 👪 👫 👬 👭 👮 👯 👰 👱 👲 👳 👴 👵 👶 👷 👸 👹 👺 👻 👼 👽 👾 👿 💀 💁 💂 💃 💄 💅 💆 💇 💈 💉 💊 💋 💌 💍 💎 💏 💐 💑 💒 💓 💔 💕 💖 💗 💘 💙 💚 💛 💜 💝 💞 💟 💠 💡 💢 💣 💤 💥 💦 💧 💨 💩 💪 💫 💬 💭 💮 💯 💰 💱 💲 💳 💴 💵 💶 💷 💸 💹 💺 💻 💼 💽 💾 💿 📀 📁 📂 📃 📄 📅 📆 📇 📈 📉 📊 📋 📌 📍 📎 📏 📐 📑 📒 📓 📔 📕 📖 📗 📘 📙 📚 📛 📜 📝 📞 📟 📠 📡 📢 📣 📤 📥 📦 📧 📨 📩 📪 📫 📬 📭 📮 📯
---
# Merci!
.fx: fullimage
---
# References
* [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/)
* [Pragmatic Unicode](https://nedbatchelder.com/text/unipain.html)
* [Unicode In Python, Completely Demystified](http://farmdev.com/talks/unicode/)
* [What every programmer absolutely, positively needs to know about encodings and character sets to work with text](http://kunststube.net/encoding/)
* [Holy batman](http://www.manuel-strehl.de/publications/holy-batman/presentation)
* [Reddit on unicode](https://www.reddit.com/r/Unicode/)