Python v2.7
[modification 10/4/2012: améliorations importantes du code]
Oui, je sais qu'il y a déjà un module csv dans le système Python. Mais j'avais un projet dans lequel il avait 2 insuffisances:
J'ai préféré refaire les 2 fonctions pour lire et écrire des fichiers '.csv', adaptées à mon problème.
Je les utilise beaucoup pour faire des échanges avec des tableurs (Excel ou OpenOffice Calc), de données issues d'une base de données ou destinées à cette base de données.
Voici les spécifications retenues:
Bien entendu, certaines particularités de profil peuvent être changées dans le code (voir la classe “Profiltableur”)
Le module csv de Python 2.x a une particularité concernant les encodages: il ne supporte pas l'unicode, mais fonctionne bien avec l'encodage 'utf-8'. Mais comme l'encodage voulu, tant sur disque qu'en mémoire n'est pas forcément l'utf-8, on utilise un fichier en ram (module cStringIO) comme intermédiaire.
Voilà le code proposé, présenté sous forme de module (c'est comme ça que je l'utilise):
#!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import division # python v2.7 """ Module pour lire/écrire des fichiers csv en échange avec des tableurs séparateur de champ: ';' entourage des chaines: guillemets (") les nombres ne sont pas entourées par des guillemets les flottants avec des virgules sont convertis en flottant en ram les dates sont transmises comme des chaines (autres profils ==> modifier la classe Profiltableur) """ import csv, codecs, re, cStringIO ############################################################################# class Profiltableur(csv.Dialect): """Profil de lecture/écriture de fichiers CSV avec des tableurs""" delimiter = ';' quotechar = '"' doublequote = True skipinitialspace = False lineterminator = '\r\n' quoting = csv.QUOTE_MINIMAL # enregistrement du profil pour lecture de fichier csv du disque csv.register_dialect('profiltableur', Profiltableur()) ############################################################################# class CSVTableur(object): def __init__(self, profilcsv = 'profiltableur', codisk='cp1252', codram='unicode'): """ le fichier sur disque est encodée 'codisk' (défaut: 'cp1252') les chaines en mémoire sont encodées 'codram' (défaut: 'unicode') pour les chaines: les entrées/sorties avec le mod. csv sont 'utf-8' si virgule=True, les 'float' sont avec virgule décimale sur disque """ self.codisk = codisk # encodage du fichier sur disque (défaut='cp1252') self.codram = codram # encodage des chaines en mémoire (défaut=unicode) self.codcsv = 'utf-8' # encodage des chaines en E/S du module csv self.profilcsv = profilcsv # profil du fichier csv pour lecture self.virgule = True # les 'float' sur disque ont la virgule décimale # pour reconnaitre un nombre entier dans une chaine self.motif_entier = re.compile(r"^[+-]?[0-9]+$") # pour reconnaitre un 'float' avec point décimal dans une chaine self.motif_reel = re.compile(r"^[+-]?(([0-9]+[eE][+-]?[0-9]+)|" +\ r"([0-9]+\.[0-9]*([eE][+-]?[0-9]+)?)|(\,[0-9]+([eE][+-]?[0-9]+)?))$") # pour reconnaitre un 'float' avec virgule décimale dans une chaine self.motif_reelv = re.compile(r"^[+-]?(([0-9]+[eE][+-]?[0-9]+)|" +\ r"([0-9]+\,[0-9]*([eE][+-]?[0-9]+)?)|(\,[0-9]+([eE][+-]?[0-9]+)?))$") #======================================================================== def lirecsv(self, fichier): """lit le fichier CSV et le retourne comme une liste de listes (tableau) """ # pour éviter une erreur de lecture du fichier disque en cas de BOM if self.codisk.upper() == 'UTF-8': self.codisk = 'utf-8-sig' # lire le fichier sur disque encodée codisk # et le mettre dans un fichier en ram avec encodage codcsv fs = cStringIO.StringIO() f = codecs.open(fichier, 'r', self.codisk) for ligne in f: fs.write(ligne.encode(self.codcsv)) #lire les lignes csv du fichier en ram et convertir en liste de listes tableau = [] fs.seek(0) # pour remettre le curseur au début du fichier en ram for ligne in csv.reader(fs, self.profilcsv): tableau.append([]) # ajouter une ligne vide for elem in ligne: if isinstance(elem, (str, unicode)): # correction d'encodage: str ==> unicode if isinstance(elem, str): elem = elem.decode(self.codcsv) if self.motif_entier.match(elem): # chaine reconnue 'nb entier': => convertie en entier elem = int(elem) elif self.motif_reel.match(elem): # chaine reconnue 'float' avec un point: => float elem = float(elem) elif self.virgule and self.motif_reelv.match(elem): # chaine reconnue 'float' avec une virgule: => float elem = float(elem.replace(',', '.')) # ajout à la ligne du tableau if isinstance(elem, unicode) and self.codram!='unicode': tableau[-1].append(elem.encode(self.codram)) else: tableau[-1].append(elem) f.close() return tableau #======================================================================== def ecrirecsv(self, tableau, fichier): """Ecrit la liste de listes 'tableau' dans le fichier CSV sur disque """ # ouverture du fichier en ram fs = cStringIO.StringIO() # écriture de la liste convertie en format csv, dans le fichier en ram c = csv.writer(fs, dialect=self.profilcsv) for liste in tableau: ligne = [] for elem in liste: if self.virgule and isinstance(elem, (float)): # nombre réel: => convertir en chaine avec virgule décimale nr = (u"%s" % (elem)).replace('.', ',') elem = nr.encode(self.codcsv) elif isinstance(elem, (unicode)): # chaine unicode: => conversion pour le fichier en ram elem = elem.encode(self.codcsv) elif isinstance(elem, (str)): if self.codram!='unicode': elem = elem.decode(self.codram).encode(self.codcsv) else: # ici, les chaines ne devraient pas être en 'str' # on laisse passer, mais ça ne devrait pas arriver # espérons que ce soit déjà de l'encodage self.codcsv elem = elem # enregistrement dans la ligne ligne.append(elem) # conversion de la ligne en liste => ligne en chaine format csv c.writerow(ligne) # écriture sur disque du fichier en ram, avec correction d'encodage fs.seek(0) # pour remettre le curseur au début du fichier en ram f = codecs.open(fichier, 'w', self.codisk) # ouverture fichier disque for ligne in fs: f.write(ligne.decode(self.codcsv)) f.close() ############################################################################# if __name__ == "__main__": pass
Exemple d'utilisation:
table = [[12, u"azertyéèç", 45.67, u"toto", "10/04/2012"], [89, u"machinàùô", 7.456, u"titi", "01/01/2010"]] tcsv = CSVTableur() tcsv.ecrirecsv(table, "table.csv") table2 = tcsv.lirecsv("table.csv") for ligne in table2: print ligne
L'affichage de table2 par une fonction adaptée donne:
[12, "azertyéèç", 45.67, "toto", "10/04/2012"] [89, "machinàùô", 7.456, "titi", "01/01/2010"]
Et le fichier table.csv, encodé ici en cp1252 (encodage Windows) contient:
12;azertyéèç;45,67;toto;10/04/2012 89;machinàùô;7,456;titi;01/01/2010
En cas de présence de guillemets dans une chaine en mémoire, cette chaine est entourée de guillemets sur disque, et les guillemets intérieurs sont doublés (…;”xxxx““toto”“xxxx”;…). Bien entendu, ces guillemets intérieurs sont dédoublés à la lecture.
En cas de présence de séparateur de champs ';' dans une chaine en mémoire, elle est simplement entourée de guillemets sur disque (…;”xxxx;xxxx“;…).
Dans tous les cas, on voit que ces fichiers csv sont lu par les tableurs (Excel et OpenOffice calc), et les valeurs sont correctement interprétées et affichées dans leur colonne:
Petite remarque concernant la manière “intelligente” dont les tableurs comprennent les données csv: tout ce qui ressemble à des nombres entiers, même entourés de guillemets, sera interprété comme des nombres. Même des codes postaux (!), pour lesquels ce n'est pas très pertinent…
Amusez-vous bien!