Texte, binaire et fichiers

Sébastien Boisgérault

Friday 25 august 2023

https://github.com/boisgera/python-fr

#7da617f

Table des matières

Chaînes de caractères

Les chaînes de caractères Python sont définies comme des suites de caractères Unicode délimités par les caractères ' ou ".

>>> s = "Hello world! 👋"
>>> s
'Hello world! 👋'

Le choix de la 🇺🇸 simple quote ou de la 🇺🇸 double quote est la plupart du temps indifférent. Préférez la double quote quand votre texte comporte des simples quotes (ou apostrophes) et réciproquement :

>>> 'Je n'ai pas compris!'
  File "<stdin>", line 1
    'Je n'ai pas compris!'
          ^
SyntaxError: invalid syntax
>>> "J'ai compris!"
"J'ai compris!"

Les caractères précédés d’un slash (\) sont interprétés comme des séquences d’échappement (🇺🇸 escape sequences) et non pas litéralement. Ainsi "\n" est un retour à la ligne, "\t" une tabulation

>>> print("a\nb")
a
b
>>> print("a\tb")
a   b

\\ un slash (et oui !), \' une simple quote et \" une double quote,

>>> print("\\")
\
>>> print('J\'ai compris!')
J'ai compris!

etc.

Un caractère Unicode est caractérisé par un 🇺🇸 code point, un entier le plus souvent représenté sous la forme “U+????????” où les ? sont des caractères hécadécimaux ; ce qui se traduit en Python par \U????????. Par exemple :

>>> ord("a")
97
>>> hex(97)
'0x61'
>>> "\U00000061"
'a'

Lorsqu’il suffit de quatre ou deux caractères hexadécimaux pour décrire le code point, on peut utiliser les syntaxes \u???? ou \x?? qui sont plus compactes

>>> "\u0061"
'a'
>>> "\x61"
'a'

Les émojis par exemple nécessitent la syntaxe la plus longue :

>>> "smiley: \U0001f600"
'smiley: 😀'
>>> 
>>> "pile of poo: \U0001f4a9"
'pile of poo: 💩'

Le chaînes de caractères se comportement également comme des collections (immuables) de caractères … même s’il n’existe pas de type “caractère” ! (Un “caractère” est en fait représenté comme une chaîne de caractères de longueur 1.)

>>> s = "Hello world! 👋"
>>> len(s)
14
>>> s[0]
'H'
>>> s[-1]
'👋'
>>> s[0:5]
'Hello'
>>> s[:5] + s[5:]
'Hello world! 👋'
>>> for c in s:
...     print(c) 
... 
H
e
l
l
o
 
w
o
r
l
d
!
 
👋
>>> list(s)
['H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', ' ', '👋']

Les f-strings permettent d’insérer au sein de chaînes de caractères des chaînes de caractères stockées dans des variables

>>> target = "world"
>>> emoji = "👋"
>>> f"Hello {target} {emoji}"
'Hello world 👋'

ou bien des données qui peuvent être représentées comme des chaînes de caractères, ou bien même des expressions qui s’évaluent en de tels objets

>>> f"1+1 = {1+1}"
'1+1 = 2'
>>> ok = True
>>> f"Annie are you ok? {'yep' if ok else 'nope'}."
'Annie are you ok? yep.'
>>> ok = False
>>> f"Annie are you ok? {'yep' if ok else 'nope'}."
'Annie are you ok? nope.'

Données binaires

les octets Python (🇺🇸 bytes) sont des suites de valeurs entières comprises entre 0 et 255 qui représentent des données binaires arbitraires. Elle sont le plus fréquemment représentées sous une forme analogue aux chaînes de caractères, mais préfixées par un b :

>>> b"Hello world!"
b'Hello world!'

Néanmoins, seul les caractères ASCII sont autorisés

>>> b"Hello world! 👋"
  File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.

Pour décrire des octets qui ne correspondent pas à des caractères ASCII, on peut utiliser la syntaxe d’échappement (🇺🇸 escape sequence) \x?? ou les ? représentent un caractère hexadécimal.

>>> b"Hello world! \xf0\x9f\x91\x8b"
b'Hello world! \xf0\x9f\x91\x8b'

Il est aussi possible d’utiliser la syntaxe d’échappement à la place des caractères ASCII

>>> b"\x48\x65\x6C\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21\x20\xf0\x9f\x91\x8b"
b'Hello world! \xf0\x9f\x91\x8b'

Les octets peuvent aussi être manipulés comme des listes (mais immuables !) d’entiers compris entre 0 et 255

>>> data = b"Hello world! \xf0\x9f\x91\x8b"
>>> data[0]
72
>>> data[0] = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
>>> list(data)
[72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 32, 240, 159, 145, 139]

D’ailleurs on peut les créer à partir d’une telle liste

>>> bytes([72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 32, 240, 159, 145, 139])
b'Hello world! \xf0\x9f\x91\x8b'

Encodage de texte

Pour être stocké dans un fichier ou transmis sur le réseau, une chaîne de caractères doit être convertie en données binaires. Il existe plusieurs méthodes pour opérer cette conversion, qu’on appelle un encodage (🇺🇸 encoding). L’encodage UTF-8 est un bon choix par défaut (notamment, parce qu’il est compatible avec le vénérable encodage ASCII mais qu’il sait gérer tous les caractères Unicode).

>>> "Hello world! 👋".encode("utf-8")
b'Hello world! \xf0\x9f\x91\x8b'

Il existe d’autres encodages, comme UTF-16, qui produisent des binaires différents.

>>> "Hello world! 👋".encode("utf-16")
b'\xff\xfeH\x00e\x00l\x00l\x00o\x00 \x00w\x00o\x00r\x00l\x00d\x00!\x00 \x00=\xd8K\xdc'

L’opération inverse est le décodage (🇺🇸 decoding) des données binaires en chaînes de caractères

>>> b'Hello world! \xf0\x9f\x91\x8b'.decode("utf-8")
'Hello world! 👋'

Vous noterez qu’il faut savoir quel encodage a été utilisé pour décoder correcter les données binaires. Si l’on se trompe, le résultat peut être déplaisant …

>>> "Sébastien".encode("utf-8").decode("cp1252")
'Sébastien'

Tous les encodages ne permettent pas de décrire tous les caractères du standard Unicode (mais UTF-8, UTF-16 et UTF-32 le permettent).

>>> "Sébastien".encode("ascii")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in position 1: ordinal not in range(128)
>>> "Sébastien".encode("cp1252")
b'S\xe9bastien'
>>> "Hello world! 👋".encode("cp1252")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/boisgera/miniconda3/envs/python-fr/lib/python3.9/encodings/cp1252.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_table)
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f44b' in position 13: character maps to <undefined>

Fichiers

Pour ouvrir un fichier afin d’y écrire du texte, vous pouvez utiliser le mode "w" (pour “write”)

>>> file = open("texte.txt", mode="w")
>>> file.write("Hello world! 👋")

mais cela n’est pas nécessairement une bonne idée, car Python va alors décider par lui-même de l’encodage utilisé pour convertir votre texte en données binaires. Il va pour cela utiliser l’encodage déclaré par votre environnement (et encore, si tout va bien …). Sur ma machine, coup de chance, il s’agit d’UTF-8, et ce choix me convient

>>> import locale
>>> locale.getpreferredencoding(False)
'UTF-8'

mais rien ne dit que ce soit la même chose sur votre machine. Si nous devons ensuite partager les fichiers texte avec d’autres personne, il faut être en mesure de savoir comment ils sont encodés, ou mieux encore, de choisir quel encodage est utilisé. Le plus sage consiste à spécifier systématiquement et explicitement quel encodage vous souhaitez utiliser.

>>> file = open("texte.txt", mode="w", encoding="utf-8")
>>> file.write("Hello world! 👋")

Et pour être totalement explicite, nous pouvons spécifier avec l’argument mode que nous souhaitons ouvrir le fichier en écriture et en mode texte, en précisant que mode="wt", au lieu de mode="w". Cela ne change rien pour l’interpréteur Python, mais cela simplifie la tâche des programmeurs qui vont être amenés à relire ce code.

>>> file = open("texte.txt", mode="wt", encoding="utf-8")
>>> file.write("Hello world! 👋")

C’est aussi une bonne habitude de fermer le fichier après usage1

>>> file = open("texte.txt", mode="wt", encoding="utf-8")
>>> file.write("Hello world! 👋")
>>> file.close()

Pour autant, si vous insérez du code Python entre l’ouverture et la fermeture du fichier et que ce code peut échouer (par exemple, s’il n’y a plus de place sur votre disque dur pour écrire "Hello world! 👋"), l’instruction de fermeture du fichier ne sera jamais exécutée. Une version plus robuste consisterait à fermer le fichier dans tous les cas (erreur ou non), ce qui peut être fait de la façon suivante :

>>> file = open("texte.txt", mode="wt", encoding="utf-8")
>>> try:
...     file.write("Hello world! 👋")
... finally:
...     file.close()
...

… mais c’est un peu lourd ! Heureusement pour nous, il existe une construction plus compacte qui offre les mêmes garanties :

>>> with open("texte.txt", mode="wt", encoding="utf-8") as file:
...     file.write("Hello world! 👋")
...

L’écriture dans un fichier se fait de façon analogue en remplaçant "w" par "r" (pour “read”) dans le mode d’ouverture du fichier.

>>> with open("texte.txt", mode="rt", encoding="utf-8") as file:
...     print(file.read())
...
Hello world! 👋

Mais, si vous voulez accéder à des données qui ne sont pas du texte en clair (🇺🇸 plain text) comme une image ou un document PDF, ou bien à du texte que vous souhaitez décoder vous-même, utilisez le mode binaire ((🇺🇸 binary)) "b" (en lecture comme en écriture) :

>>> with open("texte.txt", mode="rb") as file:
...     data = file.read()
...     print(f"{type(data) = }")
...     text = data.decode("utf-8")
...     print(text)
...
type(data) = <class 'bytes'>
Hello world! 👋

  1. Il est possible que l’écriture dans le fichier soit temporisée et n’ait lieu qu’à la fermeture du fichier. Il est aussi possible que l’ouverture du fichier “bloque” aux autres processus l’accès au même fichier, etc.↩︎