# Chapitre 07 — Les fichiers : texte, CSV et JSON > **Durée estimée :** 5 à 6 heures (cours, exercices et mini-projet) > > **Prérequis :** [Chapitre 06 — Chaînes et compréhensions](06-chaines-et-comprehensions.md) (méthodes de `str`, `split`/`join`, compréhensions) > > **Objectifs — à la fin de ce chapitre, tu sauras…** > > - manipuler des chemins de fichiers proprement avec **`pathlib`** ; > - ouvrir un fichier avec **`with open(...)`** dans le bon **mode** (`r`, `w`, `a`) et le bon **encodage** (UTF-8) ; > - lire un fichier texte en entier ou ligne par ligne, et écrire dedans ; > - lire et écrire des fichiers **CSV** avec `csv.reader`, `csv.DictReader` et `csv.DictWriter` ; > - sauvegarder et recharger des données Python au format **JSON** ; > - rendre un programme **persistant** : ses données survivent à sa fermeture. --- ## Cours ### 1. Pourquoi des fichiers ? Jusqu'ici, toutes les données de tes programmes disparaissaient à la fin de l'exécution. Ta liste de courses du chapitre 04, aussi belle soit-elle, repartait de zéro à chaque lancement. C'est parce que les variables vivent dans la **mémoire vive** (RAM) de l'ordinateur, qui est vidée quand le programme se termine. **Analogie :** la mémoire vive, c'est ton bureau : rapide d'accès, mais on le débarrasse le soir. Un fichier, c'est un classeur rangé dans une armoire : un peu plus long à consulter, mais il est encore là le lendemain. Les fichiers servent à : - **sauvegarder** des données (scores, contacts, paramètres) pour les retrouver plus tard ; - **lire** des données produites ailleurs (un export de tableur, un fichier de configuration…) ; - **échanger** avec d'autres programmes : le format CSV s'ouvre dans Excel ou LibreOffice, le format JSON est la langue commune du web. Dans ce chapitre, on travaille avec trois sortes de fichiers **texte** : | Format | Extension | Usage | Exemple de contenu | |---|---|---|---| | texte brut | `.txt` | notes, journaux, listes simples | `Acheter du pain` | | CSV | `.csv` | tableaux (lignes et colonnes) | `nom;age;ville` | | JSON | `.json` | données structurées (listes, dictionnaires) | `{"nom": "Alice", "age": 30}` | --- ### 2. Les chemins de fichiers avec `pathlib` Avant d'ouvrir un fichier, il faut savoir **où** il se trouve. C'est le rôle du **chemin** (*path*). #### Chemins absolus et relatifs - Un chemin **absolu** part de la racine du disque : `/home/alice/projets/notes.txt` (Linux, macOS) ou `C:\Users\alice\projets\notes.txt` (Windows). - Un chemin **relatif** part du **répertoire courant**, c'est-à-dire le dossier depuis lequel tu as lancé ton programme : `notes.txt`, `donnees/contacts.json`. > **Attention :** le répertoire courant est le dossier ouvert dans ton terminal au moment où tu tapes `python mon_script.py`, pas forcément le dossier où se trouve le script ! Dans VS Code, c'est généralement le dossier ouvert dans l'explorateur (cf. [chapitre 00](00-installation-environnement.md)). On verra plus bas comment construire un chemin fiable. #### Le module `pathlib` et l'objet `Path` Les chemins s'écrivent différemment selon le système (`/` sous Linux et macOS, `\` sous Windows). Le module `pathlib` de la bibliothèque standard règle ce problème : on manipule des objets `Path` qui s'adaptent tout seuls au système. On l'importe avec une ligne `from ... import ...` en haut du fichier (les imports seront détaillés au [chapitre 09](09-modules-bibliotheque-standard.md)) : ```python from pathlib import Path dossier = Path("donnees") fichier = dossier / "contacts.json" # l'opérateur / assemble les morceaux de chemin print(fichier) # → donnees/contacts.json print(fichier.name) # → contacts.json print(fichier.stem) # → contacts print(fichier.suffix) # → .json print(fichier.parent) # → donnees ``` (Sous Windows, le premier `print` affichera `donnees\contacts.json` : c'est justement l'intérêt de `pathlib`.) L'opérateur `/` est la grande astuce de `pathlib` : il colle les morceaux de chemin avec le bon séparateur. Plus besoin de concaténer des chaînes à la main. | Attribut ou méthode | Résultat pour `Path("donnees/contacts.json")` | |---|---| | `.name` | `contacts.json` (nom complet) | | `.stem` | `contacts` (nom sans extension) | | `.suffix` | `.json` (extension) | | `.parent` | `donnees` (dossier parent) | | `.exists()` | `True` si le fichier ou dossier existe | | `.is_file()` / `.is_dir()` | `True` si c'est un fichier / un dossier | | `.with_suffix(".csv")` | `donnees/contacts.csv` (change l'extension) | #### Quelques chemins utiles ```python from pathlib import Path print(Path.cwd()) # le répertoire courant (current working directory) print(Path.home()) # ton dossier personnel, ex. /home/alice ou C:\Users\alice ``` Pour obtenir le dossier **où se trouve ton script** (et non le répertoire courant), on utilise la variable spéciale `__file__` : ```python from pathlib import Path DOSSIER_SCRIPT = Path(__file__).parent FICHIER_DONNEES = DOSSIER_SCRIPT / "contacts.json" ``` Ainsi, ton programme trouvera toujours `contacts.json` à côté du script, quel que soit le dossier d'où tu le lances. C'est la méthode recommandée pour les vrais projets. (Ce code ne fonctionne que dans un fichier `.py`, pas dans la console interactive.) #### Créer des dossiers, tester l'existence, lister ```python from pathlib import Path dossier = Path("essais") / "sous_dossier" dossier.mkdir(parents=True, exist_ok=True) # crée aussi les dossiers parents, sans erreur s'il existe déjà print(dossier.exists()) # → True print(dossier.is_dir()) # → True print(Path("inexistant.txt").exists()) # → False ``` - `parents=True` : crée tous les dossiers intermédiaires manquants (ici `essais`). - `exist_ok=True` : ne provoque pas d'erreur si le dossier existe déjà. Pour lister le contenu d'un dossier, `iterdir()` renvoie tous les éléments et `glob("*.txt")` ceux qui correspondent à un motif (`*` signifie « n'importe quoi ») : ```python from pathlib import Path dossier = Path("essais") for nom in ["a.txt", "b.txt", "c.csv"]: (dossier / nom).write_text("contenu\n", encoding="utf-8") # crée des fichiers (expliqué plus bas) print(sorted(p.name for p in dossier.iterdir())) print(sorted(p.name for p in dossier.glob("*.txt"))) ``` ```text ['a.txt', 'b.txt', 'c.csv', 'sous_dossier'] ['a.txt', 'b.txt'] ``` L'ordre renvoyé par `iterdir` et `glob` n'est pas garanti : on utilise `sorted` pour un résultat stable. `glob("**/*.txt")` cherche aussi dans les sous-dossiers. --- ### 3. Ouvrir un fichier : `open`, `with` et les modes #### La fonction `open` Pour lire ou écrire dans un fichier, on l'**ouvre** avec `open(chemin, mode, encoding=...)`. On obtient un objet « fichier » sur lequel on peut lire ou écrire. Puis il faut le **fermer**, pour que tout soit bien enregistré et que le fichier soit libéré. #### Le bloc `with` : la bonne façon de faire On ouvre **toujours** un fichier avec `with` : ```python from pathlib import Path chemin = Path("bonjour.txt") with open(chemin, "w", encoding="utf-8") as fichier: fichier.write("Bonjour le monde !\n") fichier.write("Deuxième ligne, avec des accents : é è ç ù.\n") print("Fichier écrit, et déjà fermé.") ``` ```text Fichier écrit, et déjà fermé. ``` Lis-le ainsi : « avec le fichier ouvert sous le nom `fichier`, fais… ». Dès qu'on sort du bloc indenté, Python **ferme automatiquement** le fichier, même si une erreur survient au milieu. C'est comme une porte qui se referme toute seule derrière toi. Ouvre maintenant `bonjour.txt` dans VS Code : tu y verras tes deux lignes. #### Les modes d'ouverture Le deuxième argument de `open` indique ce qu'on veut faire : | Mode | Nom | Si le fichier existe | S'il n'existe pas | |---|---|---|---| | `"r"` | lecture (*read*), **par défaut** | on le lit | **erreur** `FileNotFoundError` | | `"w"` | écriture (*write*) | **son contenu est effacé !** | il est créé | | `"a"` | ajout (*append*) | on écrit **à la fin** | il est créé | | `"x"` | création exclusive | **erreur** `FileExistsError` | il est créé | Le mode `"w"` est dangereux : il vide le fichier dès l'ouverture, avant même d'écrire quoi que ce soit. Pour ajouter des lignes sans rien perdre, utilise `"a"`. #### L'encodage : toujours `encoding="utf-8"` Un fichier ne contient que des octets (des nombres). L'**encodage** est la règle qui traduit ces octets en caractères. Le standard universel aujourd'hui est **UTF-8**, qui gère tous les caractères : accents, emojis, alphabets du monde entier. Si tu ne précises rien, Python utilise l'encodage par défaut **de ton système**, qui n'est pas forcément UTF-8 (sous Windows, c'est souvent `cp1252`). Résultat : un fichier écrit sur un ordinateur peut afficher `é` au lieu de `é` sur un autre. Voici ce qui se passe quand on relit un fichier UTF-8 avec le mauvais encodage : ```python from pathlib import Path with open(Path("bonjour.txt"), "r", encoding="latin-1") as fichier: print(fichier.read()) ``` ```text Bonjour le monde ! Deuxième ligne, avec des accents : é è ç ù. ``` **Règle d'or de ce chapitre :** écris **toujours** `encoding="utf-8"` quand tu ouvres un fichier texte. Pour lire comme pour écrire. --- ### 4. Lire un fichier texte Préparons un fichier de travail : ```python from pathlib import Path with open(Path("fruits.txt"), "w", encoding="utf-8") as fichier: fichier.write("pomme\n") fichier.write("banane\n") fichier.write("cerise\n") ``` Il existe plusieurs façons de lire, selon le besoin. #### Tout lire d'un coup : `read()` ```python from pathlib import Path with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier: contenu = fichier.read() print(repr(contenu)) print(contenu) ``` ```text 'pomme\nbanane\ncerise\n' pomme banane cerise ``` `read()` renvoie **une seule chaîne** contenant tout le fichier, retours à la ligne `\n` compris. `repr()` permet de voir ces caractères invisibles (d'où la ligne vide à la fin du second affichage : c'est le dernier `\n`, plus celui ajouté par `print`). Comme on lit, on pourrait omettre `"r"` (c'est le mode par défaut), mais l'écrire rend le code plus explicite. #### Ligne par ligne : boucler sur le fichier Un objet fichier se parcourt avec `for`, **ligne par ligne**. C'est la méthode à privilégier, surtout pour les gros fichiers, car Python ne charge qu'une ligne à la fois en mémoire. ```python from pathlib import Path with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier: for numero, ligne in enumerate(fichier, start=1): print(f"{numero}: {ligne}") ``` ```text 1: pomme 2: banane 3: cerise ``` Pourquoi ces lignes vides ? Chaque `ligne` se termine par `\n`, et `print` en ajoute un autre. On retire donc presque toujours le retour à la ligne avec `strip()` (ou `rstrip("\n")` pour ne retirer que lui) : ```python from pathlib import Path with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier: for numero, ligne in enumerate(fichier, start=1): ligne = ligne.strip() print(f"{numero}: {ligne}") ``` ```text 1: pomme 2: banane 3: cerise ``` #### Obtenir une liste de lignes Pour récupérer une **liste** de lignes propres, la façon la plus simple combine `read()` et `splitlines()` (vue au chapitre 06), qui découpe sur les retours à la ligne et les supprime : ```python from pathlib import Path with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier: fruits = fichier.read().splitlines() print(fruits) # → ['pomme', 'banane', 'cerise'] ``` Il existe aussi `fichier.readlines()`, qui renvoie une liste de lignes **avec** leur `\n` final, et `fichier.readline()` (sans s), qui lit une seule ligne à la fois. #### Les raccourcis de `pathlib` : `read_text` et `write_text` Pour les cas simples (lire ou écrire tout un fichier d'un coup), un objet `Path` propose deux méthodes qui ouvrent et ferment le fichier pour toi : ```python from pathlib import Path chemin = Path("message.txt") chemin.write_text("Salut !\nÀ bientôt.\n", encoding="utf-8") # mode "w" : écrase ! texte = chemin.read_text(encoding="utf-8") print(texte.splitlines()) # → ['Salut !', 'À bientôt.'] ``` C'est très pratique, mais `write_text` remplace tout le contenu, comme le mode `"w"`. Pour ajouter à la fin ou traiter un gros fichier ligne par ligne, reviens à `with open(...)`. #### Que se passe-t-il si le fichier n'existe pas ? ```python from pathlib import Path with open(Path("inexistant.txt"), "r", encoding="utf-8") as fichier: print(fichier.read()) ``` ```text Traceback (most recent call last): File "", line 3, in FileNotFoundError: [Errno 2] No such file or directory: 'inexistant.txt' ``` Pour l'instant, la solution est de **vérifier avant** avec `exists()` : ```python from pathlib import Path chemin = Path("inexistant.txt") if chemin.exists(): print(chemin.read_text(encoding="utf-8")) else: print(f"Le fichier {chemin} n'existe pas encore.") # → Le fichier inexistant.txt n'existe pas encore. ``` > Au [chapitre 08](08-erreurs-et-debogage.md), tu apprendras à **intercepter** ce genre d'erreur avec `try/except`, ce qui est souvent plus robuste. --- ### 5. Écrire dans un fichier #### `write()` : écrire une chaîne `fichier.write(chaine)` écrit la chaîne **telle quelle** : il n'ajoute **pas** de retour à la ligne, contrairement à `print`. Il faut mettre les `\n` soi-même. ```python from pathlib import Path with open(Path("sans_retour.txt"), "w", encoding="utf-8") as fichier: fichier.write("un") fichier.write("deux") fichier.write("trois") print(Path("sans_retour.txt").read_text(encoding="utf-8")) # → undeuxtrois ``` `write()` n'accepte que des chaînes : pour écrire un nombre, convertis-le avec `str()` ou une f-string. ```python from pathlib import Path notes = [12, 15.5, 9] with open(Path("notes.txt"), "w", encoding="utf-8") as fichier: for note in notes: fichier.write(f"{note}\n") print(Path("notes.txt").read_text(encoding="utf-8").splitlines()) # → ['12', '15.5', '9'] ``` Pour écrire une liste de lignes d'un coup, utilise `join` : ```python from pathlib import Path villes = ["Paris", "Lyon", "Marseille"] with open(Path("villes.txt"), "w", encoding="utf-8") as fichier: fichier.write("\n".join(villes) + "\n") print(Path("villes.txt").read_text(encoding="utf-8").splitlines()) # → ['Paris', 'Lyon', 'Marseille'] ``` #### `print(..., file=...)` : écrire comme on affiche Tu peux aussi utiliser `print` en lui indiquant un fichier : il ajoute alors automatiquement les espaces et le retour à la ligne, comme à l'écran. ```python from pathlib import Path with open(Path("rapport.txt"), "w", encoding="utf-8") as fichier: print("Rapport du jour", file=fichier) print("Total :", 42, "ventes", file=fichier) print(Path("rapport.txt").read_text(encoding="utf-8"), end="") ``` ```text Rapport du jour Total : 42 ventes ``` #### Le mode `"a"` : ajouter à la fin Le mode ajout est idéal pour un **journal** (*log*) : chaque exécution ajoute des lignes sans effacer les précédentes. ```python from pathlib import Path journal = Path("journal.txt") with open(journal, "w", encoding="utf-8") as fichier: # on repart d'un fichier vide fichier.write("Démarrage\n") with open(journal, "a", encoding="utf-8") as fichier: # ajout fichier.write("Connexion d'Alice\n") with open(journal, "a", encoding="utf-8") as fichier: # nouvel ajout fichier.write("Connexion de Bob\n") print(journal.read_text(encoding="utf-8"), end="") ``` ```text Démarrage Connexion d'Alice Connexion de Bob ``` Si on avait utilisé `"w"` à chaque fois, il ne resterait que la dernière ligne. #### Exemple complet : traiter un fichier et en écrire un autre Un schéma très courant : lire un fichier, transformer chaque ligne, écrire le résultat dans un autre fichier. Ici, on nettoie une liste d'inscrits (espaces superflus, lignes vides, majuscules incohérentes, doublons). ```python from pathlib import Path brut = Path("inscrits_brut.txt") brut.write_text(" alice MARTIN\n\nBob durand \nalice martin\n \nchloé petit\n", encoding="utf-8") propres = [] with open(brut, "r", encoding="utf-8") as entree: for ligne in entree: nom = " ".join(ligne.split()).title() # espaces normalisés + majuscules if nom != "" and nom not in propres: # ignore lignes vides et doublons propres.append(nom) with open(Path("inscrits.txt"), "w", encoding="utf-8") as sortie: for nom in sorted(propres): sortie.write(nom + "\n") print(Path("inscrits.txt").read_text(encoding="utf-8"), end="") ``` ```text Alice Martin Bob Durand Chloé Petit ``` --- ### 6. Les fichiers CSV #### Le format CSV **CSV** signifie *Comma-Separated Values*, « valeurs séparées par des virgules ». C'est le format le plus simple pour stocker un **tableau** : une ligne du fichier par ligne du tableau, et un séparateur entre les colonnes. La première ligne contient généralement les **en-têtes** (noms des colonnes). ```text nom,age,ville Alice,30,Lyon Bob,25,Paris ``` En France, les tableurs utilisent souvent le **point-virgule** `;` comme séparateur, car la virgule sert de séparateur décimal (`12,5`). Tous les tableurs (Excel, LibreOffice Calc, Google Sheets) savent ouvrir et exporter du CSV. **Pourquoi ne pas simplement utiliser `split(",")` ?** Parce qu'une valeur peut elle-même contenir une virgule ou un retour à la ligne. Le format CSV prévoit alors de l'entourer de guillemets : `"Dupont, Jean",42`. Un `split` naïf couperait au mauvais endroit. Le module `csv` de la bibliothèque standard gère tous ces cas pour toi. #### Écrire un CSV avec `csv.writer` ```python import csv from pathlib import Path eleves = [ ["nom", "note", "classe"], ["Alice", 15.5, "3A"], ["Dupont, Jean", 12, "3B"], ["Chloé", 18, "3A"], ] with open(Path("eleves.csv"), "w", encoding="utf-8", newline="") as fichier: ecrivain = csv.writer(fichier) ecrivain.writerows(eleves) # écrit toutes les lignes d'un coup print(Path("eleves.csv").read_text(encoding="utf-8"), end="") ``` ```text nom,note,classe Alice,15.5,3A "Dupont, Jean",12,3B Chloé,18,3A ``` Points importants : - **`newline=""`** est obligatoire pour les fichiers CSV : le module `csv` gère lui-même les fins de ligne. Sans cela, sous Windows, tu obtiendrais une ligne vide entre chaque ligne. - `writerow(liste)` écrit **une** ligne, `writerows(liste_de_listes)` en écrit **plusieurs**. - Les nombres sont convertis automatiquement en texte. - La valeur contenant une virgule a été entourée de guillemets automatiquement. #### Lire un CSV avec `csv.reader` ```python import csv from pathlib import Path with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier: lecteur = csv.reader(fichier) for ligne in lecteur: print(ligne) ``` ```text ['nom', 'note', 'classe'] ['Alice', '15.5', '3A'] ['Dupont, Jean', '12', '3B'] ['Chloé', '18', '3A'] ``` Chaque ligne devient une **liste de chaînes**. Remarque deux choses : 1. Les guillemets autour de `Dupont, Jean` ont été correctement interprétés. 2. **Tout est du texte** : `'15.5'` et `'12'` sont des chaînes ! Pour calculer, il faut convertir avec `float()` ou `int()`. Pour sauter la ligne d'en-têtes, on peut utiliser `next(lecteur)`, qui lit la ligne suivante : ```python import csv from pathlib import Path with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier: lecteur = csv.reader(fichier) en_tetes = next(lecteur) # consomme la première ligne notes = [float(ligne[1]) for ligne in lecteur] print(en_tetes) # → ['nom', 'note', 'classe'] print(notes) # → [15.5, 12.0, 18.0] print(f"Moyenne : {sum(notes) / len(notes):.2f}") # → Moyenne : 15.17 ``` #### Changer de séparateur : `delimiter` Pour un CSV « à la française » avec des points-virgules, on précise `delimiter=";"`, à la lecture comme à l'écriture : ```python import csv from pathlib import Path Path("depenses.csv").write_text( "date;libelle;montant\n2024-03-01;Courses;54,20\n2024-03-03;Cinéma;11,50\n", encoding="utf-8", ) total = 0 with open(Path("depenses.csv"), "r", encoding="utf-8", newline="") as fichier: lecteur = csv.reader(fichier, delimiter=";") next(lecteur) for date, libelle, montant in lecteur: valeur = float(montant.replace(",", ".")) # 54,20 → 54.20 total += valeur print(f"{date} {libelle:<10} {valeur:>7.2f}") print(f"Total : {total:.2f} €") ``` ```text 2024-03-01 Courses 54.20 2024-03-03 Cinéma 11.50 Total : 65.70 € ``` Note le déballage `for date, libelle, montant in lecteur` et la conversion de la virgule décimale en point, indispensable avant `float()`. #### Lire avec `csv.DictReader` : accéder aux colonnes par leur nom Accéder aux colonnes par indice (`ligne[1]`) n'est pas très lisible, et casse dès qu'on ajoute une colonne. `csv.DictReader` utilise la ligne d'en-têtes pour transformer chaque ligne en **dictionnaire** : ```python import csv from pathlib import Path with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier: lecteur = csv.DictReader(fichier) for eleve in lecteur: print(eleve) ``` ```text {'nom': 'Alice', 'note': '15.5', 'classe': '3A'} {'nom': 'Dupont, Jean', 'note': '12', 'classe': '3B'} {'nom': 'Chloé', 'note': '18', 'classe': '3A'} ``` C'est beaucoup plus parlant : `eleve["note"]` au lieu de `eleve[1]`. La ligne d'en-têtes est consommée automatiquement. Et on retrouve la liste de dictionnaires du chapitre 04 : ```python import csv from pathlib import Path with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier: eleves = list(csv.DictReader(fichier)) classe_3a = [e["nom"] for e in eleves if e["classe"] == "3A"] meilleur = max(eleves, key=lambda e: float(e["note"])) print(classe_3a) # → ['Alice', 'Chloé'] print(meilleur["nom"]) # → Chloé ``` #### Écrire avec `csv.DictWriter` Symétriquement, `csv.DictWriter` écrit une liste de dictionnaires. On lui indique la liste des colonnes avec `fieldnames`, et on écrit la ligne d'en-têtes avec `writeheader()` : ```python import csv from pathlib import Path produits = [ {"nom": "Stylo", "prix": 1.5, "stock": 120}, {"nom": "Cahier", "prix": 3.2, "stock": 45}, {"nom": "Gomme", "prix": 0.8, "stock": 0}, ] colonnes = ["nom", "prix", "stock"] with open(Path("produits.csv"), "w", encoding="utf-8", newline="") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=colonnes, delimiter=";") ecrivain.writeheader() ecrivain.writerows(produits) print(Path("produits.csv").read_text(encoding="utf-8"), end="") ``` ```text nom;prix;stock Stylo;1.5;120 Cahier;3.2;45 Gomme;0.8;0 ``` - L'ordre des colonnes dans le fichier est celui de `fieldnames`. - Si un dictionnaire contient une clé absente de `fieldnames`, `DictWriter` provoque une erreur `ValueError` (on peut ajouter `extrasaction="ignore"` pour l'ignorer). - Si une clé manque dans un dictionnaire, la case reste vide. #### Ajouter des lignes à un CSV existant On combine le mode `"a"` et `DictWriter`, **sans** réécrire les en-têtes : ```python import csv from pathlib import Path nouveau = {"nom": "Règle", "prix": 2.1, "stock": 30} with open(Path("produits.csv"), "a", encoding="utf-8", newline="") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=["nom", "prix", "stock"], delimiter=";") ecrivain.writerow(nouveau) print(Path("produits.csv").read_text(encoding="utf-8"), end="") ``` ```text nom;prix;stock Stylo;1.5;120 Cahier;3.2;45 Gomme;0.8;0 Règle;2.1;30 ``` > **Et Excel ?** Excel a parfois du mal à détecter l'UTF-8 et affiche alors des caractères bizarres. Si un fichier doit être ouvert par Excel, utilise `encoding="utf-8-sig"` à l'écriture : cela ajoute une petite signature invisible au début du fichier que Excel reconnaît. Python relit sans problème ce fichier avec `encoding="utf-8-sig"`. --- ### 7. Les fichiers JSON #### Le format JSON **JSON** (*JavaScript Object Notation*) est un format texte pour représenter des **données structurées** : des listes et des dictionnaires imbriqués. C'est la langue commune des applications web et de très nombreux fichiers de configuration. Bonne nouvelle : il ressemble énormément à la syntaxe Python ! ```json { "nom": "Alice", "age": 30, "abonne": true, "telephone": null, "loisirs": ["lecture", "escalade"], "adresse": {"ville": "Lyon", "code_postal": "69001"} } ``` Correspondance entre JSON et Python : | JSON | Python | |---|---| | objet `{...}` | `dict` | | tableau `[...]` | `list` | | chaîne `"..."` (guillemets **doubles** obligatoires) | `str` | | nombre `42`, `3.14` | `int`, `float` | | `true` / `false` | `True` / `False` | | `null` | `None` | Contrairement au CSV, qui ne stocke que des tableaux de textes, le JSON conserve la **structure** et les **types** (nombres, booléens, listes imbriquées…). C'est le format idéal pour sauvegarder les données d'un programme. #### Sauvegarder avec `json.dump` ```python import json from pathlib import Path contact = { "nom": "Chloé", "age": 28, "abonne": True, "telephone": None, "loisirs": ["piano", "randonnée"], } with open(Path("contact.json"), "w", encoding="utf-8") as fichier: json.dump(contact, fichier, ensure_ascii=False, indent=4) print(Path("contact.json").read_text(encoding="utf-8")) ``` ```text { "nom": "Chloé", "age": 28, "abonne": true, "telephone": null, "loisirs": [ "piano", "randonnée" ] } ``` `json.dump(donnees, fichier, ...)` convertit les données Python en texte JSON et les écrit dans le fichier. Deux options à utiliser **systématiquement** : - **`indent=4`** : met en forme le JSON sur plusieurs lignes avec une indentation, pour qu'il soit lisible par un humain. Sans cela, tout est écrit sur une seule ligne. - **`ensure_ascii=False`** : écrit les caractères accentués tels quels. Sans cette option, Python remplace chaque caractère non-ASCII par un code, comme `\u00e9` pour `é` : le fichier reste valide, mais devient illisible pour un humain. Compare : ```python import json print(json.dumps({"ville": "Besançon"})) print(json.dumps({"ville": "Besançon"}, ensure_ascii=False)) ``` ```text {"ville": "Besan\u00e7on"} {"ville": "Besançon"} ``` (`json.dumps`, avec un **s** pour *string*, renvoie le JSON sous forme de chaîne au lieu de l'écrire dans un fichier.) #### Charger avec `json.load` ```python import json from pathlib import Path with open(Path("contact.json"), "r", encoding="utf-8") as fichier: contact = json.load(fichier) print(type(contact)) # → print(contact["nom"]) # → Chloé print(contact["loisirs"]) # → ['piano', 'randonnée'] print(contact["abonne"]) # → True print(contact["telephone"]) # → None ``` `json.load(fichier)` lit le fichier et reconstruit les objets Python : on retrouve un vrai dictionnaire, avec de vrais booléens, de vraies listes, de vrais nombres. Aucune conversion manuelle n'est nécessaire, contrairement au CSV. Il existe aussi `json.loads(chaine)` (avec un **s**), qui analyse une chaîne JSON : ```python import json donnees = json.loads('{"scores": [12, 18, 15], "joueur": "Léo"}') print(donnees["joueur"], max(donnees["scores"])) # → Léo 18 ``` Pour retenir : **`dump`** = de Python **vers** le JSON (on « vide » ses données dans le fichier), **`load`** = du JSON **vers** Python (on « charge »). Avec un `s` final, on travaille sur une chaîne au lieu d'un fichier. #### Sauvegarder toute une structure Le JSON gère les structures imbriquées : une liste de dictionnaires, un dictionnaire de listes… Voici le schéma typique d'un programme qui conserve ses données entre deux exécutions : ```python import json from pathlib import Path FICHIER_SCORES = Path("scores.json") def charger_scores(chemin): """Renvoie le dictionnaire des scores, ou un dictionnaire vide si le fichier n'existe pas.""" if not chemin.exists(): return {} with open(chemin, "r", encoding="utf-8") as fichier: return json.load(fichier) def sauvegarder_scores(scores, chemin): """Écrit le dictionnaire des scores dans le fichier JSON.""" with open(chemin, "w", encoding="utf-8") as fichier: json.dump(scores, fichier, ensure_ascii=False, indent=2) scores = charger_scores(FICHIER_SCORES) print(f"Scores chargés : {scores}") scores["Inès"] = scores.get("Inès", 0) + 150 scores["Noé"] = scores.get("Noé", 0) + 90 sauvegarder_scores(scores, FICHIER_SCORES) print(f"Scores sauvegardés : {charger_scores(FICHIER_SCORES)}") ``` Première exécution : ```text Scores chargés : {} Scores sauvegardés : {'Inès': 150, 'Noé': 90} ``` Si tu relances le programme, les scores sont retrouvés et augmentés : ```text Scores chargés : {'Inès': 150, 'Noé': 90} Scores sauvegardés : {'Inès': 300, 'Noé': 180} ``` Ton programme a désormais une **mémoire** ! Retiens bien ce couple de fonctions `charger` / `sauvegarder` : tu le réutiliseras dans presque tous tes projets. #### Ce que JSON ne sait pas stocker JSON ne connaît que les types du tableau ci-dessus. Quelques conséquences : - Les **tuples** deviennent des listes : `(1, 2)` est relu comme `[1, 2]`. - Les **ensembles** (`set`) ne sont pas acceptés : convertis-les en liste avant (`list(mon_ensemble)`). - Les **clés** de dictionnaire deviennent toujours des chaînes (voir les pièges). - Les dates et les objets que tu créeras plus tard (chapitres 09 et 10) doivent être convertis à la main (en chaîne ou en dictionnaire). ```python import json donnees = {"point": (3, 4), "tags": sorted({"b", "a"})} texte = json.dumps(donnees) print(texte) # → {"point": [3, 4], "tags": ["a", "b"]} print(json.loads(texte)) # → {'point': [3, 4], 'tags': ['a', 'b']} ``` Essayer d'enregistrer un ensemble directement provoque une erreur : ```python import json json.dumps({"tags": {"python", "json"}}) ``` ```text Traceback (most recent call last): File "", line 3, in TypeError: Object of type set is not JSON serializable ``` --- ### 8. CSV ou JSON : lequel choisir ? | Critère | CSV | JSON | |---|---|---| | Structure | un tableau plat (lignes × colonnes) | arbitraire : listes et dictionnaires imbriqués | | Types | tout est texte, il faut convertir | nombres, booléens, `null` conservés | | Lisible par un tableur | oui (Excel, LibreOffice) | non | | Usage typique | exports, données tabulaires, échanges avec des non-programmeurs | sauvegarde de l'état d'un programme, configuration, web (API) | En pratique : si tes données ressemblent à un tableau et doivent être ouvertes dans un tableur, choisis le **CSV**. Si elles sont structurées (un contact avec plusieurs téléphones, des paramètres imbriqués), ou s'il s'agit simplement de sauvegarder les variables de ton programme, choisis le **JSON**. On peut d'ailleurs passer facilement de l'un à l'autre : `csv.DictReader` produit une liste de dictionnaires, que `json.dump` sait écrire, et inversement (c'est l'exercice 7). --- ## Pièges fréquents ### Piège 1 — Oublier `encoding="utf-8"` ```python from pathlib import Path Path("accents.txt").write_text("Crème brûlée\n", encoding="utf-8") with open(Path("accents.txt"), "r", encoding="ascii") as fichier: # mauvais encodage print(fichier.read()) ``` ```text Traceback (most recent call last): File "", line 5, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 2: ordinal not in range(128) ``` Selon le système et l'encodage utilisé, on obtient soit une `UnicodeDecodeError` comme ici, soit des caractères bizarres (`Crème brûlée`). Si tu n'indiques pas d'encodage, Python utilise celui de ton système, et ton programme peut fonctionner chez toi mais pas chez quelqu'un d'autre. **Correction :** toujours `encoding="utf-8"`, en lecture **et** en écriture. ### Piège 2 — Écraser un fichier avec le mode `"w"` ```python from pathlib import Path with open(Path("journal.txt"), "w", encoding="utf-8") as fichier: fichier.write("Nouvelle entrée\n") print(Path("journal.txt").read_text(encoding="utf-8"), end="") # → Nouvelle entrée ``` Toutes les entrées précédentes du journal ont disparu, sans aucun avertissement. **Correction :** utiliser le mode `"a"` pour **ajouter**. En cas de doute, vérifie avec `exists()` avant d'écrire, ou utilise `"x"` qui refuse d'écraser un fichier existant. ### Piège 3 — Oublier les `\n` avec `write` ```python from pathlib import Path with open(Path("courses.txt"), "w", encoding="utf-8") as fichier: for article in ["pain", "lait", "oeufs"]: fichier.write(article) print(Path("courses.txt").read_text(encoding="utf-8")) # → painlaitoeufs ``` **Correction :** `fichier.write(article + "\n")`, ou `print(article, file=fichier)`. ### Piège 4 — Oublier de retirer les `\n` à la lecture ```python from pathlib import Path Path("mdp.txt").write_text("secret123\n", encoding="utf-8") with open(Path("mdp.txt"), "r", encoding="utf-8") as fichier: mot_de_passe = fichier.readline() print(mot_de_passe == "secret123") # → False print(repr(mot_de_passe)) # → 'secret123\n' ``` Le `\n` fait partie de la ligne lue ! **Correction :** `fichier.readline().strip()`. Quand une comparaison échoue mystérieusement, affiche la valeur avec `repr()` pour voir les caractères invisibles. ### Piège 5 — Calculer avec des valeurs CSV sans les convertir ```python import csv from pathlib import Path Path("ventes.csv").write_text("produit,quantite\nA,3\nB,5\n", encoding="utf-8") with open(Path("ventes.csv"), "r", encoding="utf-8", newline="") as fichier: total = 0 for ligne in csv.DictReader(fichier): total += ligne["quantite"] ``` ```text Traceback (most recent call last): File "", line 7, in TypeError: unsupported operand type(s) for +=: 'int' and 'str' ``` Le module `csv` renvoie **toujours** des chaînes. **Correction :** `total += int(ligne["quantite"])`. ### Piège 6 — Oublier `newline=""` avec le module `csv` Sous Linux et macOS, on ne voit pas la différence. Mais sous Windows, sans `newline=""`, chaque ligne écrite par `csv.writer` est suivie d'une **ligne vide** quand on ouvre le fichier dans un tableur. **Correction :** ouvre **toujours** un fichier CSV avec `open(chemin, mode, encoding="utf-8", newline="")`, en lecture comme en écriture. ### Piège 7 — Lire deux fois le même fichier ouvert ```python from pathlib import Path with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier: premiere_lecture = fichier.read() deuxieme_lecture = fichier.read() print(len(premiere_lecture)) # → 20 print(repr(deuxieme_lecture)) # → '' ``` Un fichier ouvert a un **curseur**, comme une tête de lecture : après le premier `read()`, il est à la fin et il n'y a plus rien à lire. Même chose si tu boucles deux fois sur un fichier ou un `csv.reader`. **Correction :** stocke le contenu dans une variable (une liste, par exemple) et réutilise-la, ou rouvre le fichier. ### Piège 8 — Les clés JSON deviennent des chaînes ```python import json niveaux = {1: "facile", 2: "moyen"} relu = json.loads(json.dumps(niveaux)) print(relu) # → {'1': 'facile', '2': 'moyen'} print(relu.get(1)) # → None ``` En JSON, les clés d'un objet sont **toujours** des chaînes. Après un aller-retour, `1` est devenu `"1"`, et `relu[1]` provoquerait une `KeyError`. **Correction :** utiliser des chaînes comme clés dès le départ, ou reconvertir après chargement : `{int(cle): valeur for cle, valeur in relu.items()}`. --- ## Exercices > Pour chaque exercice, le corrigé commence par créer les fichiers de données nécessaires : tu peux donc les tester dans un dossier vide. ### Exercice 1 — Écrire puis relire ⭐ 1. Écris dans `table7.txt` la table de multiplication de 7, de 1 à 10, une opération par ligne (`7 x 1 = 7`). 2. Relis le fichier et affiche seulement les lignes dont le résultat est pair, précédées de leur numéro de ligne. Sortie attendue : ```text Ligne 2 : 7 x 2 = 14 Ligne 4 : 7 x 4 = 28 Ligne 6 : 7 x 6 = 42 Ligne 8 : 7 x 8 = 56 Ligne 10 : 7 x 10 = 70 ``` ### Exercice 2 — Compteur de fichier ⭐ Écris une fonction `statistiques_fichier(chemin)` qui renvoie un dictionnaire avec le nombre de lignes, de mots et de caractères d'un fichier texte (comme la commande `wc` de Linux), en comptant les caractères **sans** les retours à la ligne. Teste-la sur un fichier `poeme.txt` contenant : ```text Demain, dès l'aube, à l'heure où blanchit la campagne, Je partirai. Vois-tu, je sais que tu m'attends. J'irai par la forêt, j'irai par la montagne. ``` Sortie attendue : ```text {'lignes': 3, 'mots': 25, 'caracteres': 145} ``` ### Exercice 3 — Nettoyer un fichier de configuration ⭐⭐ Un fichier `config.txt` contient des réglages `cle = valeur`, des lignes vides et des commentaires commençant par `#` : ```text # Configuration du jeu largeur = 800 hauteur=600 # Son volume = 75 pseudo = Joueur 1 ``` Écris une fonction `lire_config(chemin)` qui renvoie un dictionnaire des réglages, en ignorant les commentaires et les lignes vides, et en nettoyant les espaces autour des clés et des valeurs. Indice : `ligne.split("=", 1)` coupe uniquement au premier `=`. ```python print(lire_config(Path("config.txt"))) ``` ```text {'largeur': '800', 'hauteur': '600', 'volume': '75', 'pseudo': 'Joueur 1'} ``` ### Exercice 4 — Bulletin à partir d'un CSV ⭐⭐ Le fichier `notes.csv` (séparateur `;`) contient une ligne par élève : ```text nom;maths;francais;anglais Alice;15;12;17 Bob;9;11;8 Chloé;18;16;14 David;10;9;12 ``` Avec `csv.DictReader` et `csv.DictWriter`, crée un fichier `bulletin.csv` (séparateur `;`) avec les colonnes `nom`, `moyenne` (arrondie à 2 décimales) et `resultat` (`admis` si moyenne ≥ 10, sinon `ajourné`), trié par moyenne décroissante. Affiche ensuite son contenu. Sortie attendue : ```text nom;moyenne;resultat Chloé;16.0;admis Alice;14.67;admis David;10.33;admis Bob;9.33;ajourné ``` ### Exercice 5 — Préférences en JSON ⭐⭐ Écris un programme qui gère un fichier `preferences.json` : - une constante `PREFERENCES_PAR_DEFAUT = {"theme": "clair", "langue": "fr", "taille_police": 12}` ; - une fonction `charger_preferences(chemin)` qui renvoie les préférences du fichier s'il existe, **complétées** par les valeurs par défaut pour les clés manquantes ; si le fichier n'existe pas, elle renvoie une copie des valeurs par défaut ; - une fonction `sauvegarder_preferences(preferences, chemin)` (JSON lisible, accents conservés). Teste : supprime le fichier s'il existe (`chemin.unlink(missing_ok=True)`), charge, passe le thème à `"sombre"`, sauvegarde ; écris ensuite à la main un fichier ne contenant que `{"langue": "en"}` et recharge. ```text Au départ : {'theme': 'clair', 'langue': 'fr', 'taille_police': 12} Après sauvegarde : {'theme': 'sombre', 'langue': 'fr', 'taille_police': 12} Fichier partiel : {'theme': 'clair', 'langue': 'en', 'taille_police': 12} ``` ### Exercice 6 — Inventaire d'un dossier ⭐⭐ Crée un dossier `inventaire` contenant les fichiers `rapport.txt`, `photo.jpg`, `budget.csv`, `notes.txt` et `vacances.jpg` (contenu au choix, par exemple leur propre nom). Écris une fonction `inventaire_par_extension(dossier)` qui renvoie un dictionnaire `{extension: [noms de fichiers triés]}`, puis affiche-le par extension triée. Sortie attendue : ```text .csv : budget.csv .jpg : photo.jpg, vacances.jpg .txt : notes.txt, rapport.txt ``` ### Exercice 7 — Convertisseur CSV ↔ JSON ⭐⭐⭐ Écris deux fonctions : - `csv_vers_json(chemin_csv, chemin_json)` : lit un CSV (séparateur `,`) avec `DictReader` et écrit la liste de dictionnaires en JSON. Les valeurs qui ne contiennent que des chiffres doivent être converties en `int` ; - `json_vers_csv(chemin_json, chemin_csv)` : fait l'inverse avec `DictWriter`, en prenant comme colonnes les clés du premier élément. Teste avec un fichier `villes.csv` : ```text ville,departement,habitants Lyon,Rhône,522250 Lille,Nord,236710 Nantes,Loire-Atlantique,320732 ``` Puis reconvertis le JSON en `villes_copie.csv` et vérifie que le contenu est identique à l'original. Sortie attendue : ```text [ { "ville": "Lyon", "departement": "Rhône", "habitants": 522250 }, { "ville": "Lille", "departement": "Nord", "habitants": 236710 }, { "ville": "Nantes", "departement": "Loire-Atlantique", "habitants": 320732 } ] Total habitants : 1079692 Copie identique : True ``` ### Exercice 8 — Rapport de ventes ⭐⭐⭐ Le fichier `ventes.csv` liste des ventes : ```text date,produit,categorie,quantite,prix_unitaire 2024-05-02,Clavier,Informatique,2,49.90 2024-05-02,Stylo,Papeterie,10,1.20 2024-05-03,Souris,Informatique,3,19.90 2024-05-03,Cahier,Papeterie,5,3.50 2024-05-04,Clavier,Informatique,1,49.90 2024-05-04,Casque,Audio,1,89.00 ``` Écris un programme qui : 1. calcule le chiffre d'affaires (quantité × prix) **par catégorie** et le nombre d'articles vendus par catégorie ; 2. écrit `rapport_ventes.csv` avec les colonnes `categorie`, `articles`, `chiffre_affaires` (2 décimales), trié par chiffre d'affaires décroissant ; 3. écrit `rapport_ventes.json` contenant `{"total": ..., "meilleur_produit": ..., "categories": {...}}`, où `meilleur_produit` est le produit au plus gros chiffre d'affaires cumulé ; 4. affiche le contenu des deux fichiers. Sortie attendue : ```text categorie,articles,chiffre_affaires Informatique,6,209.40 Audio,1,89.00 Papeterie,15,29.50 { "total": 327.9, "meilleur_produit": "Clavier", "categories": { "Informatique": 209.4, "Papeterie": 29.5, "Audio": 89.0 } } ``` --- ## Mini-projet — Le carnet de contacts sauvegardé en JSON ### Cahier des charges Écris un carnet de contacts en ligne de commande, dont les données **survivent** à la fermeture du programme grâce à un fichier `contacts.json`. Chaque contact est un dictionnaire : ```json {"nom": "Martin", "prenom": "Alice", "telephone": "06 12 34 56 78", "email": "alice@exemple.fr"} ``` Le carnet entier est une **liste** de ces dictionnaires. Le programme affiche un menu en boucle : ```text === CARNET DE CONTACTS (2 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter ``` - **Au démarrage**, les contacts sont chargés depuis `contacts.json` (liste vide si le fichier n'existe pas). - **Lister** : affiche les contacts triés par nom puis prénom, numérotés, dans un tableau aligné. - **Ajouter** : demande nom, prénom, téléphone et email ; nom et prénom sont obligatoires (on redemande s'ils sont vides). Le nom est mis en majuscules, le prénom avec une majuscule initiale. **Sauvegarde immédiate.** - **Rechercher** : demande un texte et affiche les contacts dont le nom, le prénom ou l'email le contient (sans tenir compte de la casse). - **Supprimer** : affiche la liste numérotée, demande un numéro, demande confirmation (`o/n`), supprime et **sauvegarde**. - **Exporter** : écrit tous les contacts dans `contacts.csv` (séparateur `;`, encodage `utf-8-sig` pour Excel) avec `DictWriter`. - **Quitter** : message d'au revoir. ### Étapes suggérées 1. Écris `charger_contacts(chemin)` et `sauvegarder_contacts(contacts, chemin)` (sur le modèle des scores de la section 7) et teste-les seules. 2. Écris `afficher_contacts(contacts)` avec un tableau aligné par f-strings et `sorted(..., key=lambda c: (c["nom"], c["prenom"]))`. 3. Écris `demander_non_vide(message)` puis `ajouter_contact(contacts)`. 4. Écris `rechercher(contacts, texte)` qui **renvoie** la liste des résultats (sans `print`). 5. Écris `supprimer_contact(contacts)` en vérifiant que le numéro saisi est valide (`isdigit()` et dans les bornes). 6. Écris `exporter_csv(contacts, chemin)`. 7. Assemble le tout dans `main()` avec le menu. ### Pistes d'amélioration - **Modifier** un contact existant. - Refuser les **doublons** (même nom et même prénom). - Vérifier le format de l'email (présence d'un `@` et d'un `.` après) et du téléphone (10 chiffres). - Permettre **plusieurs numéros** par contact (une liste dans le dictionnaire : impossible en CSV simple, facile en JSON !). - **Importer** des contacts depuis un CSV. - Faire une **sauvegarde de sécurité** `contacts.bak.json` avant chaque écriture. - Gérer un fichier JSON corrompu avec `try/except` ([chapitre 08](08-erreurs-et-debogage.md)), puis transformer les contacts en objets ([chapitre 10](10-poo-classes-objets.md)). --- ## Corrigés
Corrigé de l'exercice 1 ```python from pathlib import Path chemin = Path("table7.txt") # 1. Écriture with open(chemin, "w", encoding="utf-8") as fichier: for i in range(1, 11): fichier.write(f"7 x {i} = {7 * i}\n") # 2. Relecture et filtrage with open(chemin, "r", encoding="utf-8") as fichier: for numero, ligne in enumerate(fichier, start=1): ligne = ligne.strip() resultat = int(ligne.split("=")[1]) if resultat % 2 == 0: print(f"Ligne {numero} : {ligne}") ``` Sortie : ```text Ligne 2 : 7 x 2 = 14 Ligne 4 : 7 x 4 = 28 Ligne 6 : 7 x 6 = 42 Ligne 8 : 7 x 8 = 56 Ligne 10 : 7 x 10 = 70 ``` À l'écriture, la f-string se termine par `\n` pour que chaque opération soit sur sa propre ligne. À la lecture, on boucle directement sur le fichier avec `enumerate` pour avoir le numéro de ligne, on retire le `\n` avec `strip()`, puis on récupère le résultat en coupant sur `=` : `"7 x 2 = 14".split("=")` donne `['7 x 2 ', ' 14']`, et `int()` accepte les espaces autour du nombre.
Corrigé de l'exercice 2 ```python from pathlib import Path def statistiques_fichier(chemin): """Renvoie le nombre de lignes, de mots et de caractères (hors retours à la ligne).""" nb_lignes = 0 nb_mots = 0 nb_caracteres = 0 with open(chemin, "r", encoding="utf-8") as fichier: for ligne in fichier: ligne = ligne.rstrip("\n") nb_lignes += 1 nb_mots += len(ligne.split()) nb_caracteres += len(ligne) return {"lignes": nb_lignes, "mots": nb_mots, "caracteres": nb_caracteres} poeme = Path("poeme.txt") poeme.write_text( "Demain, dès l'aube, à l'heure où blanchit la campagne,\n" "Je partirai. Vois-tu, je sais que tu m'attends.\n" "J'irai par la forêt, j'irai par la montagne.\n", encoding="utf-8", ) print(statistiques_fichier(poeme)) ``` Sortie : ```text {'lignes': 3, 'mots': 25, 'caracteres': 145} ``` On lit ligne par ligne (efficace même pour un très gros fichier) et on met à jour trois compteurs. `rstrip("\n")` ne retire que le retour à la ligne, pas les éventuels espaces en fin de ligne, qui sont de vrais caractères. `split()` sans argument découpe sur les espaces : « l'aube, » compte pour un seul mot. Les accents comptent pour un caractère chacun, grâce au décodage UTF-8.
Corrigé de l'exercice 3 ```python from pathlib import Path def lire_config(chemin): """Lit un fichier 'cle = valeur' et renvoie un dictionnaire des réglages.""" reglages = {} with open(chemin, "r", encoding="utf-8") as fichier: for ligne in fichier: ligne = ligne.strip() if ligne == "" or ligne.startswith("#"): continue # ligne vide ou commentaire cle, valeur = ligne.split("=", 1) reglages[cle.strip()] = valeur.strip() return reglages Path("config.txt").write_text( "# Configuration du jeu\n" "largeur = 800\n" "hauteur=600\n" "\n" "# Son\n" "volume = 75\n" "pseudo = Joueur 1\n", encoding="utf-8", ) print(lire_config(Path("config.txt"))) ``` Sortie : ```text {'largeur': '800', 'hauteur': '600', 'volume': '75', 'pseudo': 'Joueur 1'} ``` `continue` (chapitre 03) passe directement à la ligne suivante pour les lignes à ignorer. `split("=", 1)` coupe au **premier** `=` seulement : une valeur pourrait contenir un `=` (par exemple une formule). Les valeurs restent des chaînes ; c'est au programme qui les utilise de les convertir (`int(reglages["largeur"])`). Une ligne sans `=` provoquerait une erreur au déballage : on apprendra à la gérer proprement au chapitre 08.
Corrigé de l'exercice 4 ```python import csv from pathlib import Path Path("notes.csv").write_text( "nom;maths;francais;anglais\n" "Alice;15;12;17\n" "Bob;9;11;8\n" "Chloé;18;16;14\n" "David;10;9;12\n", encoding="utf-8", ) # Lecture et calcul bulletin = [] with open(Path("notes.csv"), "r", encoding="utf-8", newline="") as fichier: for ligne in csv.DictReader(fichier, delimiter=";"): notes = [int(ligne[matiere]) for matiere in ["maths", "francais", "anglais"]] moyenne = round(sum(notes) / len(notes), 2) bulletin.append({ "nom": ligne["nom"], "moyenne": moyenne, "resultat": "admis" if moyenne >= 10 else "ajourné", }) bulletin.sort(key=lambda eleve: eleve["moyenne"], reverse=True) # Écriture with open(Path("bulletin.csv"), "w", encoding="utf-8", newline="") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=["nom", "moyenne", "resultat"], delimiter=";") ecrivain.writeheader() ecrivain.writerows(bulletin) print(Path("bulletin.csv").read_text(encoding="utf-8"), end="") ``` Sortie : ```text nom;moyenne;resultat Chloé;16.0;admis Alice;14.67;admis David;10.33;admis Bob;9.33;ajourné ``` `DictReader` permet d'écrire `ligne["maths"]` plutôt que `ligne[1]`. Une compréhension convertit les trois notes en entiers. On construit une liste de dictionnaires (les lignes du futur fichier), qu'on trie avec une `lambda` avant de la donner à `DictWriter`. Le même `delimiter=";"` est utilisé en lecture et en écriture.
Corrigé de l'exercice 5 ```python import json from pathlib import Path PREFERENCES_PAR_DEFAUT = {"theme": "clair", "langue": "fr", "taille_police": 12} def charger_preferences(chemin): """Renvoie les préférences du fichier, complétées par les valeurs par défaut.""" preferences = PREFERENCES_PAR_DEFAUT.copy() if chemin.exists(): with open(chemin, "r", encoding="utf-8") as fichier: preferences.update(json.load(fichier)) return preferences def sauvegarder_preferences(preferences, chemin): """Enregistre les préférences au format JSON lisible.""" with open(chemin, "w", encoding="utf-8") as fichier: json.dump(preferences, fichier, ensure_ascii=False, indent=4) chemin = Path("preferences.json") chemin.unlink(missing_ok=True) # on part d'une situation propre preferences = charger_preferences(chemin) print(f"Au départ : {preferences}") preferences["theme"] = "sombre" sauvegarder_preferences(preferences, chemin) print(f"Après sauvegarde : {charger_preferences(chemin)}") chemin.write_text('{"langue": "en"}', encoding="utf-8") print(f"Fichier partiel : {charger_preferences(chemin)}") ``` Sortie : ```text Au départ : {'theme': 'clair', 'langue': 'fr', 'taille_police': 12} Après sauvegarde : {'theme': 'sombre', 'langue': 'fr', 'taille_police': 12} Fichier partiel : {'theme': 'clair', 'langue': 'en', 'taille_police': 12} ``` L'astuce est de partir d'une **copie** des valeurs par défaut, puis de la mettre à jour avec `update()`, une méthode des dictionnaires qui ajoute ou remplace les paires d'un autre dictionnaire. Sans `.copy()`, on modifierait la constante elle-même (piège de la référence, chapitre 04). `unlink()` supprime un fichier ; `missing_ok=True` évite l'erreur s'il n'existe pas.
Corrigé de l'exercice 6 ```python from pathlib import Path def inventaire_par_extension(dossier): """Renvoie {extension: [noms de fichiers triés]} pour les fichiers du dossier.""" inventaire = {} for chemin in dossier.iterdir(): if chemin.is_file(): extension = chemin.suffix if extension not in inventaire: inventaire[extension] = [] inventaire[extension].append(chemin.name) return {ext: sorted(noms) for ext, noms in inventaire.items()} dossier = Path("inventaire") dossier.mkdir(exist_ok=True) for nom in ["rapport.txt", "photo.jpg", "budget.csv", "notes.txt", "vacances.jpg"]: (dossier / nom).write_text(nom, encoding="utf-8") inventaire = inventaire_par_extension(dossier) for extension in sorted(inventaire): print(f"{extension} : {', '.join(inventaire[extension])}") ``` Sortie : ```text .csv : budget.csv .jpg : photo.jpg, vacances.jpg .txt : notes.txt, rapport.txt ``` `iterdir()` parcourt le contenu du dossier ; `is_file()` écarte les éventuels sous-dossiers. `suffix` donne l'extension et `name` le nom complet. Comme l'ordre de `iterdir()` n'est pas garanti, on trie les noms (compréhension de dictionnaire) et les extensions à l'affichage. C'est la base d'un script qui range automatiquement un dossier de téléchargements (cf. [chapitre 14](14-specialisations.md)).
Corrigé de l'exercice 7 ```python import csv import json from pathlib import Path def csv_vers_json(chemin_csv, chemin_json): """Convertit un CSV en JSON (liste de dictionnaires), les nombres entiers étant convertis.""" with open(chemin_csv, "r", encoding="utf-8", newline="") as fichier: lignes = list(csv.DictReader(fichier)) donnees = [] for ligne in lignes: donnees.append({cle: int(valeur) if valeur.isdigit() else valeur for cle, valeur in ligne.items()}) with open(chemin_json, "w", encoding="utf-8") as fichier: json.dump(donnees, fichier, ensure_ascii=False, indent=2) def json_vers_csv(chemin_json, chemin_csv): """Convertit un JSON (liste de dictionnaires) en CSV.""" with open(chemin_json, "r", encoding="utf-8") as fichier: donnees = json.load(fichier) with open(chemin_csv, "w", encoding="utf-8", newline="") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=list(donnees[0].keys()), lineterminator="\n") ecrivain.writeheader() ecrivain.writerows(donnees) Path("villes.csv").write_text( "ville,departement,habitants\n" "Lyon,Rhône,522250\n" "Lille,Nord,236710\n" "Nantes,Loire-Atlantique,320732\n", encoding="utf-8", ) csv_vers_json(Path("villes.csv"), Path("villes.json")) print(Path("villes.json").read_text(encoding="utf-8")) with open(Path("villes.json"), "r", encoding="utf-8") as fichier: villes = json.load(fichier) print(f"Total habitants : {sum(v['habitants'] for v in villes)}") json_vers_csv(Path("villes.json"), Path("villes_copie.csv")) original = Path("villes.csv").read_text(encoding="utf-8") copie = Path("villes_copie.csv").read_text(encoding="utf-8") print(f"Copie identique : {original == copie}") ``` Sortie : ```text [ { "ville": "Lyon", "departement": "Rhône", "habitants": 522250 }, { "ville": "Lille", "departement": "Nord", "habitants": 236710 }, { "ville": "Nantes", "departement": "Loire-Atlantique", "habitants": 320732 } ] Total habitants : 1079692 Copie identique : True ``` La compréhension de dictionnaire avec `int(valeur) if valeur.isdigit() else valeur` convertit seulement les valeurs numériques : dans le JSON, `522250` apparaît sans guillemets, c'est un vrai nombre, et la somme fonctionne directement après rechargement. Pour l'inverse, les colonnes sont les clés du premier dictionnaire. Un détail : par défaut, `csv.writer` termine les lignes par `\r\n` (la convention officielle du format CSV). Pour obtenir un fichier **strictement** identique à l'original, qui utilise `\n`, on a précisé `lineterminator="\n"`. Sans cette option, le contenu serait le même pour un tableur, mais la comparaison de chaînes renverrait `False`.
Corrigé de l'exercice 8 ```python import csv import json from pathlib import Path Path("ventes.csv").write_text( "date,produit,categorie,quantite,prix_unitaire\n" "2024-05-02,Clavier,Informatique,2,49.90\n" "2024-05-02,Stylo,Papeterie,10,1.20\n" "2024-05-03,Souris,Informatique,3,19.90\n" "2024-05-03,Cahier,Papeterie,5,3.50\n" "2024-05-04,Clavier,Informatique,1,49.90\n" "2024-05-04,Casque,Audio,1,89.00\n", encoding="utf-8", ) # 1. Agrégation ca_categories = {} articles_categories = {} ca_produits = {} with open(Path("ventes.csv"), "r", encoding="utf-8", newline="") as fichier: for vente in csv.DictReader(fichier): quantite = int(vente["quantite"]) montant = quantite * float(vente["prix_unitaire"]) categorie = vente["categorie"] produit = vente["produit"] ca_categories[categorie] = ca_categories.get(categorie, 0) + montant articles_categories[categorie] = articles_categories.get(categorie, 0) + quantite ca_produits[produit] = ca_produits.get(produit, 0) + montant # 2. Rapport CSV, trié par chiffre d'affaires décroissant categories_triees = sorted(ca_categories, key=lambda c: ca_categories[c], reverse=True) with open(Path("rapport_ventes.csv"), "w", encoding="utf-8", newline="") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=["categorie", "articles", "chiffre_affaires"], lineterminator="\n") ecrivain.writeheader() for categorie in categories_triees: ecrivain.writerow({ "categorie": categorie, "articles": articles_categories[categorie], "chiffre_affaires": f"{ca_categories[categorie]:.2f}", }) # 3. Rapport JSON rapport = { "total": round(sum(ca_categories.values()), 2), "meilleur_produit": max(ca_produits, key=lambda p: ca_produits[p]), "categories": {c: round(montant, 2) for c, montant in ca_categories.items()}, } with open(Path("rapport_ventes.json"), "w", encoding="utf-8") as fichier: json.dump(rapport, fichier, ensure_ascii=False, indent=2) # 4. Affichage print(Path("rapport_ventes.csv").read_text(encoding="utf-8"), end="") print(Path("rapport_ventes.json").read_text(encoding="utf-8")) ``` Sortie : ```text categorie,articles,chiffre_affaires Informatique,6,209.40 Audio,1,89.00 Papeterie,15,29.50 { "total": 327.9, "meilleur_produit": "Clavier", "categories": { "Informatique": 209.4, "Papeterie": 29.5, "Audio": 89.0 } } ``` On réutilise le schéma « compter avec un dictionnaire et `get` » (chapitre 04), cette fois pour additionner des montants. Trois dictionnaires d'agrégation suffisent. Dans le CSV, le chiffre d'affaires est formaté avec `:.2f` pour toujours afficher 2 décimales (`89.00`) ; dans le JSON, on garde de vrais nombres arrondis avec `round` (le JSON n'affiche pas les zéros inutiles). `round` est important : sans lui, les calculs en virgule flottante pourraient donner des valeurs comme `209.39999999999998`.
Corrigé du mini-projet ```python import csv import json from pathlib import Path FICHIER_CONTACTS = Path("contacts.json") FICHIER_EXPORT = Path("contacts.csv") CHAMPS = ["nom", "prenom", "telephone", "email"] # ---------- Persistance ---------- def charger_contacts(chemin): """Renvoie la liste des contacts du fichier JSON (liste vide s'il n'existe pas).""" if not chemin.exists(): return [] with open(chemin, "r", encoding="utf-8") as fichier: return json.load(fichier) def sauvegarder_contacts(contacts, chemin): """Enregistre la liste des contacts au format JSON lisible.""" with open(chemin, "w", encoding="utf-8") as fichier: json.dump(contacts, fichier, ensure_ascii=False, indent=2) def exporter_csv(contacts, chemin): """Exporte les contacts dans un CSV lisible par Excel.""" with open(chemin, "w", encoding="utf-8-sig", newline="") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=CHAMPS, delimiter=";") ecrivain.writeheader() ecrivain.writerows(trier(contacts)) # ---------- Traitements (sans input ni print) ---------- def trier(contacts): """Renvoie une copie de la liste triée par nom puis prénom.""" return sorted(contacts, key=lambda c: (c["nom"], c["prenom"])) def rechercher(contacts, texte): """Renvoie les contacts dont le nom, le prénom ou l'email contient texte.""" texte = texte.lower() return [c for c in trier(contacts) if texte in c["nom"].lower() or texte in c["prenom"].lower() or texte in c["email"].lower()] # ---------- Interface ---------- def afficher_contacts(contacts): """Affiche les contacts dans un tableau numéroté.""" if not contacts: print("Aucun contact.") return print(f"{'N°':<4}{'Nom':<12}{'Prénom':<10}{'Téléphone':<16}Email") for numero, contact in enumerate(contacts, start=1): print(f"{numero:<4}{contact['nom']:<12}{contact['prenom']:<10}" f"{contact['telephone']:<16}{contact['email']}") def demander_non_vide(message): """Redemande tant que la saisie est vide ; renvoie la saisie nettoyée.""" saisie = input(message).strip() while saisie == "": print("Ce champ est obligatoire.") saisie = input(message).strip() return saisie def ajouter_contact(contacts): """Demande les informations d'un contact, l'ajoute et sauvegarde.""" contact = { "nom": demander_non_vide("Nom : ").upper(), "prenom": demander_non_vide("Prénom : ").capitalize(), "telephone": input("Téléphone : ").strip(), "email": input("Email : ").strip().lower(), } contacts.append(contact) sauvegarder_contacts(contacts, FICHIER_CONTACTS) print(f"{contact['prenom']} {contact['nom']} ajouté(e) et sauvegardé(e).") def supprimer_contact(contacts): """Demande un numéro de contact, confirme, supprime et sauvegarde.""" tries = trier(contacts) afficher_contacts(tries) if not tries: return saisie = input("Numéro à supprimer : ").strip() if not saisie.isdigit() or not 1 <= int(saisie) <= len(tries): print("Numéro invalide.") return contact = tries[int(saisie) - 1] confirmation = input(f"Supprimer {contact['prenom']} {contact['nom']} ? (o/n) ").strip().lower() if confirmation == "o": contacts.remove(contact) sauvegarder_contacts(contacts, FICHIER_CONTACTS) print("Contact supprimé.") else: print("Suppression annulée.") def main(): """Boucle principale du carnet de contacts.""" contacts = charger_contacts(FICHIER_CONTACTS) while True: print() print(f"=== CARNET DE CONTACTS ({len(contacts)} contacts) ===") print("1. Lister les contacts") print("2. Ajouter un contact") print("3. Rechercher") print("4. Supprimer un contact") print("5. Exporter en CSV") print("6. Quitter") choix = input("Ton choix : ").strip() if choix == "1": afficher_contacts(trier(contacts)) elif choix == "2": ajouter_contact(contacts) elif choix == "3": texte = input("Rechercher : ").strip() resultats = rechercher(contacts, texte) print(f"{len(resultats)} résultat(s) :") afficher_contacts(resultats) elif choix == "4": supprimer_contact(contacts) elif choix == "5": exporter_csv(contacts, FICHIER_EXPORT) print(f"{len(contacts)} contact(s) exporté(s) dans {FICHIER_EXPORT}.") elif choix == "6": print("Au revoir !") break else: print("Choix invalide.") main() ``` Exemple de première session, en partant sans fichier `contacts.json` (saisies : `2`, `martin`, `alice`, `06 12 34 56 78`, `Alice@Exemple.fr`, `2`, `durand`, une ligne vide, `bob`, `07 98 76 54 32`, `bob@mail.fr`, `1`, `6`) : ```text === CARNET DE CONTACTS (0 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 2 Nom : martin Prénom : alice Téléphone : 06 12 34 56 78 Email : Alice@Exemple.fr Alice MARTIN ajouté(e) et sauvegardé(e). === CARNET DE CONTACTS (1 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 2 Nom : durand Prénom : Ce champ est obligatoire. Prénom : bob Téléphone : 07 98 76 54 32 Email : bob@mail.fr Bob DURAND ajouté(e) et sauvegardé(e). === CARNET DE CONTACTS (2 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 1 N° Nom Prénom Téléphone Email 1 DURAND Bob 07 98 76 54 32 bob@mail.fr 2 MARTIN Alice 06 12 34 56 78 alice@exemple.fr === CARNET DE CONTACTS (2 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 6 Au revoir ! ``` Le fichier `contacts.json` contient alors : ```json [ { "nom": "MARTIN", "prenom": "Alice", "telephone": "06 12 34 56 78", "email": "alice@exemple.fr" }, { "nom": "DURAND", "prenom": "Bob", "telephone": "07 98 76 54 32", "email": "bob@mail.fr" } ] ``` Deuxième session, en relançant le programme (saisies : `3`, `ALI`, `4`, `1`, `o`, `5`, `6`) : les deux contacts ont bien été retrouvés. ```text === CARNET DE CONTACTS (2 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 3 Rechercher : ALI 1 résultat(s) : N° Nom Prénom Téléphone Email 1 MARTIN Alice 06 12 34 56 78 alice@exemple.fr === CARNET DE CONTACTS (2 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 4 N° Nom Prénom Téléphone Email 1 DURAND Bob 07 98 76 54 32 bob@mail.fr 2 MARTIN Alice 06 12 34 56 78 alice@exemple.fr Numéro à supprimer : 1 Supprimer Bob DURAND ? (o/n) o Contact supprimé. === CARNET DE CONTACTS (1 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 5 1 contact(s) exporté(s) dans contacts.csv. === CARNET DE CONTACTS (1 contacts) === 1. Lister les contacts 2. Ajouter un contact 3. Rechercher 4. Supprimer un contact 5. Exporter en CSV 6. Quitter Ton choix : 6 Au revoir ! ``` **Points clés de la solution :** - Le code est organisé en trois couches : **persistance** (fichiers), **traitements** (calculs purs, sans `input` ni `print`), **interface** (dialogue avec l'utilisateur). Cette séparation facilitera les tests ([chapitre 13](13-tests-pytest-bonnes-pratiques.md)) et un éventuel passage à une interface web ([chapitre 14](14-specialisations.md)). - Le fichier est **sauvegardé après chaque modification** : même si le programme est fermé brutalement, rien n'est perdu. - `charger_contacts` gère l'absence de fichier au premier lancement grâce à `exists()`. - `ensure_ascii=False` et `indent=2` rendent `contacts.json` lisible et modifiable à la main. - La suppression se fait sur la liste **triée** affichée (pour que le numéro corresponde à ce que voit l'utilisateur), puis `contacts.remove(contact)` retire ce même dictionnaire de la liste d'origine : les deux listes contiennent des références vers les mêmes dictionnaires (chapitre 04). - `not 1 <= int(saisie) <= len(tries)` vérifie les bornes ; grâce à l'évaluation paresseuse de `or`, `int(saisie)` n'est appelé que si `isdigit()` est vrai. - L'export utilise `utf-8-sig` et `;` pour qu'Excel affiche correctement les accents et les colonnes.
--- ## Récapitulatif - Les fichiers rendent les données **persistantes** : elles survivent à la fin du programme. - `pathlib.Path` manipule les chemins de façon portable : `/` pour assembler, `.name`, `.suffix`, `.parent`, `.exists()`, `.mkdir(parents=True, exist_ok=True)`, `.iterdir()`, `.glob("*.txt")`. `Path(__file__).parent` donne le dossier du script. - On ouvre **toujours** un fichier avec `with open(chemin, mode, encoding="utf-8") as fichier:` ; il est fermé automatiquement à la sortie du bloc. - Modes : `"r"` lecture (erreur si absent), `"w"` écriture (**écrase**), `"a"` ajout à la fin, `"x"` création exclusive. - Lecture : `read()` (tout), `for ligne in fichier` (ligne par ligne, penser à `strip()`), `read().splitlines()` (liste propre). Raccourcis : `Path.read_text()` / `Path.write_text()`. - Écriture : `write()` n'ajoute pas de `\n` ; `print(..., file=fichier)` si. - CSV : `import csv`, ouvrir avec `newline=""` ; `csv.reader` / `csv.writer` (listes), `csv.DictReader` / `csv.DictWriter` (dictionnaires, avec `fieldnames` et `writeheader()`) ; `delimiter=";"` pour le format français. Toutes les valeurs lues sont des **chaînes**. - JSON : `import json` ; `json.dump(donnees, fichier, ensure_ascii=False, indent=4)` pour écrire, `json.load(fichier)` pour lire ; `dumps`/`loads` pour les chaînes. Types conservés, mais clés toujours converties en chaînes, et pas d'ensembles. - Schéma à retenir : une fonction `charger(chemin)` (avec valeur par défaut si le fichier n'existe pas) et une fonction `sauvegarder(donnees, chemin)`. ### Checklist de validation - [ ] Je sais construire un chemin avec `Path` et l'opérateur `/`, et tester s'il existe. - [ ] J'ouvre mes fichiers avec `with` et `encoding="utf-8"`, sans exception. - [ ] Je connais la différence entre les modes `"r"`, `"w"` et `"a"`, et le danger de `"w"`. - [ ] Je sais lire un fichier ligne par ligne en retirant les retours à la ligne. - [ ] Je sais lire un CSV avec `DictReader` et convertir les valeurs en nombres. - [ ] Je sais écrire un CSV avec `DictWriter` (avec `newline=""` et `writeheader()`). - [ ] Je sais sauvegarder et recharger une liste de dictionnaires en JSON lisible et avec accents. - [ ] Mon carnet de contacts retrouve ses données quand je le relance. --- [← Chapitre précédent](06-chaines-et-comprehensions.md) | [Sommaire](README.md) | [Chapitre suivant →](08-erreurs-et-debogage.md)