Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 719 minDébutant

Lire et écrire des fichiers : pathlib, CSV et JSON

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • manipuler chemins et dossiers avec pathlib ;
  • lire et écrire un fichier texte dans le bon encodage ;
  • lire et écrire un CSV, y compris un export Excel avec BOM ;
  • convertir des données entre Python et JSON.

3 exercices corrigés

Lire et écrire des fichiers en Python repose sur trois outils de la bibliothèque standard : pathlib pour manipuler les chemins et parcourir les dossiers, open() pour lire et écrire du texte, et les modules csv et json pour les deux formats d'échange les plus courants. Ce chapitre les présente sur l'inventaire du parc de Sorbier Logistique, un fichier CSV exporté d'un tableur, avec les deux pièges qui font perdre une heure : l'encodage et la marque d'ordre des octets (BOM).

Les chemins avec pathlib

Un chemin se manipule avec un objet Path, du module pathlib, plutôt qu'avec des chaînes. L'opérateur / assemble les morceaux avec le bon séparateur, \ sous Windows ou / sous Linux :

python
>>> from pathlib import Path
>>> rapport = Path("rapports") / "2026-11" / "disques.csv"
>>> rapport
PosixPath('rapports/2026-11/disques.csv')
>>> rapport.name, rapport.stem, rapport.suffix
('disques.csv', 'disques', '.csv')
>>> rapport.parent
PosixPath('rapports/2026-11')
>>> rapport.with_suffix(".json")
PosixPath('rapports/2026-11/disques.json')
>>> rapport.exists()
False
>>> Path("inventaire.csv").exists()
True
>>> Path("inventaire.csv").stat().st_size
732

La console affiche PosixPath sous Linux et macOS, WindowsPath sous Windows : le même code marche partout. Un chemin relatif (rapports/…) part du dossier courant du terminal, pas du dossier du script. Pour un fichier rangé à côté du script, quel que soit l'endroit d'où on le lance, écris Path(__file__).parent / "inventaire.csv".

Attribut ou méthodeRôle
p.name, p.stem, p.suffixNom complet, nom sans extension, extension
p.parentLe dossier qui contient p
p.exists(), p.is_file(), p.is_dir()Tests d'existence et de nature
p.mkdir(parents=True, exist_ok=True)Crée le dossier et ses parents, sans erreur s'il existe
p.stat().st_sizeTaille en octets
p.read_text(encoding=…), p.write_text(…)Lire ou écrire tout le fichier d'un coup
p.glob("*.log"), p.rglob("*.log")Fichiers du dossier, ou de toute l'arborescence
p.unlink()Supprime le fichier

Lire et écrire un fichier texte

python
# texte.py : écrire, compléter et relire un fichier texte
from pathlib import Path

dossier = Path("rapports")
dossier.mkdir(exist_ok=True)               # crée le dossier s'il n'existe pas
fichier = dossier / "controle.txt"

fichier.write_text("Contrôle du 8 novembre 2026\n", encoding="utf-8")

with open(fichier, "a", encoding="utf-8") as f:     # "a" : ajout à la fin
    f.write("srv-ad : OK\n")
    f.write("srv-fichiers : ALERTE (87,5 %)\n")

with open(fichier, encoding="utf-8") as f:          # lecture ligne par ligne
    for numero, ligne in enumerate(f, start=1):
        print(numero, ligne.strip())

print(len(fichier.read_text(encoding="utf-8").splitlines()), "lignes")
text
1 Contrôle du 8 novembre 2026
2 srv-ad : OK
3 srv-fichiers : ALERTE (87,5 %)
3 lignes

open(fichier, mode, encoding=…) ouvre un fichier ; le mode "r" (lecture, par défaut), "w" (écriture, qui écrase le fichier) ou "a" (ajout à la fin). Le bloc with ferme le fichier à la fin, même si une erreur survient au milieu : c'est la seule façon correcte d'ouvrir un fichier. Parcourir l'objet fichier donne les lignes une à une, avec leur \n final, que strip() retire. Cette lecture ligne à ligne ne charge jamais tout le fichier en mémoire : elle convient à un journal de plusieurs gigaoctets.

Parcourir un dossier

python
# dossiers.py : parcourir un dossier avec glob et rglob
from pathlib import Path

for journal in sorted(Path("journaux").glob("*.log")):
    print(journal.name)

total = 0
for fichier in Path("partage").rglob("*"):           # récursif : sous-dossiers compris
    if fichier.is_file():
        total += fichier.stat().st_size
print(f"partage : {total / 1024 ** 2:.1f} Mio")

for photo in sorted(Path("partage").rglob("*.jpg")):
    print(photo.as_posix())
text
sauvegarde-2026-11-06.log
sauvegarde-2026-11-07.log
sauvegarde-2026-11-08.log
partage : 163.3 Mio
partage/Logistique/photos/quai-3.jpg
partage/Logistique/photos/quai-4.jpg

glob("*.log") cherche dans le dossier lui-même, rglob("*") dans toute l'arborescence. L'ordre des résultats dépend du système de fichiers : trie-les avec sorted() pour un affichage stable. as_posix() affiche le chemin avec des / sur tous les systèmes.

Lire un CSV avec DictReader

Un fichier CSV (Comma-Separated Values) est un tableau en texte : une ligne par enregistrement, des champs séparés par un délimiteur. Un tableur français utilise le point-virgule, car la virgule y sert de séparateur décimal. Voici l'inventaire du parc, inventaire.csv, enregistré depuis un tableur au format « CSV UTF-8 » :

text
nom;type;service;site;os;achat;fin_garantie;ip
PC-0012;Fixe;Comptabilité;Siège;Windows 11 24H2;2021-03-15;2025-03-15;192.168.10.71
PC-0015;Portable;Logistique;Mâcon;Windows 11 25H2;2024-09-02;2027-09-02;192.168.20.53
PC-0021;Fixe;Logistique;Siège;Windows 11 24H2;2023-01-10;2026-01-10;192.168.10.80
PC-0042;Portable;Direction;Siège;Windows 11 23H2;2020-06-30;2024-06-30;192.168.10.92
PC-0050;Fixe;Logistique;Mâcon;Windows 11 25H2;2025-02-17;2028-02-17;192.168.20.61
PC-0063;Portable;Commercial;Siège;Windows 11 25H2;2025-10-06;2028-10-06;192.168.10.97
PC-0071;Fixe;Comptabilité;Siège;Windows 11 24H2;2023-11-20;2026-11-20;192.168.10.104
srv-web;Serveur;Informatique;Siège;Debian 13;2025-08-11;2030-08-11;192.168.10.225

Première tentative :

python
import csv

with open("inventaire.csv", encoding="utf-8", newline="") as f:
    for ligne in csv.DictReader(f, delimiter=";"):
        print(ligne["nom"])
text
Traceback (most recent call last):
  File "/home/etudiant/cours-python/bom.py", line 5, in <module>
    print(ligne["nom"])
          ~~~~~^^^^^^^
KeyError: 'nom'

La colonne nom existe bien, pourtant Python ne la trouve pas. Le coupable est invisible : Excel commence ses fichiers « CSV UTF-8 » par trois octets spéciaux, la BOM (byte order mark), et le module csv lit la première colonne comme '\ufeffnom'. L'encodage utf-8-sig retire la BOM si elle est là et ne gêne pas si elle n'y est pas :

python
# lire_csv.py : lire l'inventaire et lister les postes hors garantie
import csv

AUJOURD_HUI = "2026-11-08"          # format ISO : les dates se comparent comme du texte

with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
    machines = list(csv.DictReader(f, delimiter=";"))

print(len(machines), "machines ; colonnes :", list(machines[0].keys()))
print(machines[0])

hors_garantie = [m for m in machines if m["fin_garantie"] < AUJOURD_HUI]
for m in hors_garantie:
    print(f"{m['nom']:<9}{m['service']:<14}fin de garantie {m['fin_garantie']}")
text
8 machines ; colonnes : ['nom', 'type', 'service', 'site', 'os', 'achat', 'fin_garantie', 'ip']
{'nom': 'PC-0012', 'type': 'Fixe', 'service': 'Comptabilité', 'site': 'Siège', 'os': 'Windows 11 24H2', 'achat': '2021-03-15', 'fin_garantie': '2025-03-15', 'ip': '192.168.10.71'}
PC-0012  Comptabilité  fin de garantie 2025-03-15
PC-0021  Logistique    fin de garantie 2026-01-10
PC-0042  Direction     fin de garantie 2024-06-30

csv.DictReader lit la première ligne comme en-tête et renvoie chaque ligne suivante sous forme de dictionnaire : on accède aux champs par leur nom, et le script résiste à une colonne ajoutée ou déplacée. Les valeurs sont toujours des chaînes : convertis avec int() ou float() avant de calculer. Les dates au format ISO (AAAA-MM-JJ) ont une propriété pratique : leur ordre alphabétique est l'ordre chronologique, donc "2025-03-15" < "2026-11-08" compare correctement deux dates. Pour des calculs de durée, le module datetime est plus sûr (chapitre 9).

Écrire un CSV

python
# ecrire_csv.py : écrire la liste des postes à renouveler dans un nouveau CSV
import csv
from pathlib import Path

AUJOURD_HUI = "2026-11-08"

with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
    machines = list(csv.DictReader(f, delimiter=";"))

a_renouveler = [
    {"nom": m["nom"], "service": m["service"], "fin_garantie": m["fin_garantie"]}
    for m in machines
    if m["fin_garantie"] < AUJOURD_HUI
]

with open("a-renouveler.csv", "w", encoding="utf-8-sig", newline="") as f:
    ecrivain = csv.DictWriter(f, fieldnames=["nom", "service", "fin_garantie"], delimiter=";")
    ecrivain.writeheader()
    ecrivain.writerows(a_renouveler)

print(Path("a-renouveler.csv").read_text(encoding="utf-8-sig"), end="")
text
nom;service;fin_garantie
PC-0012;Comptabilité;2025-03-15
PC-0021;Logistique;2026-01-10
PC-0042;Direction;2024-06-30

csv.DictWriter écrit des dictionnaires, en suivant l'ordre de colonnes donné par fieldnames ; writeheader() écrit la ligne d'en-tête. Écrire en utf-8-sig remet une BOM : c'est ce qui permet à Excel d'ouvrir le fichier avec les bons accents d'un double-clic. Pour un fichier destiné à un autre programme, utf-8 suffit. Si tu n'as pas de dictionnaires, csv.writer écrit des listes avec writerow().

Ne fabrique jamais un CSV en collant des chaînes avec ";".join(...) : le jour où un champ contient un point-virgule ("Dupont; Martin"), le fichier est corrompu. Le module csv met ce champ entre guillemets automatiquement.

Lire et écrire du JSON

JSON (JavaScript Object Notation) est le format des API web et de nombreux fichiers de configuration. Ses objets correspondent aux dictionnaires Python, ses tableaux aux listes (JSON, XML et CSV).

python
# json_demo.py : écrire puis relire une configuration en JSON
import json
from pathlib import Path

config = {
    "site": "Siège",
    "seuils": {"alerte": 80, "critique": 90},
    "serveurs": ["srv-ad", "srv-fichiers", "srv-journaux"],
    "notifier": True,
    "proxy": None,
}

Path("config.json").write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
print(Path("config.json").read_text(encoding="utf-8"))

relue = json.loads(Path("config.json").read_text(encoding="utf-8"))
print(relue["seuils"]["critique"], relue["serveurs"][-1], type(relue["notifier"]))
text
{
  "site": "Siège",
  "seuils": {
    "alerte": 80,
    "critique": 90
  },
  "serveurs": [
    "srv-ad",
    "srv-fichiers",
    "srv-journaux"
  ],
  "notifier": true,
  "proxy": null
}
90 srv-journaux <class 'bool'>
FonctionRôle
json.dumps(objet)Objet Python → texte JSON
json.loads(texte)Texte JSON → objet Python
json.dump(objet, f)Objet Python → fichier ouvert
json.load(f)Fichier ouvert → objet Python

Les correspondances : dict ↔ objet, list ↔ tableau, str ↔ chaîne, int et float ↔ nombre, True/False ↔ true/false, None ↔ null. indent=2 rend le fichier lisible par un humain ; ensure_ascii=False écrit Siège tel quel au lieu de Si\u00e8ge. Un tuple devient un tableau, et un ensemble n'est pas sérialisable : convertis-le en liste.

CSVJSON
StructureUn tableau plat : lignes et colonnesDes objets imbriqués, des listes dans des objets
TypesTout est texteNombres, booléens, null
Lu parLes tableurs, les imports en masse (comptes, inventaire)Les API, les fichiers de configuration, les applications

Exercices

Exercice 1 sur 3 Combien de machines par service ? Réussi À revoir

À partir de inventaire.csv, écris un script qui compte les machines de chaque service, les affiche par ordre alphabétique de service et écrit le résultat dans machines-par-service.csv (colonnes service et machines), lisible par Excel.

Voir la réponseMasquer la réponse de l'exercice 1
python
# ex_services.py : nombre de machines par service, écrit dans un CSV
import csv
from collections import Counter

with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
    machines = list(csv.DictReader(f, delimiter=";"))

par_service = Counter(m["service"] for m in machines)

with open("machines-par-service.csv", "w", encoding="utf-8-sig", newline="") as f:
    ecrivain = csv.writer(f, delimiter=";")
    ecrivain.writerow(["service", "machines"])
    for service, nombre in sorted(par_service.items()):
        ecrivain.writerow([service, nombre])
        print(f"{service:<14}{nombre}")
text
Commercial    1
Comptabilité  2
Direction     1
Informatique  1
Logistique    3

Counter reçoit directement un générateur des services. L'encodage utf-8-sig en lecture retire la BOM d'Excel, en écriture il en remet une pour qu'Excel affiche correctement Comptabilité.

Tu avais trouvé ?

Exercice 2 sur 3 Du CSV au JSON Réussi À revoir

Convertis inventaire.csv en inventaire.json, un objet dont les clés sont les sites et les valeurs la liste des machines du site (nom et adresse IP seulement). Affiche le nombre de machines par site, puis le JSON du site de Mâcon.

Voir la réponseMasquer la réponse de l'exercice 2
python
# ex_csv_json.py : convertir l'inventaire CSV en JSON, en regroupant par site
import csv
import json
from pathlib import Path

with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
    machines = list(csv.DictReader(f, delimiter=";"))

par_site = {}
for m in machines:
    par_site.setdefault(m["site"], []).append({"nom": m["nom"], "ip": m["ip"]})

texte = json.dumps(par_site, indent=2, ensure_ascii=False)
Path("inventaire.json").write_text(texte, encoding="utf-8")

print({site: len(liste) for site, liste in par_site.items()})
print(json.dumps(par_site["Mâcon"], indent=2, ensure_ascii=False))
text
{'Siège': 6, 'Mâcon': 2}
[
  {
    "nom": "PC-0015",
    "ip": "192.168.20.53"
  },
  {
    "nom": "PC-0050",
    "ip": "192.168.20.61"
  }
]

setdefault(cle, []) renvoie la liste de la clé, en la créant vide la première fois : c'est l'équivalent, sans import, du defaultdict(list) du chapitre 6.

Tu avais trouvé ?

Exercice 3 sur 3 Les plus gros fichiers Réussi À revoir

Le partage partage est presque plein. Écris un script qui affiche les trois plus gros fichiers de toute son arborescence, avec leur taille en Mio et leur chemin relatif au partage.

Voir la réponseMasquer la réponse de l'exercice 3
python
# ex_gros.py : les 3 plus gros fichiers d'un partage
from pathlib import Path

fichiers = [f for f in Path("partage").rglob("*") if f.is_file()]
fichiers.sort(key=lambda f: f.stat().st_size, reverse=True)

for f in fichiers[:3]:
    print(f"{f.stat().st_size / 1024 ** 2:8.1f} Mio  {f.relative_to('partage').as_posix()}")
text
   150.0 Mio  Direction/video-voeux.mp4
     5.0 Mio  Comptabilite/factures-octobre.pdf
     3.2 Mio  Logistique/photos/quai-3.jpg

On ne garde que les fichiers (is_file()), on trie par taille décroissante, et la tranche [:3] garde les trois premiers. relative_to('partage') retire le début du chemin. Sur un vrai partage de plusieurs centaines de milliers de fichiers, appeler stat() deux fois par fichier coûte cher : calcule la taille une fois, dans une liste de tuples (taille, chemin).

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Erreurs et exceptions : lire un traceback, try et exceptRevoir le sommaire du cours