logo le blog invivoo blanc

Gestion avancée de fichiers (Python)

1 avril 2021 | Design & Code, Python | 0 comments

Lorsque l’on travaille sur des tâches au niveau système, on doit régulièrement appliquer une même action à tous les fichiers (Python) d’un même type : archivage, compter le nombre de lignes de code, supprimer tous les fichiers log ayant plus de 30 jours, etc…

PARCOURIR LES ENTREES D’UN REPERTOIRE

> listdir

Le package ‘os’ fournit la fonction ‘listdir’ qui, pour un nom de répertoire donné, retourne la liste de noms de fichiers/répertoires présents dans ce répertoire. Voici un exemple d’utilisation :

from os import listdir

for name in listdir( "C:\\Temp" ):
    print( name )

> walk

‘walk’ est un itérateur qui permet à l’utilisateur de s’affranchir du parcours récursif du répertoire.

from os import walk

for root, dirs, files in walk( 'C:\\Temp', topdown=True ):
    print( root )
    print( dirs )
    print( files )
    print( '--------------------------------' )

Cette fonction alloue de nombreuses listes de chaînes de caractères.

> scandir

Depuis Python 3.4, la méthode conseillée est ‘scandir’ car elle est beaucoup plus efficace. J’ai pu constater un gain de performance de x20 sur des cas complexes d’utilisation…

from os import scandir

with scandir( "C:\\Temp" ) as it:
    for entry in it:
        print( entry.name )

L’utilisation contextuelle avec ‘with’ est importante, elle garantit la libération des ressources ouvertes. L’itérateur obtenu avec ‘scandir’ permet de parcourir des instances d’objet de type os.DirEntry (Plus de détail ici) grâce auquel on peut accéder à différentes fonctions/propriétés permettant de connaître la nature de l’entrée (fichier, répertoire ou lien symbolique) ou des informations comme les dates et tailles.

REUTILISATION

L’idée consiste à créer une fonction ‘filemap’ qui va parcourir récursivement une arborescence et appliquer une action sur tous les fichiers qui sont une cible. On pourra ainsi réutiliser cette fonction dans différents contextes…

from os import scandir

def filemap( path, action, is_target ):
    with scandir( path ) as it:
        for entry in it:
            if entry.is_dir():
                filemap( entry.path, action, is_target )
            elif is_target( entry ):
                action( entry )

Le paramètre ‘action’ est utilisé pour passer une fonction callback qui prendra en entrée l’instance d’un objet de type os.DirEntry. Cette fonction appliquera une action (le supprimer, le déplacer, etc…) sur un fichier. Voici un exemple d’utilisation :

filemap( "c:\\Temp",
         lambda e : print( e.name ),
         lambda e : e.name.lower().endswith( ".py" ) )

On affiche le nom de chaque fichier source Python présent dans l’arborescence de « C:\Temp ».

Cet article sur la gestion avancées des fichiers (Python) vous est proposé par l’expertise Python d’Invivoo.

COMPTER LES FICHIERS

Compter les fichiers est un besoin qui présente un intérêt d’un point de vue fonctionnel (valider que le nombre de fichiers générés par la production est identique à l’attendu) mais aussi sur le plan informatique car il nous amènera à mieux penser notre code. L’implémentation naïve serait :

count = 0
def count_file( entry ):
    global count
    count += 1

filemap( "c:\\Temp",
         count_file,
         lambda e : e.name.lower().endswith( ".py" ) )
print( count )

‘count_file’ permet effectivement de compter les fichiers mais elle ne respecte pas le concept de pureté de la programmation fonctionnelle car elle dépend d’une variable globale. Si j’oubliais de réinitialiser ‘count’ entre 2 appels à ‘filemap’ j’obtiendrais un résultat incorrect.

Pour corriger ce problème, Python dispose d’un outil très intéressant : on peut créer des objets qui se comporte comme des fonctions. Grâce à l’objet on va pouvoir stocker l’état du compteur tout en préservant la pureté…

class CountFile:
    def __init__( self ):
        self.__count = 0

    @property
    def count( self ):
        return self.__count

    def __call__( self, entry ):
        self.__count += 1

    def __str__( self ):
        return F"{self.__count}"

c = CountFile()
filemap( "c:\\Temp",
         c,
         lambda e : e.name.lower().endswith( ".py" ) )
print( c )

La fonction ‘__call__’ est l’action qui sera appelée et qui permet l’objet de se comporter comme une fonction.

LE CIBLAGE

> Ciblage par les extensions

Dans notre exemple précédent, nous avons ciblé les fichiers sources Python grâce à une fonction lambda :

lambda e : e.name.lower().endswith( ".py" )

C’est régulièrement que l’on va avoir besoin de cibler les fichiers via leur extension. Il nous faudrait avoir un moyen plus simple et plus rapide pour cibler un type d’extension ou un groupe d’extensions. La solution pythonique par excellence sera fourni par un générateur de fonction :

def endswith( *args ):
    extensions = [ ext.lower() for ext in args ]

    def _is_target( entry ):
        name = entry.name.lower()
        for ext in extensions:
            if name.endswith( ext ):
                return True
        return False

    return _is_target


c = CountFile()
filemap( "C:\\Tools\\Anaconda3",
         c,
         endswith( ".py", ".txt" ) )
print( c ) 

> Ciblage par la date de création

Lorsque l’on gère des arborescences de production, on est souvent amené à mettre en place des outils de suppression des fichiers en fonction de leurs dates de créations : on supprime les fichiers créés il y a plus de 30 jours.

def older_than( day_count ):
    from datetime import datetime as DT,
                         timedelta, timezone
    today = DT.now()
    epoch = DT( 1970, 1, 1 )
    ref   = timedelta( hours = day_count * 24 )

    def _is_target( entry ):
        creation_date = epoch + timedelta( seconds = entry.stat().st_ctime )
        return ( today - creation_date ) >= ref

    return _is_target


filemap( "C:\\Temp",
         lambda e: unlink( e.path ),
         older_than( 30 ) )