Fichiers¶
Lorsqu’un programme termine son exécution, toutes les informations stockées dans des variables sont perdues. Un moyen de les conserver est de les enregistrer dans un fichier sur disque dur. A l’intérieur de celui-ci, ces informations peuvent apparaître sous un format texte qui est lisible par n’importe quel éditeur de texte, dans un format compressé, ou sous un autre format connu par le concepteur du programme. On appelle ce dernier type un format binaire, il est adéquat lorsque les données à conserver sont très nombreuses ou lorsqu’on désire que celles-ci ne puissent pas être lues par une autre application que le programme lui-même. En d’autres termes, le format binaire est illisible excepté pour celui qui l’a conçu.
Ce chapitre abordera pour commencer les formats texte, binaire et compressé (zip) directement manipulable depuis python. Les manipulations de fichiers suivront pour terminer sur les expressions régulières qui sont très utilisées pour effectuer des recherches textuelles. A l’issue de ce chapitre, on peut envisager la recherche à l’intérieur de tous les documents textes présents sur l’ordinateur, de dates particulières, de tous les numéros de téléphones commençant par 06… En utilisant des modules tels que reportlab ou encore openpyxl, il serait possible d’étendre cette fonctionnalité aux fichiers de type pdf et aux fichiers Excel.
Format texte¶
Les fichiers texte sont les plus simples : ce sont des suites de caractères. Le format HTML et XML font partie de cette catégorie. Ils servent autant à conserver des informations qu’à en échanger comme par exemple transmettre une matrice à Excel.
Ce format, même s’il est simple, implique une certaine organisation dans la façon de conserver les données afin de pouvoir les récupérer. Le cas le plus fréquent est l’enregistrement d’une matrice : on choisira d’écrire les nombres les uns à la suite des autres en choisissant un séparateur de colonnes et un séparateur de lignes. Ce point sera abordé à la fin de cette section. Les fichiers texte que les programmes informatiques manipulent sont souvent structurées.
Ecriture¶
La première étape est l’écriture. Les informations sont toujours écrites sous forme de chaînes de caractères et toujours ajoutées à la fin du fichier qui s’allonge jusqu’à ce que toutes les informations y soient écrites. L’écriture s’effectue toujours selon le même schéma.
création ou ouverture du fichier,
écriture,
fermeture.
Lors de l’ouverture, le fichier dans lequel seront écrites les informations est créé s’il n’existe pas ou nettoyé s’il existe déjà. La fermeture permet à d’autres programmes de lire ce que vous avez placé dans ce fichier. Sans cette dernière étape, il sera impossible d’y accéder à nouveau pour le lire ou y écrire à nouveau. A l’intérieur d’un programme informatique, écrire dans un fichier suit toujours le même schéma :
f = open ("nom-fichier", "w") # ouverture
f.write ( s ) # écriture de la chaîne de caractères s
f.write ( s2 ) # écriture de la chaîne de caractères s2
...
f.close () # fermeture
Les étapes d’ouverture et de fermeture sont toujours présentes en ce qui concerne les fichiers. Il s’agit d’indiquer au système d’exploitation que le programme souhaite accéder à un fichier et interdire à tout autre programme l’accès à ce fichier. Un autre programme qui souhaiterait créer un fichier du même nom ne le pourrait pas tant que l’étape de fermeture n’est pas exécutée. En revanche, il pourrait tout à fait le lire car la lecture ne perturbe pas l’écriture.
Il faut donc toujours fermer le fichier à la fin pour indiquer que le fichier est accessible pour un autre usage. Un fichier est une ressource. Et comme ce schéma se répète toujours, le langage Python a prévu une syntaxe avec le mot-clé with.
with open ("nom-fichier", "w") as f: # ouverture
f.write ( s ) # écriture de la chaîne de caractères s
f.write ( s2 ) # écriture de la chaîne de caractères s2
...
L’instruction f.close()
est implicite et automatiquement exécutée
dès que le programme sort de la section with.
Lorsque que le programme se termine, même s’il reste des fichiers « ouverts »
pour lesquels la méthode close
n’a pas été appelée, ils seront automatiquement fermés.
Certains caractères sont fort utiles lors de l’écriture de fichiers texte
afin d’organiser les données. Le symbole ;
est très utilisé comme
séparateur de colonnes pour une matrice, on utilise également le
passage à la ligne ou la tabulation. Comme ce ne sont pas des
caractères « visibles », ils ont des codes :
\n
: passage à la ligne\t
: tabulation, indique un passage à la colonne suivante dans le format tsv (Tabulation-separated values).
Il existe peu de manières différentes de conserver une matrice dans un fichier, le programme ressemble dans presque tous les cas à celui qui suit :
mat = ... # matrice de type liste de listes
f = open ("mat.txt", "w")
for i in range (0,len (mat)) : # la fonction join est aussi
for j in range (0, len (mat [i])) : # fréquemment utilisée
f.write ( str (mat [i][j]) + "\t") # pour assembler les chaînes
f.write ("\n") # un une seule et réduire le
f.close () # nombre d'appels à f.write
Ou encore :
mat = ... # matrice de type liste de listes
with open ("mat.txt", "w") as f:
for i in range (0,len (mat)) :
for j in range (0, len (mat [i])) :
f.write ( str (mat [i][j]) + "\t")
f.write ("\n")
La fonction open()
accepte deux paramètres, le premier est le nom du fichier,
le second définit le mode d’ouverture : "w"
pour écrire (w rite),
« a » pour écrire et ajouter (a ppend),
« r » pour lire (r ead). Ceci signifie que la fonction open
sert à ouvrir un fichier quelque soit l’utilisation qu’on en fait.
A la première écriture dans un fichier (premier appel à la fonction write
,
la taille du fichier créée est souvent nulle. L’écriture dans un fichier
n’est pas immédiate, le langage python attend d’avoir reçu beaucoup
d’informations avant de les écrire physiquement sur le disque dur.
Les informations sont placées dans un tampon ou buffer.
Lorsque le tampon est plein, il est écrit sur disque dur. Pour éviter ce délai,
il faut soit fermer puis réouvrir le fichier soit appeler la méthode
flush
qui ne prend aucun paramètre. Ce mécanisme vise à réduire le nombre
d’accès au disque dur car selon les technologies,
il n’est pas nécessairement beaucoup plus long d’y écrire un caractère
plutôt que 1000 en une fois.
Ecriture en mode « ajout »¶
Lorsqu’on écrit des informations dans un fichier, deux cas se présentent. Le premier consiste à ne pas tenir compte du précédent contenu de ce fichier lors de son ouverture pour écriture et à l’écraser. C’est le cas traité par le précédent paragraphe. Le second cas consiste à ajouter toute nouvelle information à celles déjà présentes lors de l’ouverture du fichier. Ce second cas est presque identique au suivant hormis la première ligne qui change :
with open ("nom-fichier", "a") as f: # ouverture en mode ajout, mode "a"
...
Pour comprendre la différence entre ces deux modes d’ouverture, voici deux programmes. Celui de gauche n’utilise pas le mode ajout tandis que celui de droite l’utilise lors de la seconde ouverture.
Premier programme
with open ("essai.txt", "w") as f:
f.write (" premiere fois ")
f.close ()
with f = open ("essai.txt", "w") as f:
f.write (" seconde fois ")
f.close ()
Second programme
with open ("essai.txt", "w") as f:
f.write (" premiere fois ")
f.close ()
with f = open ("essai.txt", "a") as f: ###
f.write (" seconde fois ")
f.close ()
Le premier programme crée un fichier "essai.txt"
qui ne contient que les
informations écrites lors de la seconde phase d’écriture, soit
seconde fois
. Le second utilise le mode ajout lors de la seconde
ouverture. Le fichier "essai.txt"
, même s’il existait avant l’exécution
de ce programme, est effacé puis rempli avec l’information premiere fois
.
Lors de la seconde ouverture, en mode ajout, une seconde chaîne de caractères
est ajoutée. le fichier "essai.txt"
, après l’exécution du programme
contient donc le message : premiere fois seconde fois
.
Un des moyens pour comprendre ou suivre l’évolution d’un programme est d’écrire des informations dans un fichier ouvert en mode ajout qui est ouvert et fermé sans cesse. Ce sont des fichiers de traces ou de log. Ils sont souvent utilisés pour vérifier des calculs complexes. Ils permettent par exemple de comparer deux versions différentes d’un programme pour trouver à quel endroit ils commencent à diverger.
Lecture¶
La lecture d’un fichier permet de retrouver les informations stockées grâce à une étape préalable d’écriture. Elle se déroule selon le même principe, à savoir :
ouverture du fichier en mode lecture,
lecture,
fermeture.
Une différence apparaît cependant lors de la lecture d’un fichier :
celle-ci s’effectue ligne par ligne alors que l’écriture ne suit
pas forcément un découpage en ligne. Les instructions à écrire
pour lire un fichier diffèrent rarement du schéma qui suit où seule
la ligne indiquée par (*)
change en fonction ce qu’il
faut faire avec les informations lues.
with f = open ("essai.txt", "r") as f: # ouverture du fichier en mode lecture
for ligne in f : # pour toutes les lignes du fichier
print ligne # on affiche la ligne (*)
# f.close () # on ferme le fichier, ce qui est implicite avec with
Pour des fichiers qui ne sont pas trop gros (< 100000 lignes), il est possible d’utiliser la méthode readlines qui récupère toutes les lignes d’un fichier texte en une seule fois. Le programme suivant donne le même résultat que le précédent.
with open ("essai.txt", "r") as f: # ouverture du fichier en mode lecture
l = f.readlines () # lecture de toutes les lignes, placées dans une liste
for s in l:
print(s) # on affiche les lignes à l'écran (*)
Lorsque le programme précédent lit une ligne dans un fichier,
le résultat lu inclut le ou les caractères (\n
, \r
- sous Windows seulement)
qui marquent la fin
d’une ligne. C’est pour cela que la lecture est parfois suivie d’une
étape de nettoyage.
with open ("essai.txt", "r") as f: # ouverture du fichier en mode lecture
l = f.readlines () # lecture de toutes les lignes, placées dans une liste
# contiendra la liste des lignes nettoyées
l_net = [ s.strip ("\n\r") for s in l ]
Les informations peuvent être structurées de façon plus élaborée dans un fichier texte,
c’est le cas des formats HTML et
XML.
Pour ce type de format plus complexe, il est déconseillé de concevoir soi-même
un programme capable de les lire, il existe presque toujours un module qui permette
de le faire. C’est le cas du module html.parser
ou xml
.
De plus, les modules sont régulièrement mis à jour et suivent l’évolution des
formats qu’ils décryptent.
Un fichier texte est le moyen le plus simple d’échanger des matrices ou des données avec un
tableur et il n’est pas besoin de modules dans ce cas. Lorsqu’on enregistre
une feuille de calcul sous format texte, le fichier obtenu est organisé en colonnes :
sur une même ligne, les informations sont disposées en colonne délimitées par
un séparateur qui est souvent une tabulation (\t
) ou un point virgule
comme dans l’exemple suivant :
nom ; prénom ; livre
Hugo ; Victor ; Les misérables
Kessel ; Joseph ; Le lion
Woolf ; Virginia ; Mrs Dalloway
Calvino ; Italo ; Le baron perché
Pour lire ce fichier, il est nécessaire de scinder chaque ligne en une liste de chaînes de caractères, on utilise pour cela la méthode split des chaînes de caractères.
mat = [] # création d'une liste vide,
with open ("essai.txt", "r") as f: # ouverture du fichier en mode lecture
for li in f : # pour toutes les lignes du fichier
s = li.strip ("\n\r") # on enlève les caractères de fin de ligne
l = s.split (";") # on découpe en colonnes
mat.append (l) # on ajoute la ligne à la matrice
Ce format de fichier texte est appelé CSV (Comma Separated Value), il peut être relu depuis un programme python comme le montre l’exemple précédent, par Excel en précisant que le format du fichier est le format CSV et par toutes les applications ou langages traitant de données. Pour les valeurs numériques, il ne faut pas oublier de convertir en caractères lors de l’écriture et de convertir en nombres lors de la lecture.
Les nombres réels s’écrivent en anglais avec un point pour séparer la partie entière de la partie décimale. En français, il s’agit d’une virgule. Il est possible que, lors de la conversion d’une matrice, il faille remplacer les points par des virgules et réciproquement pour éviter les problèmes de conversion.
Encoding et les accents¶
Par défaut, un fichier n’accepte pas d’enregistrer des accents, uniquement
les acaractères ascii.
C’est pourquoi il faut presque tout le temps utiliser le paramètre encoding
de la fonction open()
que ce soit pour écrire ou lire.
with open("fichier.txt", "r", encoding="utf-8") as f:
texte = f.read()
L’encoding utf-8 est une façon de représenter les caractères, les caractères ascii sur un octet, les autres sur deux ou trois octets. Cet encoding est le plus fréquent sur internet.
Fichiers zip¶
Les fichiers zip sont très répandus de nos jours et constituent un standard de compression facile d’accès quelque soit l’ordinateur et son système d’exploitation. Le langage python propose quelques fonctions pour compresser et décompresser ces fichiers par l’intermédiaire du module zipfile. Le format de compression zip est un des plus répandus bien qu’il ne soit pas le plus performant. D’autres formats proposent de meilleurs taux de compression sur les fichiers textes existent comme 7-zip. Ce format n’est pas seulement utilisé pour compresser mais aussi comme un moyen de regrouper plusieurs fichiers en un seul.
Lecture (zip)¶
L’exemple suivant permet par exemple d’obtenir la liste des fichiers inclus dans un fichier zip :
import zipfile
with zipfile.ZipFile ("exemplezip.zip", "r") as fz:
for info in fz.infolist () :
print(info.filename, info.date_time, info.file_size)
Les fichiers compressés ne sont pas forcément des fichiers textes mais de tout format. Le programme suivant extrait un fichier parmi ceux qui ont été compressés puis affiche son contenu (on suppose que le fichier lu est au format texte donc lisible).
import zipfile
with zipfile.ZipFile ("exemplezip.zip", "r") as fz:
data = fz.read ("informatique/testzip.py")
print(data)
On retrouve dans ce cas les étapes d’ouverture et de fermeture même si
la première est implicitement inclus dans le constructeur de la classe
zipfile.ZipFile
.
Ecriture (zip)¶
Pour créer un fichier zip, le procédé ressemble à la création de
n’importe quel fichier. La seule différence provient du fait qu’il
est possible de stocker le fichier à compresser sous un autre nom à
l’intérieur du fichier zip, ce qui explique les deux premiers arguments
de la méthode zipfile.ZipFile.write()
.
Le troisième paramètre indique si le fichier doit être compressé
ZIP_DEFLATED
ou non ZIP_STORED.
import zipfile
with zipfile.ZipFile ("test.zip", "w") as f:
file.write ("fichier.txt", "nom_fichier_dans_zip.txt", zipfile.ZIP_DEFLATED)
Une utilisation possible de ce procédé serait l’envoi automatique d’un mail contenant un fichier zip en pièce jointe. Une requête comme python précédant le nom de votre serveur de mail permettra, via un moteur de recherche, de trouver des exemples sur Internet.
Selon les serveurs de mails, le programme permettant d’envoyer automatiquement un mail en python peut varier. L’exemple suivant permet d’envoyer un email automatiquement via un serveur de mails, il montre aussi comment attacher des pièces jointes. Il faut bien sûr être autorisé à se connecter. De plus, il est possible que l’exécution de ce programme ne soit pas toujours couronnée de succès si le mail est envoyé plusieurs fois à répétition, ce comportement est en effet proche de celui d’un spammeur.
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.utils import formatdate
from email import encoders
import os
def envoyer_mail (aqui, sujet, contenu, files = []):
de = "email de l'auteur"
msg = MIMEMultipart()
msg['From'] = de
msg['To'] = aqui
msg['Date'] = formatdate (localtime = True)
msg['Subject'] = sujet
msg.attach(MIMEText(contenu))
for file in files:
part = MIMEBase('application', 'octet-stream')
with open(file,'rb') as f:
content = f.read()
part.set_payload(content)
encoders.encode_base64(part)
part.add_header('Content-Disposition',
'attachment; filename="%s"' % os.path.basename(file))
msg.attach(part)
smtp = smtplib.SMTP("smtp.gmail.com", 587)
smtp.ehlo()
smtp.starttls()
smtp.ehlo()
smtp.login("login", "mot_de_passe")
smtp.sendmail(de, aqui, msg.as_string())
smtp.close()
envoyer_mail("destinataire", "sujet","contenu", ["mail.py"])
Manipulation de fichiers¶
Il arrive fréquemment de copier, recopier, déplacer, effacer des fichiers. Lorsqu’il s’agit de quelques fichiers, le faire manuellement ne pose pas de problème. Lorsqu’il s’agit de traiter plusieurs centaines de fichiers, il est préférable d’écrire un programme qui s’occupe de le faire automatiquement. Cela peut être la création automatique d’un fichier zip incluant tous les fichiers modifiés durant la journée ou la réorganisation de fichiers musicaux au format mp3 à l’aide de modules complémentaires tel que mutagen.
Pour ceux qui ne sont pas familiers des systèmes d’exploitation, il faut noter que Windows ne fait pas de différences entre les majuscules et les minuscules à l’intérieur d’un nom de fichier. Les systèmes Linux et Mac OSX font cette différence. Ceci explique que certains programmes aient des comportements différents selon le système d’exploitation sur lequel ils sont exécutés ou encore que certains liens Internet vers des fichiers ne débouchent sur rien car ils ont été saisis avec des différences au niveau des minuscules majuscules.
Gestion des noms de chemins¶
Le module os.path propose plusieurs fonctions très utiles qui permettent entre autres de tester l’existence d’un fichier, d’un répertoire, de récupérer diverses informations comme sa date de création, sa taille… La liste qui suit est loin d’être exhaustive mais elle donne une idée de ce qu’il est possible de faire.
Retourne le chemin absolu d’un fichier ou d’un répertoire. |
|
Retourne le plus grand préfixe commun à un ensemble de chemins. |
|
Retourne le nom du répertoire. |
|
Dit si un chemin est valide ou non. |
|
date de la dernière modification |
|
date de la dernière modification |
|
date de la création |
|
Retourne la taille d’un fichier. |
|
Retourne |
|
Retourne |
|
Retourne |
|
Construit un nom de chemin étant donné une liste de répertoires. |
|
Découpe un chemin, isole le nom du fichier ou le dernier répertoire des autres répertoires. |
|
Découpe un chemin en nom + extension. |
Copie, suppression¶
Copie le fichier |
|
Change le répertoire courant, cette fonction peut être importante lorsqu’on utilise la fonction system du module os pour lancer une instruction en ligne de commande ou lorsqu’on écrit un fichier sans préciser le nom du répertoire, le fichier sera écrit dans ce répertoire courant qui est par défaut le répertoire où est situé le programme python. C’est à partir du répertoire courant que sont définis les chemins relatifs. |
|
Retourne le répertoire courant, voir la fonction |
|
Crée le répertoire |
|
Crée le répertoire |
|
Supprime un fichier. |
|
Renomme un fichier |
|
Supprime un répertoire |
Liste de fichiers¶
La fonction listdir permet de retourner les listes des éléments inclus dans un répertoire (fichiers et sous-répertoires). Le module glob propose une fonction plus intéressante qui permet de retourner la liste des éléments d’un répertoire en appliquant un filtre. Le programme suivant permet par exemple de retourner la liste des fichiers et des répertoires inclus dans un répertoire.
<<<
import glob
import os.path
def liste_fichier_repertoire(folder, filter):
# résultats
file, fold = [], []
# recherche des fichiers obéissant au filtre
res = glob.glob(folder + "\\" + filter)
# on inclut les sous-répertoires qui n'auraient pas été
# sélectionnés par le filtre
rep = glob.glob(folder + "\\*")
for r in rep:
if r not in res and os.path.isdir(r):
res.append(r)
# on ajoute fichiers et répertoires aux résultats
for r in res:
path = r
if os.path.isfile(path):
# un fichier, rien à faire à part l'ajouter
file.append(path)
else:
# sous-répertoire : on appelle à nouveau la fonction
# pour retourner la liste des fichiers inclus
fold.append(path)
fi, fo = liste_fichier_repertoire(path, filter)
file.extend(fi) # on étend la liste des fichiers
fold.extend(fo) # on étend la liste des répertoires
# fin
return file, fold
folder = r"."
filter = "*.rst"
file, fold = liste_fichier_repertoire(folder, filter)
for i, f in enumerate(file):
print("fichier ", f)
if i >= 10:
break
for i, f in enumerate(fold):
print("répertoire ", f)
if i >= 10:
break
>>>
Le programme repose sur l’utilisation d’une fonction récursive qui explore d’abord le premier répertoire. Elle se contente d’ajouter à une liste les fichiers qu’elle découvre puis cette fonction s’appelle elle-même sur le premier sous-répertoire qu’elle rencontre. La fonction walk permet d’obtenir la liste des fichiers et des sous-répertoire. Cette fonction parcourt automatiquement les sous-répertoires inclus, le programme est plus court mais elle ne prend pas en compte le filtre qui peut être alors pris en compte grâce aux expressions régulières (voir Expressions régulières).
<<<
import os
def liste_fichier_repertoire(folder):
file, rep = [], []
for r, d, f in os.walk(folder):
for a in d:
rep.append(r + "/" + a)
for a in f:
file.append(r + "/" + a)
return file, rep
folder = r"."
file, fold = liste_fichier_repertoire(folder)
for i, f in enumerate(file):
print("fichier ", f)
if i > 5:
break
for i, f in enumerate(fold):
print("répertoire ", f)
if i > 5:
break
>>>
fichier ./.gitattributes
fichier ./.gitignore
fichier ./info.bin
fichier ./pyproject.toml
fichier ./thread_0.log
fichier ./MANIFEST.in
fichier ./CODE_OF_CONDUCT.md
répertoire ./.git
répertoire ./.ruff_cache
répertoire ./_unittests
répertoire ./dist
répertoire ./teachpyx
répertoire ./.circleci
répertoire ./.github
Sans format ou format binaire¶
Ecrire et lire des informations au travers d’un fichier texte revient à convertir les informations quel que soit leur type dans un format lisible pour tout utilisateur. Un entier est écrit sous forme de caractères décimaux alors que sa représentation en mémoire est binaire. Cette conversion dans un sens puis dans l’autre est parfois jugée coûteuse en temps de traitement et souvent plus gourmande en terme de taille de fichiers. Un fichier texte compressé, au format zip par exemple, est une alternative aux fichiers binaires en terme de taille mais il allonge la lecture et l’écriture par des étapes de compression et de décompression. Même si elle permet de relire les informations écrites grâce à n’importe quel éditeur de texte, il est parfois plus judicieux pour une grande masse d’informations d’utiliser directement le format binaire, c’est-à-dire celui dans lequel elles sont stockées en mémoire. Les informations apparaissent dans leur forme la plus simple pour l’ordinateur : une suite d’octets (bytes en anglais). Deux étapes vont intervenir que ce soit pour l’écriture :
On récupère les informations dans une suite d’octets (fonction
struct.pack()
du modulestruct
).On les écrit dans un fichier (méthode
io.RawIOBase.write()
affiliée aux fichiers).
Ou la lecture :
On lit une suite d’octets depuis un fichier (méthode
io.RawIOBase.read()
affiliée aux fichiers).On transforme cette suite d’octets pour retrouver l’information qu’elle formait initialement (fonction
struct.unpack()
).
L’utilisation de fichiers binaires est moins évidente qu’il n’y paraît et il faut faire appel à des modules spécialisés alors que la gestion des fichiers texte ne pose aucun problème. Cela vient du fait que python ne donne pas directement accès à la manière dont sont stockées les informations en mémoire contrairement à des langages tels que le C++. L’intérêt de ces fichiers réside dans le fait que l’information qu’ils contiennent prend moins de place stockée en binaire plutôt que convertie en chaînes de caractères au format texte. Par exemple, un réel est toujours équivalent à huit caractères en format binaire alors que sa conversion au format texte va souvent jusqu’à quinze caractères.
L’écriture et la lecture d’un fichier binaire soulèvent les mêmes
problèmes que pour un fichier texte : il faut organiser les données
avant de les enregistrer pour savoir comment les retrouver. Les
types immuables (réel, entier, caractère) sont assez simples à gérer
dans ce format. Pour les objets complexes, python propose une solution grâce au module
pickle
(voir aussi le modile dill
pour des types telles que des fonctions).
Ecriture dans un fichier binaire¶
L’écriture d’un fichier binaire commence par l’ouverture du fichier en mode
écriture par l’instruction open("<nom_fichier>", "wb")
.
C’est le code "wb"
qui est important (w pour write, b pour binary),
il spécifie le mode d’ouverture "w"
et le format "b"
.
La fermeture est la même que pour un fichier texte.
Le module struct
et la fonction struct.pack()
permet de convertir les informations sous forme de chaîne de caractères
avant de les enregistrer au format binaire.
La fonction struct.pack()
construit une chaîne de caractères égale
au contenu de la mémoire. Son affichage avec la fonction print
produit quelque chose d’illisible le plus souvent.
Le tableau suivant montre les principaux formats de conversion
(liste complète) :
c
: caractèreB
: caractère non signé (octet)i
: entier (4 octets)I
: entier non signé (4 octets)d
: double (8 octets)
L’utilisation de ces codes est illustrée au paragraphe suivant.
Lecture d’un fichier binaire¶
Le code associé à l’ouverture d’un fichier binaire en mode
lecture est "rb"
, cela donne : open("<nom_fichier>", "rb")
.
La lecture utilise la fonction
struct.unpack()
pour effectuer la conversion inverse, celle d’une chaîne de caractères en
entiers, réels, … Le paragraphe suivant illustre la lecture et l’écriture au format binaire.
Exemple fichier binaire¶
Cet exemple crée un fichier "info.bin"
puis écrit des informations à
l’intérieur. Il ne sera pas possible d’afficher le contenu du
fichier à l’aide d’un éditeur de texte.
<<<
import struct
# on enregistre un entier, un réel et 4 caractères
i = 10
x = 3.1415692
s = "ABCD"
# écriture
with open("info.bin", "wb") as fb:
fb.write(struct.pack("i", i))
fb.write(struct.pack("d", x))
octets = s.encode("ascii") # il faut convertir les caractères en bytes
fb.write(struct.pack("4s", octets))
# lecture
with open("info.bin", "rb") as fb:
i = struct.unpack("i", fb.read(4))
x = struct.unpack("d", fb.read(8))
s = struct.unpack("4s", fb.read(4))
# affichage pour vérifier que les données ont été bien lues
print(i)
print(x)
print(s)
>>>
(10,)
(3.1415692,)
(b'ABCD',)
Les résultats de la méthode struct.unpack()
apparaissent dans un tuple mais les données sont correctement récupérées.
Ce programme fait aussi apparaître une des particularité du format
binaire. On suppose ici que la chaîne de caractères est toujours de
longueur 4. En fait, pour stocker une information de dimension variable,
il faut d’abord enregistrer cette dimension puis s’en servir lors de
la relecture pour connaître le nombre d’octets à lire. On modifie le
programme précédent pour sauvegarder une chaîne de caractères de longueur variable.
<<<
import struct
# on enregistre un entier, un réel et n caractères
i = 10
x = 3.1415692
s = "ABCDEDF"
# écriture
with open("info.bin", "wb") as fb:
fb.write(struct.pack("i", i))
fb.write(struct.pack("d", x))
r = s.encode("utf-8")
fb.write(struct.pack("i", len(r))) # on sauve la dimension de r
fb.write(struct.pack("{0}s".format(len(r)), r))
# lecture
with open("info.bin", "rb") as fb:
i = struct.unpack("i", fb.read(4))
x = struct.unpack("d", fb.read(8))
size = struct.unpack("i", fb.read(4)) # on récupère la dimension de s
size = size[0] # l est un tuple, on s'intéresse à son unique élément
s = struct.unpack("{0}s".format(size), fb.read(size))
# affichage pour contrôler
print(i)
print(x)
print(s)
>>>
(10,)
(3.1415692,)
(b'ABCDEDF',)
Cette méthode utilisée pour les chaînes de caractères est applicable aux listes et aux dictionnaires de longueur variable : il faut d’abord stocker leur dimension. Il faut retenir également que la taille d’un réel est de huit octets, celle d’un entier de quatre octets et celle d’un caractère d’un octet. Cette règle est toujours vrai sur des ordinateurs 32 bits. Cette taille varie sur les ordinateurs 64 bits. Le programme suivant donnera la bonne réponse.
<<<
from struct import pack
print(len(pack("i", 0)))
print(len(pack("d", 0)))
print(len(pack("s", b"0")))
>>>
4
8
1
Cette taille doit être passée en argument à la méthode read
.
Objets plus complexes¶
Il existe un moyen de sauvegarder dans un fichier des objets
plus complexes à l’aide du module pickle
Celui-ci permet de stocker dans un fichier le contenu d’un dictionnaire
à partir du moment où celui-ci contient des objets standard du
langage python. Le principe pour l’écriture est le suivant :
import pickle
dico = {'a': [1, 2.0, 3, "e"], 'b': ('string', 2), 'c': None}
lis = [1, 2, 3]
with open ('data.bin', 'wb') as fb:
pickle.dump(dico, fb)
pickle.dump(lis, fb)
La lecture est aussi simple :
with open('data.bin', 'rb') as fb:
dico = pickle.load(fb)
lis = pickle.load(fb)
Un des avantages du module pickle
est de pouvoir gérer les références circulaires : il est capable d’enregistrer
et de relire une liste qui se contient elle-même,
ce peut être également une liste qui en contient une autre qui contient la première…
Le module pickle peut aussi gérer les classes définies par un programmeur
à condition qu’elles puissent convertir leur contenu en un dictionnaire
dans un sens et dans l’autre, ce qui correspond à la plupart des cas.
<<<
import pickle
import copy
class Test:
def __init__(self):
self.chaine = "a"
self.entier = 5
self.tuple = {"h": 1, 5: "j"}
def __str__(self):
return "c='{0}' e={1} t={2}".format(self.chaine, self.entier, self.tuple)
t = Test()
with open("data.bin", "wb") as fb: # lecture
pickle.dump(t, fb)
with open("data.bin", "rb") as fb: # écriture
t = pickle.load(fb)
print(t)
>>>
c='a' e=5 t={'h': 1, 5: 'j'}
Lorsque la conversion nécessite un traitement spécial, il faut surcharger les opérateurs __getstate__ et __setstate__ Ce cas se produit par exemple lorsqu’il n’est pas nécessaire d’enregistrer tous les attributs de la classe car certains sont calculés ainsi que le montre l’exemple suivant :
<<<
import pickle
import copy
class Test:
def __init__(self):
self.x = 5
self.y = 3
self.calcule_norme() # attribut calculé
def calcule_norme(self):
self.n = (self.x**2 + self.y**2) ** 0.5
def __getstate__(self):
"""conversion de Test en un dictionnaire"""
d = copy.copy(self.__dict__)
del d["n"] # attribut calculé, on le sauve pas
return d
def __setstate__(self, dic):
"""conversion d'un dictionnaire dic en Test"""
self.__dict__.update(dic)
self.calcule_norme() # attribut calculé
def __str__(self):
return "x={0} y={1} n={2}".format(self.x, self.y, self.n)
t = Test()
with open("data.bin", "wb") as fb: # lecture
pickle.dump(t, fb)
with open("data.bin", "rb") as fb: # écriture
t = pickle.load(fb)
print(t)
>>>
x=5 y=3 n=5.830951894845301
Le module pickle
ne permet de sérialiser tout type d’objet comme les fonctions. Il est
parfois utile de sauver une fonction car c’est un paramètre du programme.
Il faut dans ce cas soit le faire soi-même, soit utiliser le module
dill.