1) En exécutant le programme de l'énoncé, puis en relisant le fichier produit :
import csv
lignes = [["titre", "auteur"], ["Analyse ; tome 1", "Nour"], ["Algebre", "Ines"]]
with open("catalogue.csv", "w", encoding="utf-8", newline="") as sortie:
redacteur = csv.writer(sortie, delimiter=";")
redacteur.writerows(lignes)
with open("catalogue.csv", "r", encoding="utf-8") as entree:
print(entree.read(), end="")
Le fichier contient les trois lignes :
titre;auteur
"Analyse ; tome 1";Nour
Algebre;Ines
Le module a ajouté une paire de guillemets autour du champ
Analyse ; tome 1, et seulement autour de celui-là. C'est la convention du format : un champ qui contient le séparateur est protégé par des guillemets, de sorte que le point-virgule intérieur ne compte pas comme une frontière de champ. Les deux autres lignes ne contiennent aucun caractère à protéger et sont écrites telles quelles.
Chaque enregistrement est par ailleurs terminé par les deux caractères
\r\n, que le module écrit lui-même sur toutes les machines. C'est pour cela que le flux est ouvert avec
newline="" : sous Windows, un flux de texte ouvert sans cet argument traduirait en outre chaque
\n en
\r\n, ajoutant un
\r de trop, et le fichier compterait une ligne vide sur deux ; ailleurs, la fin de ligne s'écrivant
\n, cette traduction n'a pas lieu et la faute dort jusqu'au premier passage sur une machine Windows.
2) Le découpage naïf coupe le titre en deux et garde les guillemets, puisqu'il ne connaît que le caractère
; :
with open("catalogue.csv", "r", encoding="utf-8", newline="") as entree:
deuxieme = entree.readlines()[1]
print("split :", deuxieme.strip().split(";"))
with open("catalogue.csv", "r", encoding="utf-8", newline="") as entree:
print("csv :", list(csv.reader(entree, delimiter=";"))[1])
split : ['"Analyse ', ' tome 1"', 'Nour']
csv : ['Analyse ; tome 1', 'Nour']
Le premier donne trois champs, dont aucun n'est le titre ; le second en donne deux, et rend le titre intact. Conclusion : dès qu'un fichier est au format CSV, on le lit avec le module
csv, en lecture comme en écriture. Un
split ne convient qu'à un format dont on a vérifié soi-même qu'aucun champ ne peut contenir le séparateur.
3) Les deux fonctions prennent le séparateur en paramètre, ce qui permettra de les fausser au 4).
import csv
import random
ALPHABET = "ab ;\"'"
def ecrire_table(nom_fichier, table, separateur=";"):
"""Écrit une table de chaînes dans un fichier CSV.
Entrées : nom_fichier, chaîne nommant le fichier à créer ou à écraser ;
table, liste d'enregistrements, chaque enregistrement étant une liste
de chaînes ; separateur, le caractère qui sépare les champs.
Sortie : rien ; le fichier contient un enregistrement par ligne.
"""
with open(nom_fichier, "w", encoding="utf-8", newline="") as sortie:
redacteur = csv.writer(sortie, delimiter=separateur)
redacteur.writerows(table)
def lire_table(nom_fichier, separateur=";"):
"""Renvoie la table des enregistrements d'un fichier CSV.
Entrées : nom_fichier, chaîne nommant un fichier CSV existant ;
separateur, le caractère qui sépare les champs.
Sortie : la liste des enregistrements, chacun étant la liste de ses champs,
tous de type chaîne.
"""
with open(nom_fichier, "r", encoding="utf-8", newline="") as entree:
return [champs for champs in csv.reader(entree, delimiter=separateur)]
Le test tire d'abord la forme de la table, puis chaque champ, caractère par caractère. La propriété éprouvée s'énonce en une phrase : pour toute table de chaînes, écrire puis relire redonne la table de départ.
def table_au_hasard():
"""Renvoie une table de chaînes tirée au hasard sur l'alphabet du test.
Entrées : aucune.
Sortie : une liste de 0 à 5 enregistrements, chacun formé de 1 à 4 champs
de longueur au plus 5, tirés sur ALPHABET.
"""
table = []
for _ in range(random.randint(0, 5)):
enregistrement = []
for _ in range(random.randint(1, 4)):
longueur = random.randint(0, 5)
enregistrement.append("".join(random.choice(ALPHABET) for _ in range(longueur)))
table.append(enregistrement)
return table
def tester_aller_retour(separateur_ecriture=";", separateur_lecture=";"):
"""Éprouve le couple ecrire_table et lire_table par des allers-retours.
Entrées : les deux séparateurs employés, à l'écriture et à la lecture.
Sortie : rien ; l'exécution s'interrompt sur une AssertionError au premier
écart entre la table écrite et la table relue, et se tait si tout passe.
"""
random.seed(2026)
cas = [[], [[""]], [["", ""]]]
for _ in range(200):
cas.append(table_au_hasard())
for attendu in cas:
ecrire_table("essai_catalogue.csv", attendu, separateur_ecriture)
obtenu = lire_table("essai_catalogue.csv", separateur_lecture)
assert obtenu == attendu, (attendu, obtenu)
La graine fixée rend l'échec rejouable, et le message d'assertion porte les deux tables : le contre-exemple est sous les yeux sans avoir à le chercher. Les trois cas ajoutés à la main sont ceux que le tirage donne rarement, et ce sont précisément les plus fragiles : un champ vide unique s'écrit
"" dans le fichier, faute de quoi la ligne serait vide et l'enregistrement disparaîtrait à la relecture.
4) Avec
delimiter="," à l'écriture et
delimiter=";" à la lecture, c'est le troisième cas ajouté à la main qui tombe le premier, avant même le premier tirage au hasard : la table
[["", ""]] s'écrit
, avec une virgule pour séparateur, et la relecture au point-virgule y voit un unique champ, la chaîne
,.
tester_aller_retour()
print("aller-retour : 203 tables verifiees, cas limites compris")
try:
tester_aller_retour(",", ";")
except AssertionError as erreur:
print("AssertionError :", erreur)
tester_aller_retour(",", ",")
print("erreur des deux cotes : 203 tables verifiees, et le test se tait")
Ce programme affiche :
aller-retour : 203 tables verifiees, cas limites compris
AssertionError : ([['', '']], [[',']])
erreur des deux cotes : 203 tables verifiees, et le test se tait
La troisième ligne est la leçon de l'exercice. Avec la virgule des deux côtés, les 203 cas passent, alors que le fichier produit n'est pas celui que l'énoncé demandait et qu'aucun autre programme ne le relira correctement. Un test d'aller-retour contrôle le couple, pas chacune des deux fonctions : deux fonctions fausses de la même manière se donnent raison l'une à l'autre. Il faut lui adjoindre un test de la lecture seule, sur un fichier écrit à la main dont on connaît le contenu.
5) La propriété n'est vraie que sur les tables dont tous les champs sont des chaînes. Le format CSV est un format texte : le module convertit les nombres à l'écriture et ne les reconnaît pas à la relecture, qui rend toujours des chaînes.
ecrire_table("essai_catalogue.csv", [["note"], [12.5]])
print(lire_table("essai_catalogue.csv"))
Ce programme affiche :
[['note'], ['12.5']]
La table relue diffère de la table écrite, sans qu'aucune des deux fonctions soit en cause : c'est la propriété qui était mal bornée. Tirer les champs sur un alphabet de caractères, comme au 3), évite ce piège par construction, et la spécification de
lire_table annonce d'ailleurs que tous les champs renvoyés sont des chaînes.
À retenir : le module
csv protège les champs qui contiennent le séparateur, un test d'aller-retour ne contrôle que le couple, et une propriété testée au hasard doit d'abord être bornée au domaine où elle est vraie.