Outils pour utilisateurs

Outils du site


lecture_ecriture_csv

Module spécifique de lecture/écriture de fichier csv

Python v2.7

[modification 10/4/2012: améliorations importantes du code]

Objectif

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:

  • problème d'encodage: j'ai des fichiers csv encodées 'cp1252' sur disque (échanges avec Excel) et unicode en mémoire alors que le module csv ne supporte que l'utf-8: comment je fais?
  • les flottants Python traités par le module csv sont restitués avec un point décimal, ce qui n'est pas compris comme un nombre dans Excel, et qui est compris comme une date dans OpenOffice!

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.

Code proposé

Voici les spécifications retenues:

  • sont supportés: les chaines, les entiers (int et long), les flottants (float) et les dates.
  • les chaines sont entourées par des guillemets: “…..” en cas de présence à l'intérieur de séparateurs ';' ou de guillemets '“'. Dans ce dernier cas, les guillemets internes sont doublés sur disque et dédoublés à la lecture.
  • on donne l'encodage des chaines csv sur disque ('cp1252'=Windows par défaut)
  • on donne l'encodage des chaines en mémoire (unicode par défaut)
  • les valeurs des champs sont séparées par des ';'
  • les fichiers csv peuvent avoir des nombres flottants avec virgules décimales (format français des tableurs)

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:

  • chaines calées à gauche,
  • nombres entiers calés à droite
  • nombres décimaux avec virgules calées à droite (donc bien compris comme un nombre)
  • dates calées à droite (donc bien compris comme une date et non comme une chaine)

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!

lecture_ecriture_csv.txt · Dernière modification: 2012/04/10 07:49 de tyrtamos

Outils de la page