2010 lines
70 KiB
Markdown
2010 lines
70 KiB
Markdown
# 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 "<stdin>", line 3, in <module>
|
||
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)) # → <class 'dict'>
|
||
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 "<stdin>", line 3, in <module>
|
||
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 "<stdin>", line 5, in <module>
|
||
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 "<stdin>", line 7, in <module>
|
||
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
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 1</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 2</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 3</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 4</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 5</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 6</summary>
|
||
|
||
```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)).
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 7</summary>
|
||
|
||
```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`.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé de l'exercice 8</summary>
|
||
|
||
```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`.
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Corrigé du mini-projet</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
## 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)
|