Bonjour,

Nos PC actuels, complétés par les disques accessibles en réseau, peuvent comporter plusieurs millions de fichiers. Et la question de mon titre est l'une des questions que je me pose de temps en temps, et jusqu'à présent, je n'ai rien trouvé de pratique pour y répondre facilement. Alors, j'ai cherché à créer une solution en Python qui soit très adaptable à mes besoins.

Voilà un petit exemple simplifié de ce que j'ai fait.

Il faut d'abord créer ce qu'il faut pour chercher tous les fichiers d'un répertoire et de ses sous-répertoires, et ne retenir que ceux qui satisfont un filtre: j'ai utilisé os.walk pour naviguer de façon récursive dans une arborescence:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding: utf-8 -*-
# Python 3
 
import os
from datetime import datetime, timedelta, date
from fnmatch import fnmatch
 
class Cherchefichiers(object):
    """Permet de trouver tous les fichiers d'un répertoire et de ses 
       sous-répertoire qui répond à des tests de filtrage
    """
 
    #========================================================================
    def __init__(self, repertoire=""):
        # Enregistre le répertoire fourni après sa normalisation
        self.repertoire = os.path.abspath(os.path.expanduser(repertoire))
        # initialise la liste des erreurs consultable en fin de recherche
        self.listerreurs = [] 
 
    #========================================================================
    def fnerreurs(self, msgerr):
        """Reçoit et enregistre les éventuels messages d'erreur de os.walk
        """
        self.listerreurs.append([msgerr.errno, msgerr.strerror, msgerr.filename])
 
    #========================================================================
    def cherche(self, filtre=None):
        """Trouve les fichiers du répertoire et de ses sous-répertoires
           qui satisfont le filtre donné
        """
        if filtre==None:
            filtre = lambda x: True # sans filtre donné, on accepte tout
        self.listerreurs = [] # remet à vide la liste des erreurs
        for repert, sreps, fics in os.walk(self.repertoire, onerror=self.fnerreurs):
            for fic in fics:
                fichier = os.path.join(repert, fic) # ajout du chemin
                try:
                    os.path.getsize(fichier) # uniquement pour valider l'accès
                    if filtre(fichier):
                        yield fichier
                except OSError as msgerr:
                    self.fnerreurs(msgerr)
Il était, bien sûr, indispensable de gérer les erreurs d'accès à des répertoires ou à des fichiers, et dans le code précédent, on peut les questionner après coup pour les afficher (si on veut).

On va ensuite créer un filtre portant sur les noms de fichier, permettant de ne retenir que les noms qui satisfont un motif donné, par exemple "*.py", et même sous Windows, "*.py;*.pyw":

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Filtrewildcard(object):
    """Dit si le nom de fichier donné satisfait le motif wildcard donné
       Le motif (type str) peut être plusieurs motifs séparés par des ';'
       Le fichier peut être donné avec ou sans son chemin
    """
 
    #========================================================================
    def __init__(self, motif="*"):
        self.motifs = motif.split(';') # plusieurs motifs séparés par ';'
 
    #========================================================================
    def __call__(self, fichier):
        nomfichier = os.path.basename(fichier) # élimine le chemin s'il y est
        for motif in self.motifs:
            if fnmatch(nomfichier, motif):
                return True # on a trouvé un motif qui marche!
        return False # aucun des motifs donnés n'a été satisfait
Et enfin, on va créer un filtre que dit si un fichier a été modifié à une date donné ou après:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Filtredate(object):
    """Filtre un fichier (avec son chemin) selon sa date de modification. 
       La date dtime est donnée en type datetime: dtime=datetime(an,mois,jour)
       Les comparaisons de date ne tiennent pas compte des heures
    """
 
    #========================================================================
    def __init__(self, dtime=None):
        if dtime==None:
            self.datecible = date.today() # on prend la date d'aujourd'hui
        else:    
            self.datecible = dtime.date()
        self.epoch = datetime.utcfromtimestamp(0) # calcul de l'epoch de l'OS
 
    #========================================================================
    def apres(self, fichier):
        """Dit si le fichier a été modifié à la date dtime ou après
        """
        tempsfichier = os.path.getmtime(fichier)
        dtimefichier = (self.epoch + timedelta(seconds=tempsfichier))
        return dtimefichier.date() >= self.datecible
Il ne reste plus qu'à combiner les filtres et les donner à la recherche. Voilà un exemple d'utilisation:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    # répertoire de recherche
    repertoire = r"C:\Python34"
 
    # filtre par wildcard: on cherche tous les scripts Python
    filtrenom = Filtrewildcard("*.py;*.pyw")
 
    # filtre par date: ici c'est le 17/5/2016 et après
    filtredate = Filtredate(datetime(2016,5,17)).apres
 
    # combine les 2 filtres
    filtre = lambda f: filtrenom(f) and filtredate(f)
 
    # cherche et affiche les fichiers satisfaisant aux filtres
    fichiers = Cherchefichiers(repertoire)
    nb = 0 # compteur
     for fichier in fichiers.cherche(filtre):
        datefichier = (datetime.utcfromtimestamp(0) + timedelta(seconds=os.path.getmtime(fichier))).strftime('%Y-%m-%d')
        print(fichier, datefichier) # on ajoute la date du fichier seulement pour vérifier
        nb += 1
    print()
    print("Nombre de fichiers trouvés:", nb)
    print()
On peut si on veut consulter et afficher les erreurs après la recherche:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
    # on affiche les erreurs s'il en a
    if fichiers.listerreurs==[]:
        print("Aucune erreur")
    else:
        print(len(fichiers.listerreurs), "Erreurs trouvées:")
        for erreur in fichiers.listerreurs:
            print(erreur)
Cela affichera quelque chose comme:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
C:\Python34\Lib\site-packages\easy_install.py 2016-08-25
C:\Python34\Lib\site-packages\path.py 2016-05-17
C:\Python34\Lib\site-packages\pyparsing.py 2016-05-17
...
...
C:\Python34\Scripts\pyi-makespec-script.py 2016-05-17
C:\Python34\Scripts\pyi-set_version-script.py 2016-05-17
C:\Python34\Scripts\pyinstaller-script.py 2016-05-17
 
Nombre de fichiers trouvés: 1104
 
Aucune erreur
Ce n'est qu'un exemple simple, mais qui peut être la base d'une recherche beaucoup plus approfondie:

Pour le filtre par nom de fichier, on peut ajouter par exemple:
- un filtre par expression régulière,
- un filtre par recherche de mots similaires (avec ratio de proximité), utile si vous ne vous rappelez plus comment vous avez nommé "exactement" le fichier cherché.

Pour ces filtres par nom, on peut ajouter l'option "tenir compte de la casse ou pas" (majuscules/minuscules). Pour ne pas tenir compte de la casse, il suffit en général pour la comparaison de convertir avant le nom de fichier ainsi que le motif, en majuscules. Sauf pour les expressions régulières pour lesquelles il existe une option prédéfinie à inclure dans la recherche.

Pour le filtre par date, on peut ajouter:
- les fichiers "avant" une date,
- les fichiers de date "égale" à une date,
- les fichiers "entre" 2 dates.
- les fichiers créés ou modifiés le week-end?
- etc...

Vous pouvez aussi ajouter n'importe quel autre filtre qui vous intéresse, par exemple sur la taille du fichier (trouver tous les fichiers >1Go), ou même sur la longueur du nom de fichier (trouver tous les noms de fichiers de plus de 25 caractères)!

Les fichiers trouvés sont affichés dans l'ordre de recherche par os.walk. Mais si on veut les présenter autrement, par exemple filtrés par ordre alphabétique, ou chronologique, ou par n'importe quel autre critère, on les met tous dans une liste qu'on trie ensuite avec le critère choisi (.sort(key=...)).

Je vais essayer de trouver un peu de temps pour mettre tout ça dans un tuto sur mon site de recettes...

J'ai fait ça avec Python 3.4.3 en respectant un codage multiplateforme: n'hésitez pas à me signaler les éventuels problèmes rencontrés!

Pour mon usage perso, j'ai créé un programme graphique PyQt5 (.exe avec cx_freeze) pour faire toutes ces recherches à l'écran: ça marche très bien, c'est très pratique et mon but a été atteint.

Amusez-vous bien!