Aller au contenu
English

Lire et écrire des fichiers

  • Leçon 10
  • 50 min
  • Module 4 : Fichiers, données et services externes
Leçon 10 sur 18 · Python pour l'ingénierie de l'IA en pratiqueLire et écrire des fichiers

Ce que vous saurez faire

Lisez les billets dans un vrai fichier de données. Gardez chaque billet valide, indiquez chaque enregistrement rejeté avec sa raison, puis enregistrez le résultat dans un fichier de rapport. Rien ne disparaît sans laisser de trace, et une exécution qui échoue ne laisse jamais un rapport endommagé.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter contient chaque étape, tâche et vérification de cette leçon, avec les consignes. Ses premières cellules créent le dossier ticket-cleaner comme au début de cette leçon et téléchargent les fichiers de données. Le carnet fonctionne donc sans votre projet local. Lisez les explications ici et exécutez le code dans le carnet.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local; le carnet et les encadrés de cette page n’en ont pas besoin)

Testé avec Python 3.14.6 sur macOS et Pyodide 314.0 (navigateur). Le carnet a été testé dans Jupyter.

Les données du cours se trouvent dans le dépôt public des laboratoires. Il contient 15 billets synthétiques : 9 valides et 6 avec un problème chacun. Téléchargez-les dans un dossier data de votre projet.

  1. Assurez-vous que le dossier data existe. Vous l’avez créé dans le module 1. Sinon, exécutez mkdir data.

  2. Téléchargez le fichier :

    Fenêtre de terminal
    curl -o data/tickets.csv https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/tickets.csv

    Si vous ne pouvez pas utiliser curl, ouvrez le lien dans votre navigateur. Enregistrez la page sous tickets.csv dans le dossier data.

  3. Faites la même chose pour la version JSON : dans la commande, remplacez tickets.csv par tickets.json aux deux endroits.

  4. Ouvrez data/tickets.csv dans VS Code et lisez-le. Il est assez court pour le lire au complet.

data/tickets.csv (first lines)
id,status,category,priority,subject
T-1001,open,billing,2,Charged twice for March
T-1002,closed,login,3,Password reset email is late
T-1003,open,login,1,Cannot sign in after update

Un fichier sur disque est une suite d’octets. Pour le lire comme du texte, Python doit connaître l’encodage : la règle qui transforme les octets en lettres. Aujourd’hui, l’encodage standard du texte est UTF-8. Il couvre toutes les langues, et les données du cours l’utilisent.

Si vous ne précisez pas d’encodage, Python utilise celui par défaut de l’ordinateur. Sur macOS et Linux, c’est UTF-8. Sur Windows, c’est souvent un encodage plus ancien, et les lettres absentes de l’anglais s’affichent mal. Précisez donc toujours l’encodage :

with open("data/tickets.csv", encoding="utf-8") as file:
text = file.read()

L’instruction with ouvre le fichier et lui donne le nom file pour le bloc indenté. Elle ferme le fichier à la fin du bloc, même si une erreur arrête le bloc. Un fichier qui reste ouvert peut garder vos changements en mémoire plutôt que sur disque, ou empêcher un autre programme de l’utiliser. L’instruction with est un gestionnaire de contexte : elle prépare quelque chose avant un bloc et fait toujours le nettoyage après.

Le module pathlib fournit un type Path pour les chemins de fichiers. Un Path connaît les parties d’un chemin et assemble correctement les chemins sur chaque système :

Code Résultat Signification
Path("data") / "tickets.csv" data/tickets.csv Assemblez avec /, même sur Windows. Python y utilise \ pour vous.
Path("data/tickets.csv").suffix ".csv" L’extension du fichier.
Path("data/tickets.csv").name "tickets.csv" Le nom du fichier.
Path("reports/summary.json").parent reports Le dossier qui le contient.
path.exists() True ou False Y a-t-il un fichier ou un dossier à ce chemin?

Un chemin relatif comme data/tickets.csv part du répertoire de travail, comme dans Les fichiers et le terminal. Vous exécutez donc le script depuis le dossier du projet.

Un fichier CSV (« valeurs séparées par des virgules ») est un tableau sous forme de texte : une ligne par rangée, avec des virgules entre les valeurs. La première ligne contient généralement les noms des colonnes. Le module csv le lit. csv.DictReader fournit chaque ligne sous forme de dictionnaire, avec les noms des colonnes comme clés :

import csv
with open("data/tickets.csv", encoding="utf-8", newline="") as file:
rows = list(csv.DictReader(file))
print(rows[0])
Sortie
{'id': 'T-1001', 'status': 'open', 'category': 'billing', 'priority': '2', 'subject': 'Charged twice for March'}

Chaque valeur est une chaîne, même '2'. Un fichier CSV n’a pas de types. C’est pourquoi vous avez écrit find_problems et to_ticket dans le module 3. L’argument newline="" laisse le module csv gérer lui-même les fins de ligne, car une valeur entre guillemets peut contenir un saut de ligne.

Un fichier JSON contient les mêmes sortes de valeurs que Python : objets (comme des dictionnaires), listes, chaînes, nombres, true, false et null. Le module json transforme le texte JSON en valeurs Python :

import json
with open("data/tickets.json", encoding="utf-8") as file:
rows = json.load(file)
print(rows[0])
Sortie
{'id': 'T-1001', 'status': 'open', 'category': 'billing', 'priority': 2, 'subject': 'Charged twice for March'}

Ici, la priorité est le nombre 2, pas la chaîne '2'. find_problems accepte les deux, car clean_text transforme d’abord toute valeur en texte. La valeur JSON null devient None, et true devient True.

Exécutez du Python dans votre navigateur

Lire un fichier CSV

"w" ouvre un fichier en écriture et le recrée à neuf. \n est un saut de ligne. Exécutez le code, puis ajoutez une troisième ligne, T-1003,pending,1, et exécutez-le de nouveau.

Exemple détaillé : lire, séparer, produire un rapport, écrire

Section intitulée « Exemple détaillé : lire, séparer, produire un rapport, écrire »

Le programme comporte quatre étapes. Chacune est une fonction qui a une seule tâche.

1. Lire les lignes. Un nouveau module, files.py, lit un fichier CSV ou JSON et produit une liste de dictionnaires. L’extension du fichier détermine quel lecteur utiliser :

ticket_cleaner/files.py
import csv
import json
import os
from pathlib import Path
def read_rows(path: Path) -> list[dict]:
"""Read raw records from a .csv or .json file."""
if path.suffix == ".csv":
with open(path, encoding="utf-8", newline="") as file:
return list(csv.DictReader(file))
if path.suffix == ".json":
with open(path, encoding="utf-8") as file:
return json.load(file)
raise ValueError(f"cannot read {path.name}: use a .csv or .json file")

La dernière ligne utilise raise. Elle arrête la fonction avec une erreur et un message, car aucune liste correcte ne peut être retournée pour un fichier .txt. Le module 5 montre comment un programme intercepte une telle erreur.

2. Séparer les billets valides des enregistrements rejetés. Un enregistrement rejeté a besoin de son propre petit type, avec le numéro de ligne et les problèmes. Ajoutez-le à records.py :

ticket_cleaner/records.py (add at the end)
@dataclass(frozen=True)
class Rejected:
"""An input record that is not a valid ticket, and why."""
row: int
problems: list[str]

Ajoutez ensuite split_records à parsing.py. Cette fonction repère aussi un identifiant de billet qui apparaît deux fois. Ajoutez Rejected à la ligne d’importation en haut de parsing.py.

ticket_cleaner/parsing.py (add at the end)
def split_records(rows: list[dict]) -> tuple[list[Ticket], list[Rejected]]:
"""Separate valid tickets from rejected records. Nothing is dropped silently."""
tickets = []
rejected = []
seen_ids = set()
for row_number, raw in enumerate(rows, start=1):
problems = find_problems(raw)
if not problems:
ticket = to_ticket(raw)
if ticket.id in seen_ids:
problems.append(f"duplicate id {ticket.id}")
if problems:
rejected.append(Rejected(row=row_number, problems=problems))
else:
tickets.append(ticket)
seen_ids.add(ticket.id)
return tickets, rejected

enumerate(rows, start=1) fournit chaque ligne avec son numéro : 1 pour le premier enregistrement, 2 pour le deuxième. La fonction retourne un tuple de deux listes, que le code appelant décompose : tickets, rejected = split_records(rows). Chaque ligne d’entrée se retrouve dans exactement une des deux listes. C’est la promesse : « rien n’est écarté sans laisser de trace ».

3. Construire le rapport. Ajoutez build_summary à report.py. Cette fonction retourne un dictionnaire contenant seulement des types que JSON peut stocker : chaînes, nombres, listes, dictionnaires et None. Ajoutez Rejected à la ligne d’importation en haut de report.py.

ticket_cleaner/report.py (add at the end)
def build_summary(tickets: list[Ticket], rejected: list[Rejected], status: str) -> dict:
"""Return the report as a dictionary that can be saved as JSON."""
selected = filter_by_status(tickets, status)
return {
"status": status,
"valid_records": len(tickets),
"selected": len(selected),
"by_category": count_by_category(selected),
"average_priority": average_priority(selected),
"rejected": [{"row": r.row, "problems": r.problems} for r in rejected],
}

4. Écrire le rapport de façon sûre. Ajoutez write_json à files.py. Deux règles rendent la sortie sûre. D’abord, n’écrivez jamais par-dessus le fichier d’entrée. Ensuite, ne laissez jamais un fichier incomplet : si le programme s’arrête pendant l’écriture, l’ancien rapport doit rester complet.

ticket_cleaner/files.py (add at the end)
def write_json(data: dict, path: Path) -> None:
"""Save data as JSON. Write a temporary file first, so a failure never leaves half a file."""
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
with open(temporary, "w", encoding="utf-8") as file:
json.dump(data, file, indent=2, ensure_ascii=False)
file.write("\n")
os.replace(temporary, path)
Ligne Pourquoi
path.parent.mkdir(parents=True, exist_ok=True) Crée le dossier reports s’il n’existe pas. exist_ok=True signifie « aucune erreur s’il existe déjà ».
temporary = ... Les données vont d’abord dans summary.json.tmp.
json.dump(..., indent=2, ensure_ascii=False) Écrit du JSON lisible, en gardant les lettres comme ü telles quelles.
os.replace(temporary, path) Renomme le fichier terminé en summary.json en une seule étape. Un lecteur voit l’ancien fichier complet ou le nouveau fichier complet, jamais un mélange.

Le script lit maintenant le fichier plutôt que les lignes d’exemple :

count_tickets.py
from pathlib import Path
from ticket_cleaner.files import read_rows, write_json
from ticket_cleaner.parsing import split_records
from ticket_cleaner.report import build_summary
INPUT_FILE = Path("data/tickets.csv")
OUTPUT_FILE = Path("reports/summary.json")
def main() -> None:
rows = read_rows(INPUT_FILE)
tickets, rejected = split_records(rows)
for record in rejected:
print(f"Row {record.row} rejected: {'; '.join(record.problems)}")
summary = build_summary(tickets, rejected, status="open")
write_json(summary, OUTPUT_FILE)
print(f"{len(rows)} rows: {len(tickets)} valid, {len(rejected)} rejected.")
print(f"Report: {OUTPUT_FILE}")
if __name__ == "__main__":
main()
  1. Créez ticket_cleaner/files.py avec read_rows et write_json.

  2. Ajoutez Rejected à records.py, split_records à parsing.py et build_summary à report.py. Mettez à jour les lignes d’importation en haut de parsing.py et de report.py.

  3. Remplacez count_tickets.py par la nouvelle version.

  4. Exécutez-le :

    Fenêtre de terminal
    python count_tickets.py
    Sortie
    Row 7 rejected: unknown status 'opne'
    Row 8 rejected: missing id
    Row 9 rejected: priority must be 1, 2 or 3, not 'high'
    Row 11 rejected: priority must be 1, 2 or 3, not '5'
    Row 13 rejected: duplicate id T-1003
    Row 14 rejected: missing category
    15 rows: 9 valid, 6 rejected.
    Report: reports/summary.json
  5. Ouvrez reports/summary.json dans VS Code. Vérifiez que selected vaut 4 et que la liste rejected contient 6 entrées.

  6. Vérifiez que rien n’est perdu : 9 valides + 6 rejetés = 15 lignes. Ouvrez data/tickets.csv et trouvez la ligne 7 des données. Elle se trouve à la ligne 8 du fichier, car la ligne 1 contient les noms des colonnes.

À vous de jouer : une entrée JSON, un deuxième rapport

Section intitulée « À vous de jouer : une entrée JSON, un deuxième rapport »

Modifiez le script pour qu’il lise data/tickets.json et écrive le rapport des billets en attente dans reports/pending.json. Changez seulement les constantes et un argument. Prédisez les nombres avant de l’exécuter.

Afficher le résultat attendu

Définissez INPUT_FILE = Path("data/tickets.json"), OUTPUT_FILE = Path("reports/pending.json") et status="pending". La sortie affiche encore 9 valides et 6 rejetés, car le fichier JSON contient les mêmes enregistrements. reports/pending.json contient "selected": 2, "by_category": {"billing": 1, "login": 1} et "average_priority": 1.5. La ligne 10 (LOGIN) compte comme login, car to_ticket l’a nettoyée.

reports/summary.json n’a pas changé : chaque exécution écrit seulement dans son propre fichier de sortie. Remettez les constantes au fichier CSV et à summary.json avant la prochaine leçon.

Erreur courante

Des lettres étranges comme Müller

Symptôme : le programme fonctionne, mais un objet de billet dans la sortie ou le rapport affiche Müller plutôt que Müller. Ou le programme s’arrête avec UnicodeDecodeError.

Cause : Python a lu le fichier avec un encodage différent de celui utilisé pour l’enregistrer. Le fichier est en UTF-8, où ü occupe deux octets. Un encodage plus ancien lit ces deux octets comme deux lettres, ü. Cela arrive sur Windows quand open() n’a pas d’argument encoding, car l’encodage par défaut n’y est souvent pas UTF-8.

Correction :

  1. Trouvez chaque open( dans votre code. Chacun doit avoir encoding="utf-8".
  2. Exécutez le programme de nouveau.
  3. Si un rapport a été écrit avec les mauvaises lettres, exécutez le programme de nouveau pour l’écrire correctement. N’essayez pas de corriger les lettres à la main.

Vérifiez votre compréhension

Pourquoi count_tickets.py affiche-t-il les lignes rejetées et les enregistre-t-il dans le rapport, plutôt que de compter seulement les billets valides?

Afficher la réponse

Pour qu’aucun enregistrement ne disparaisse sans laisser de trace. Si le programme ignorait simplement les lignes invalides, le rapport afficherait 9 billets et personne ne saurait qu’il en existait 6 autres. Avec les numéros de ligne et les raisons, une personne peut trouver chaque enregistrement dans le fichier et le corriger. La vérification est simple : valides + rejetés = toutes les lignes.

Vérifiez votre compréhension

Le programme s'arrête avec une erreur pendant que json.dump écrit le rapport. Que reste-t-il sur disque : l'ancien rapport, la moitié d'un nouveau rapport ou rien?

Afficher la réponse

L’ancien rapport, complet. Les nouvelles données vont dans summary.json.tmp, et seul os.replace, à la fin, lui donne le vrai nom. Si le programme s’arrête avant cette ligne, summary.json reste le dernier rapport complet. Un fichier summary.json.tmp peut rester sur disque, et vous pouvez le supprimer.

Récapitulatif

  • Ouvrez toujours les fichiers texte avec encoding="utf-8", dans un bloc with, pour que le fichier se ferme même après une erreur.
  • pathlib.Path assemble les chemins et fournit leurs parties. Les chemins relatifs partent du répertoire de travail.
  • csv.DictReader fournit chaque valeur sous forme de chaîne. json.load conserve les nombres, true, false et null.
  • Séparez les entrées en enregistrements valides et rejetés. Chaque ligne se retrouve dans exactement une liste, avec une raison pour chaque rejet.
  • Écrivez la sortie dans un fichier temporaire, puis renommez-le avec os.replace. N’écrivez jamais par-dessus l’entrée.

Ensuite, les mêmes enregistrements proviendront d’un service Web plutôt que d’un fichier.

Leçon suivante : Utiliser un client HTTP simple