Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Une des utilisations de Python est l'écriture de scripts chargés d'automatiser certaines tâches. Traditionnellement, cela implique la lecture d'un fichier, la transformation des données et l'écriture du résultat dans un autre fichier. C'est en fait un travail de filtre selon la terminologie Unix.

C'est le prétexte qui va nous servir ici pour examiner comment lire et écrire dans un fichier texte avec Python.

Les fichiers

Texte ou binaire?

On divise habituellement les fichiers en deux catégories:

les fichiers textes
ceux-ci sont humainement lisibles. Autrement dit, ils contiennent du texte (lettres, ponctuations, nombres, ...). Leur contenu est souvent divisé en lignes. Ces fichiers peuvent être lus et manipulés avec les outils traditionnels Unix: vi, cat, sed, etc. En environnement Unix, les fichiers de configuration dans /etc sont des exemples de fichiers texte. De même que le code source d'un programme Python.
les fichiers binaires
ceux-ci ne contiennent pas (exclusivement) du texte. Et ils ne peuvent être convenablement traités que par des logiciels spécialisés. Un fichier PDF, une image JPEG ou un mp3 sont quelques exemples de fichiers binaires.

Ici, nous allons exclusivement nous intéresser aux fichiers textes. Si vous avez plutôt besoin de manipuler un fichier binaire, je vous renvoie sur l'article complémentaire: lire et écrire un fichier binaire avec Python.

Ouvrir, manipuler, fermer

Du point de vue du programme qui va s'en servir, un fichier fait parti de ce que l'on peut appeler une ressource. Or, les ressources sont limitées, et d'une manière ou d'une autre, l'accès à ces ressources doit être partagé entre les différents programmes qui peuvent en avoir besoin.

Concrètement, cela veut dire qu'il est nécessaire d'acquérir cette ressource avant de s'en servir, puis de la libérer après usage. Pour un fichier, cela se fait en l'ouvrant – puis en le fermant:

# Ouverture d'un fichier en *lecture*:
fichier = open("/etc/passwd", "r")
 
# ...
# Utilisation du fichier
# ...
 
# Fermeture du fichier
fichier.close()

Remarque:

Le second paramètre de la fonction open est le mode dans lequel ou souhaite ouvrir le fichier. Différents modes sont possibles. Parmi lesquels "r" (pour read), que nous utilisons ici, et qui indique que nous allons ouvrir le fichier pour lire des données dedans. On peut aussi ouvrir un fichier pour écrire en utilisant le mode "w" à la place:

# Ouverture d'un fichier en *écriture*:
fichier = open("/etc/passwd", "w")

Mais attention, dans ce cas, si le fichier existait déjà, il est vidé (son contenu est écrasé)! Si vous voulez plutôt ajouter des données à la fin d'un fichier, il faut utiliser le mode "a" (pour append):

# Ouverture d'un fichier en *ajout*:
fichier = open("/etc/passwd", "a")

Un exemple concret

Passées ces généralités, nous allons nous atteler à un exemple concret. Un utilisateur télécharge des fichiers au format CSV (Comma-separated values) (qui est un format texte). Ces fichiers contiennent des cotations boursières dont voici un extrait:

sh$ cat table.csv
Date,Open,High,Low,Close,Volume,Adj Close
2009-09-25,21.10,21.20,20.83,20.86,35133500,20.86
2009-09-24,21.16,21.35,21.06,21.17,28386500,21.17
2009-09-23,21.47,21.47,21.05,21.13,40676000,21.13
2009-09-22,21.59,21.71,21.35,21.41,34151900,21.41
2009-09-21,21.56,21.82,21.50,21.57,25486700,21.57
2009-09-18,21.72,21.87,21.57,21.62,70093600,21.62
2009-09-17,21.42,21.77,21.32,21.52,92130200,21.52
2009-09-16,22.60,22.61,21.98,22.13,85853100,22.13
2009-09-15,22.73,22.88,22.60,22.66,24786600,22.66
2009-09-14,22.78,22.93,22.60,22.72,24066100,22.72
2009-09-11,22.82,22.95,22.67,22.86,22600600,22.86
...

Comme vous le voyez, ce fichier contient les historiques de cours d'une action, et précise pour chaque jour, le cours d'ouverture, le plus haut, le plus bas, le volume de transaction et enfin le cours de fermeture ajusté.

Notre utilisateur n'a pas besoin de toutes ces informations, mais seulement de la date, du cours de fermeture ajusté, et d'un indicateur à calculer permettant de savoir si le cours à la fermeture était plus élevé qu'à l'ouverture ou pas.

Et nous allons utiliser un script Python pour extraire ces informations. Comme vous l'avez noté, les données sont dans un fichier table.csv. Que nous souhaitons lire. De la même manière, le résultat doit être écrit dans un fichier (disons out.csv). Ce qui nous donne les premières lignes de notre script:

#!/usr/bin/python
# vim : set fileencoding=utf-8 :
 
#
# filtrecours.py
#
# Extrait la date, le cours ajusté et la 'direction' de l'historique
# de cours d'une action
#
 
def filtrer(src, dst):
    """Fonction de traitement.
 
    Lit et traite ligne par ligne le fichier source (src).
    Le résultat est écrit au fur et à mesure dans le
    fichier destination (dst). 
    """
    pass # A DEFINIR!
 
 
# Ouverture du fichier source
source = open("table.csv", "r")
 
# Ouverture du fichier destination
destination = open("out.csv", "w")
 
 
# Appeler la fonction de traitement
filtrer(source, destination)
 
 
# Fermeture du fichier destination
destination.close()
 
# Fermerture du fichier source
source.close()

D'accord, ça fait déjà beaucoup de lignes. Mais j'attire votre attention sur le fait que dans leur immense majorité, il s'agit de commentaires! Reste à écrire la fonction de traitement.

Lire dans un fichier

Pour lire dans un fichier, Python offre plusieurs possibilités:

C'est la troisième solution que nous allons utiliser: en effet, inutile de lire tout le fichier d'un coup (il peut être excessivement volumineux – et de toute manière nous pouvons traiter nos données ligne par ligne). Nous allons juste traiter la première ligne – contenant l'en-tête – comme un cas particulier:

def filtrer(src, dst):
    """Fonction de traitement.
 
    Lit et traite ligne par ligne le fichier source (src).
    Le résultat est écrit au fur et à mesure dans le
    fichier destination (dst). 
    """
    # Lit l'en-tête
    entete = src.readline().rstrip('\n\r')
 
    # Puis les données
    for ligne in src:
        pass

rstrip("\n\r")?

Vous avez peut-être noté l'appel à la méthode rstrip:

src.readline().rstrip('\n\r')

C'est la méthode recommandée à partir de Python 2.2 [1] pour supprimer le (ou les) caractère(s) de fin de ligne ... situés au bout de chaque ligne. En effet, ceux-ci sont préservés par readline (et readlines) bien qu'ils soient souvent inutiles.

Traitement de l'en-tête

Notre traitement est minime, puisqu'il consiste à extraire certaines données. Un peu de manipulation de chaînes avec la méthode str.split notamment devrait faire l'affaire. La seule pseudo-complexité va être de déterminer quelles colonnes nous intéressent. Ici encore, Python nous simplifie la vie grâce à la méthode list.index:

def filtrer(src, dst):
    #...
 
    # Lit l'en-tête
    entete = src.readline().rstrip('\n\r').split(",")
    dateidx = entete.index("Date")
    advcloseidx = entete.index("Adj Close")
    closeidx = entete.index("Close")
    openidx = entete.index("Open")
 
    #...

Comme ce n'est pas l'essentiel de cet article, nous n'allons pas trop nous attarder là dessus. Vous retrouverez d'ailleurs quelques éléments de traitement du même acabit quand nous traiterons de l'écriture dans la section suivante. En effet, si le code ci-dessus concerne le traitement de l'en-tête, il y aura aussi un peu de traitement à faire ligne par ligne...

Ecrire dans un fichier

Muni de tous les index nécessaires, il devient facile d'écrire le résultat souhaité. Surtout quand je vous aurai dit qu'on peut écrire avec la méthode file.write. Au final, la fonction filtrer devient:

def filtrer(src, dst):
    """Fonction de traitement.
 
    Lit et traite ligne par ligne le fichier source (src).
    Le résultat est écrit au fur et à mesure dans le
    fichier destination (dst). 
    """
    # Lit l'en-tête, élimine la fin de ligne, et extrait les 
    # champs séparés par une virgule
    entete = src.readline().rstrip('\n\r').split(",")
 
    # Détermine l'index des différents champs qui nous sont utiles
    dateidx = entete.index("Date")
    advcloseidx = entete.index("Adj Close")
    closeidx = entete.index("Close")
    openidx = entete.index("Open")
 
    # Ecrit l'en-tête
    dst.write("Date,Adj Close,Direction\n")
 
    # Puis les données
    for ligne in src:
        # Extraction des données de la ligne séparées par une virgule
        donnees = ligne.rstrip('\n\r').split(",")
 
        if donnees[closeidx] > donnees[openidx]:
            direction = 1
        elif  donnees[closeidx] < donnees[openidx]:
            direction = -1
        else:
            direction = 0
 
        # Ecriture des données dans le fichier destination
        dst.write("%s,%s,%s\n" % (donnees[dateidx], donnees[advcloseidx], direction))

Un poil de robustesse en plus

Avant de terminer, nous allons faire une dernière modification. Celle-ci n'est pas vraiment indispensable dans un script utilitaire comme ici. Mais est plus que recommandé sur des programmes plus ambitieux. Alors, autant prendre de bonnes habitudes. De quoi puis-je bien parler? Et bien, du fait qu'il peut y avoir un problème à un moment ou un autre de l'exécution du programme: le disque peut être plein, ou alors le fichier source est sur un disque réseau devenu brusquement inaccessible. Ou simplement le traitement est trop long, et l'utilisateur a pressé contrôle-c. Tous ces cas entraînent l'arrêt brusque du programme. Sans passer par la case close sensée fermer proprement les fichiers.

Encore une fois, ce n'est pas si dramatique dans le cas d'un script, puisque le système ferme de force les fichiers laissés ouvert à la fin du programme. C'est plus problématique sur un programme devant fonctionner plus longtemps: en effet, le nombre de fichiers ouverts simultanément est limité. Et même dans notre cas, cela peut poser problème, puisque Python ne garantit pas que les données écrites le soient vraiment tant que le fichier n'a pas été fermé proprement...

Dans tous les cas, la solution consiste à utiliser un bloc try ... finally qui garantit qu'une séquence d'instructions sera exécutée, que le bloc try se passe normalement ou qu'une exception se produise. Notre programme principal devient alors:

# Ouverture du fichier source
source = open("table.csv", "r")
 
# Ouverture du fichier destination
destination = open("out.csv", "w")
 
try:
    # Appeler la fonction de traitement
    filtrer(source, destination)
finally:
    # Fermeture du fichier destination
    destination.close()
 
    # Fermerture du fichier source
    source.close()

Références