1Ouvrir, lire, ecrire un fichier texte
Jusqu'ici les donnees disparaissaient a la fin du programme. Les fichiers assurent la persistance : ils permettent de conserver des resultats, de traiter des jeux de donnees trop volumineux pour etre saisis a la main, et d'echanger de l'information entre programmes.
Un fichier est une suite d'octets stockee sur un support persistant, designee par un chemin. On distingue :
- les fichiers texte : la suite d'octets est interpretee comme des caracteres selon un encodage (UTF-8, Latin-1...) et decoupee en lignes ;
- les fichiers binaires : les octets sont manipules tels quels (images, sons, donnees compressees).
Au programme, on ne traite que les fichiers texte.
Ouvrir et fermer
f = open("donnees.txt", "r", encoding="utf-8")
contenu = f.read()
f.close() # obligatoire, et facile a oublier
with
with open("donnees.txt", "r", encoding="utf-8") as f:
contenu = f.read()
# ici le fichier est FERME, meme si une exception a ete levee
Le bloc with garantit la fermeture. C'est la forme attendue systematiquement :
un fichier non ferme peut perdre des donnees non ecrites (le tampon n'est pas vide) et epuise les
descripteurs du systeme.
Les modes d'ouverture
| Mode | Nom | Si le fichier existe | S'il n'existe pas |
|---|---|---|---|
"r" | lecture | ouvert au debut | FileNotFoundError |
"w" | ecriture | vide integralement | cree |
"a" | ajout | ouvert a la fin | cree |
"x" | creation exclusive | FileExistsError |
cree |
"r+" | lecture-ecriture | ouvert au debut | FileNotFoundError |
"w" detruit
Ouvrir un fichier existant en mode "w" efface immediatement tout son contenu,
avant meme la premiere ecriture. Pour ajouter a la suite, il faut le mode
"a". Cette erreur est irrattrapable.
Sans encoding="utf-8", Python utilise l'encodage par defaut du systeme, qui varie
(UTF-8 sous Linux et macOS, souvent CP1252 sous Windows). Le meme programme lit alors des
caracteres accentues differents selon la machine, ou leve
UnicodeDecodeError.
Lire
with open("f.txt", encoding="utf-8") as f:
tout = f.read() # UNE chaine contenant tout le fichier
with open("f.txt", encoding="utf-8") as f:
lignes = f.readlines() # liste des lignes, AVEC le "\n" final
with open("f.txt", encoding="utf-8") as f:
premiere = f.readline() # une seule ligne
with open("f.txt", encoding="utf-8") as f:
for ligne in f: # LA BONNE FACON : ligne par ligne, paresseuse
traite(ligne.rstrip("\n"))
read() et readlines() chargent tout le fichier en
memoire : \(\Theta(N)\) octets. Le parcours for ligne in f ne garde qu'une ligne a
la fois : \(\Theta(L)\) ou \(L\) est la longueur de la plus longue ligne. Sur un fichier de
plusieurs gigaoctets, seule la seconde methode fonctionne.
\n parasite
for ligne in f:
print(len(ligne)) # compte le "\n" final !
n = int(ligne) # fonctionne ici, int ignore les blancs
mots = ligne.split() # split() sans argument ignore aussi le "\n"
ligne = ligne.rstrip("\n") # la facon explicite et sure
Utiliser rstrip("\n") plutot que strip() si les espaces de debut de
ligne sont significatifs (fichiers indentes).
Ecrire
with open("sortie.txt", "w", encoding="utf-8") as f:
f.write("premiere ligne\n") # le \n n'est PAS ajoute automatiquement
f.write(f"resultat : {42}\n")
f.writelines(["a\n", "b\n"]) # n'ajoute aucun \n non plus
print("via print", file=f) # print ajoute le \n : souvent plus pratique
def sauvegarde(nom, lignes):
"""Ecrit une liste de chaines, une par ligne."""
with open(nom, "w", encoding="utf-8") as f:
for l in lignes:
f.write(str(l) + "\n")
def ajoute_journal(nom, message):
"""Ajoute une ligne a la fin d'un journal, sans ecraser."""
with open(nom, "a", encoding="utf-8") as f:
f.write(message + "\n")
2Fichiers CSV, traitement de donnees et erreurs
Le format CSV
Un fichier CSV (comma-separated values) represente un tableau : une ligne par enregistrement, des champs separes par un delimiteur (virgule, point-virgule ou tabulation). La premiere ligne contient souvent les noms des colonnes.
nom,classe,note
Amine,MPSI,15
Sara,PCSI,18
Youssef,MPSI,12
def lit_csv(nom, sep=","):
"""Lit un CSV a en-tete et renvoie une liste de dictionnaires."""
with open(nom, encoding="utf-8") as f:
entetes = f.readline().rstrip("\n").split(sep)
lignes = []
for ligne in f:
ligne = ligne.rstrip("\n")
if not ligne: # on ignore les lignes vides
continue
valeurs = ligne.split(sep)
lignes.append(dict(zip(entetes, valeurs)))
return lignes
def ecrit_csv(nom, lignes, colonnes, sep=","):
"""Ecrit une liste de dictionnaires au format CSV."""
with open(nom, "w", encoding="utf-8") as f:
f.write(sep.join(colonnes) + "\n")
for l in lignes:
f.write(sep.join(str(l.get(c, "")) for c in colonnes) + "\n")
split naif
Un champ peut contenir le separateur s'il est entre guillemets :
"Dupont, Jean",MPSI,15. Le split le decouperait a tort. Le module
standard csv gere correctement ces cas, ainsi que les guillemets echappes et les
sauts de ligne dans un champ.
import csv
with open("notes.csv", encoding="utf-8", newline="") as f:
for ligne in csv.DictReader(f):
print(ligne["nom"], ligne["note"])
with open("sortie.csv", "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=["nom", "classe", "note"])
w.writeheader()
w.writerows(donnees)
Le newline="" est necessaire pour eviter les lignes vides parasites sous
Windows.
Chaine de traitement type
def analyse_notes(fichier):
"""Lit un CSV de notes et renvoie des statistiques par classe."""
# 1. LECTURE : convertir les types des la lecture
donnees = []
with open(fichier, encoding="utf-8") as f:
entetes = f.readline().rstrip("\n").split(",")
for numero, ligne in enumerate(f, start=2):
champs = ligne.rstrip("\n").split(",")
if len(champs) != len(entetes):
print(f"ligne {numero} ignoree : {len(champs)} champs au lieu de {len(entetes)}")
continue
enreg = dict(zip(entetes, champs))
try:
enreg["note"] = float(enreg["note"])
except ValueError:
print(f"ligne {numero} ignoree : note illisible {enreg['note']!r}")
continue
donnees.append(enreg)
# 2. TRAITEMENT : regroupement puis agregation
par_classe = {}
for e in donnees:
par_classe.setdefault(e["classe"], []).append(e["note"])
# 3. RESULTAT
return {c: {"effectif": len(v),
"moyenne": sum(v) / len(v),
"min": min(v), "max": max(v)}
for c, v in par_classe.items()}
- Lire ligne par ligne, en convertissant les types immediatement : tout ce qui vient d'un fichier est une chaine.
- Valider : nombre de champs, valeurs convertibles. Signaler et ignorer les lignes fautives plutot que planter — un jeu de donnees reel contient toujours des anomalies.
- Traiter en memoire avec les structures adaptees (dictionnaires pour les regroupements).
- Ecrire le resultat, ou l'afficher.
Gestion des erreurs
def lit_ou_defaut(nom, defaut=""):
"""Contenu du fichier, ou une valeur par defaut en cas de probleme."""
try:
with open(nom, encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"fichier introuvable : {nom}")
except PermissionError:
print(f"acces refuse : {nom}")
except UnicodeDecodeError:
print(f"encodage incorrect : {nom} n'est pas de l'UTF-8")
return defaut
| Exception | Cause |
|---|---|
FileNotFoundError | chemin inexistant, faute de frappe |
PermissionError | droits insuffisants, fichier ouvert ailleurs |
IsADirectoryError | le chemin designe un dossier |
UnicodeDecodeError | mauvais encodage suppose |
ValueError | conversion impossible (int("abc")) |
Chemins
import os
os.path.exists("donnees.txt") # le fichier existe-t-il ?
os.path.getsize("donnees.txt") # taille en octets
os.listdir(".") # contenu du dossier courant
os.path.join("dossier", "f.txt") # separateur adapte au systeme
Un chemin relatif part du dossier courant ; un chemin absolu
part de la racine. Toujours preferer os.path.join a une concatenation manuelle, car
le separateur differe selon les systemes.
3Simulateur des modes et etude de cas
Un fichier fictif notes.txt contient trois
lignes. Observer l'effet de chaque mode d'ouverture avant d'ecrire.
Etude de cas complete — un carnet de notes
import os
FICHIER = "carnet.csv"
COLONNES = ["nom", "matiere", "note"]
def charge(fichier=FICHIER):
"""Charge le carnet, ou renvoie une liste vide s'il n'existe pas encore."""
if not os.path.exists(fichier):
return []
donnees = []
with open(fichier, encoding="utf-8") as f:
entetes = f.readline().rstrip("\n").split(",")
for ligne in f:
ligne = ligne.rstrip("\n")
if not ligne:
continue
e = dict(zip(entetes, ligne.split(",")))
try:
e["note"] = float(e["note"])
except (KeyError, ValueError):
continue
donnees.append(e)
return donnees
def enregistre(donnees, fichier=FICHIER):
"""Ecrit tout le carnet (ecrasement volontaire)."""
with open(fichier, "w", encoding="utf-8") as f:
f.write(",".join(COLONNES) + "\n")
for e in donnees:
f.write(",".join(str(e[c]) for c in COLONNES) + "\n")
def ajoute(nom, matiere, note, fichier=FICHIER):
"""Ajoute une note SANS relire tout le fichier : mode append."""
nouveau = not os.path.exists(fichier)
with open(fichier, "a", encoding="utf-8") as f:
if nouveau:
f.write(",".join(COLONNES) + "\n")
f.write(f"{nom},{matiere},{note}\n")
def moyennes(donnees):
"""Moyenne par eleve."""
par_eleve = {}
for e in donnees:
par_eleve.setdefault(e["nom"], []).append(e["note"])
return {n: sum(v) / len(v) for n, v in par_eleve.items()}
# utilisation
for n, m, x in [("Amine", "info", 15), ("Amine", "maths", 13), ("Sara", "info", 18)]:
ajoute(n, m, x)
d = charge()
print(moyennes(d)) # {'Amine': 14.0, 'Sara': 18.0}
os.remove(FICHIER) # nettoyage
Noter le choix des modes : "a" pour ajouter une ligne (peu couteux), "w"
pour reecrire l'ensemble apres une modification. Reecrire tout le fichier a chaque ajout serait
en \(\Theta(n)\) par ajout, soit \(\Theta(n^2)\) au total.
L'essentiel du chapitre
- Toujours utiliser
with open(...) as f:et preciserencoding="utf-8". - Le mode
"w"vide le fichier des l'ouverture ;"a"ajoute a la fin. - Parcourir avec
for ligne in f:: memoire constante, quel que soit le volume. - Tout ce qui vient d'un fichier est une chaine : convertir explicitement.
- Retirer le
"\n"avecrstrip("\n"). - Anticiper
FileNotFoundErroret les lignes malformees. - Pour du CSV reel, utiliser le module
csvplutot qu'unsplit.