Avec les années qui passent et les tailles de disques qui augmentent, on se retrouve de plus en plus avec des problèmes de gestion de fichiers, soit qu'on en a perdu un, soit qu'on cherche à gagner de la place en éliminant les fichiers inutiles.

J'ai donc créé du code pour:
- trouver tous les fichiers d'un répertoire et de ses sous-répertoires, répondant à des motifs "wildcard" (par exemple pour des photos: ["*.jpg", "*.jpeg", "*.png", "*.tiff"]).
- parmi ces fichiers, trouver ceux qui ont le même nom (mais pas forcément le même contenu).
- parmi ces fichiers, trouver ceux qui ont le même contenu (mais pas forcément le même nom).

Bien sûr, ce code n'est pas nécessaire si le problème ne concerne que 10 fichiers que je peux traiter à la main! Mais si j'en ai 30.000, faire à la main est carrément impossible...

1- trouver tous les fichiers d'un répertoires et de ses sous-répertoires, répondant à des motifs "wildcard"

Pour présenter un code simple, j'ai utilisé le module "glob". En contrepartie de sa simplicité, il a tout de même pour moi plusieurs inconvénients dont une gestion "silencieuse" des erreurs, et je lui préfère pour ça "os.walk". Voilà la fonction de recherche avec glob:

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
import os
from glob import iglob
 
def cherchefichiers(repertoire, motifs=('*')):
    """Cherche les fichiers du répertoire et de ses sous-répertoires, 
       satisfaisant aux motifs wildcard donnés
       - repertoire: adresse disque du repertoire avec son chemin
       - motifs: liste des motifs wildcard pour sélection des fichiers
       Retourne la liste des fichiers trouvés
    """
    fichiers = []
    for motif in motifs:
        for entree in iglob(os.path.join(repertoire, "**", motif), recursive=True):
            if os.path.isfile(entree):
                fichiers.append(entree)
    return fichiers
Exemple d'utilisation:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
    repertoire = r"E:\Programmes\Python37"
    motifs = ["*.py"]
    fichiers = cherchefichiers(repertoire, motifs)
2-parmi ces fichiers, trouver ceux qui ont le même nom (mais pas forcément le même contenu)

Avec le résultat précédent (liste "fichiers"), voilà un petit code qui va regrouper les noms de fichiers identiques:

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
import os
 
def cherchemultiplenoms(fichiers):
    """Cherche les fichiers ayant le même nom (pas forcément le même contenu!)
       - fichiers: liste des adresses disque des fichiers avec leur chemin
       Retourne la liste des fichiers regroupés par nom
    """
    # tri selon les noms de fichiers
    fichiers.sort(key=lambda v: os.path.basename(v))
    #
    multiplenoms = []
    k, kmax = 0, len(fichiers)
    while k<kmax:
        # 1er nom de fichier différent
        i1 = k
        nom = os.path.basename(fichiers[i1])
        # recherche de tous les noms identiques à la référence "nom"
        i2 = i1+1
        while i2<kmax:
            if os.path.basename(fichiers[i2]) != nom:
                break
            i2 += 1
        # enregistrement s'il y a plusieurs noms identiques
        if i2-i1>1:
            multiplenoms.append([])
            for i in range(i1, i2):
                multiplenoms[-1].append(fichiers[i])        
        # fichier suivant
        k = i2
    return multiplenoms
Exemple d'utilisation (appel et affichage des résultats):

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
    multiplenoms = cherchemultiplenoms(fichiers)
    # affichage
    if len(multiplenoms)==0:
        print("Pas de noms de fichiers multiples")
    else:    
        print(len(multiplenoms), "fichiers ayant le même nom")
        for multiplenom in multiplenoms:
            for fichier in multiplenom:
                print(os.path.basename(fichier), "====", fichier)
            print()
3-parmi ces fichiers, trouver ceux qui ont le même contenu (mais pas forcément le même nom)

Comparer plusieurs fichiers sur leur contenu nécessite le calcul préalable de leur "hashcode" (https://fr.wikipedia.org/wiki/Foncti...ryptographique) qui permettra de résumer en une chaîne de caractères de taille limitée mais unique, des contenus de fichiers qui pourront avoir plusieurs Gigaoctets. Pour ce genre de problème, le hashcode le plus courant est "MD5", mais il a l'inconvénient d'avoir des "collisions", c'est à dire de pouvoir obtenir la même valeur avec des contenus différents. Même si c'est rare, c'est très embêtant. J'ai donc choisi "SHA256" qui est meilleur sur ce plan, (https://fr.wikipedia.org/wiki/Secure_Hash_Algorithm), et qui est fourni avec le module "hashlib" de Python. Avec Python >=3.6, on peut même utiliser le SHA3-256 qui est de classe "3" plus moderne (https://docs.python.org/fr/3/library...module-hashlib).

Mais avec de nombreux fichiers, le calcul des hashcodes est assez long. Pour accélérer, il est intéressant de faire agir les multiples cœurs de nos CPU modernes "multicore". Par exemple, avec un CPU de 4 cœurs, le temps sera divisé par environ 3! On fera ce calcul avec le module Python "concurrent.futures":

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
from concurrent.futures import ProcessPoolExecutor
try:
    from hashlib import sha3_256 as sha256
except Exception:
    from hashlib import sha256 # pour Python3 < 3.6
 
def hashfichier(fichier):
    """Calcule le hashcode SHA256 du contenu du fichier
       - fichier: adresse disque du fichier avec son chemin
       Retourne le résultat en hexa (type str)
    """
    h = sha256()
    with open(fichier, 'rb') as fs:
        while True:
            bloc = fs.read(32768)
            if not bloc:
                break # lecture fichier terminée
            h.update(bloc)
    return h.hexdigest()
 
def hashmulticore(fichiers):
    """Calcule le hashcode du contenu des fichiers de la liste des fichiers
       Le calcul est fait en parallèle avec concurrent.futures qui utilisent 
       les coeurs disponibles des CPU multicore
       Utilise la fonction "hashfichier"
       Retourne la liste des ...[fichier, hashcode], ...
    """
    fichierinfos = []
    with ProcessPoolExecutor() as executor:
        for fichier, hashcode in zip(fichiers, executor.map(hashfichier, fichiers)):
            fichierinfos.append([fichier, hashcode])
    return fichierinfos
Exemple d'utilisation:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
    fichiersinfos = hashmulticore(fichiers)
Avec la liste des fichiers en argument, on obtient la liste des [fichier, hashcode].

Question performance, il n'y a pas de miracle: pour 30.000 fichiers, le temps de calcul sera proche d'une heure. Mais n'oubliez pas que c'est pour faire quelque chose que vous ne pourrez pas faire autrement...

Petit conseil: comme le calcul des hashcodes est assez long, je vous suggère d'enregistrer le résultat sur disque avec "pickle". Cela vous permettra si nécessaire de faire par la suite de multiples calculs basés sur la même liste des [fichier, hashcode] tant que vous n'avez pas modifié ces fichiers.

Avec cette liste, on peut calculer les "doublons", c'est à dire les fichiers ayant le même contenu (sans forcément avoir le même nom). Le principe est simple: on calcule un dictionnaire dont les clés sont les hashcodes, et dont les valeurs sont la liste des fichiers ayant le même hashcode. Et on ne retient bien sûr que les hashcodes avec au moins 2 fichiers. J'ai choisi d'utiliser un dictionnaire ordonné, mais ce n'est plus nécessaire avec les dernières versions de Python qui conservent l'ordre de création des clé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
22
23
24
25
26
from collections import OrderedDict
 
def cherchedoublons(fichierinfos):
    """Recherche les fichiers de même contenu quelques soient leurs noms
       - fichierinfos: liste des [...[fichier, hashcode], ...]
       Retourne un dictionnaire ordonné:
       - clé: hashcode, 
       - val: liste des fichiers ayant cet hashcode
    """
    fichierinfos.sort(key=lambda v: v[0]) # tri selon les fichiers
    fichierinfos.sort(key=lambda v: v[1]) # tri selon les hashcodes
 
    dicohash = OrderedDict()
    for fichier, hashcode in fichierinfos:    
        if hashcode in dicohash:
            dicohash[hashcode].append(fichier)
        else:
            dicohash[hashcode] = [fichier]    
 
    doublons = OrderedDict()
    for hashcode, fichiers in dicohash.items():
        if len(fichiers)>=2:
            # il faut au moins 2 fichiers avec le même hashcode pour un doublon
            doublons[hashcode] = fichiers
 
    return doublons
Exemple d'utilisation (appel et affichage):

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
    doublons = cherchedoublons(fichiersinfos)
 
    # affichage
    print("Affichage des doublons du répertoire:", repertoire)
    print()
    for hashcode, fichiers in doublons.items():
        print(hashcode)
        for fichier in fichiers:
            print("   ", os.path.basename(fichier), "====", fichier)
        print()
 
    print(len(doublons),"doublons trouvés")
Exemple de résultats affiché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
...
9dfc5874411ea7a0d5391b30a024d50a269b01e9983e501f91b2779b47cba18b
    hook-PyQt5.QtWebKitWidgets.py ==== E:\Programmes\Python37\Lib\site-packages\PyInstaller\hooks\hook-PyQt5.QtWebKitWidgets.py
    hook-PySide2.QtWebKitWidgets.py ==== E:\Programmes\Python37\Lib\site-packages\PyInstaller\hooks\hook-PySide2.QtWebKitWidgets.py
...
ed5dea2db6f8a30960f595d776753c2ddc2c3f5a83ea7e9641226fa746e9f38b
    wait.py ==== E:\Programmes\Python37\Lib\site-packages\pip\_vendor\urllib3\util\wait.py
    wait.py ==== E:\Programmes\Python37\Lib\site-packages\urllib3\util\wait.py
 
eef27b57072fd53dfe165b3d42e4dc3657df1801875529eceaae70dd3d3ea0f3
    __about__.py ==== E:\Programmes\Python37\Lib\site-packages\packaging\__about__.py
    __about__.py ==== E:\Programmes\Python37\Lib\site-packages\pip\_vendor\packaging\__about__.py
    __about__.py ==== E:\Programmes\Python37\Lib\site-packages\pkg_resources\_vendor\packaging\__about__.py
    __about__.py ==== E:\Programmes\Python37\Lib\site-packages\setuptools\_vendor\packaging\__about__.py
 
f214648cc6b9b59e703c26fd185766058d5ff03fa7ef5b9792e3ca7e993e1a8f
    initialise.py ==== E:\Programmes\Python37\Lib\site-packages\colorama\initialise.py
    initialise.py ==== E:\Programmes\Python37\Lib\site-packages\pip\_vendor\colorama\initialise.py
...
A partir de résultats portant sur vos fichiers personnels (pas ceux de Python!), vous pouvez générer un petit code pour automatiser si nécessaire des suppressions et des recopies de fichiers.

Avec la même logique, on peut traiter un cas un peu différent, c'est comparer 2 répertoires, et identifier les fichiers communs et ceux qui sont différents. Un exemple concret c'est: avec le temps, je me retrouve avec 2 versions différentes d'un même répertoire et je voudrais en supprimer un: comment faire, en sachant qu'il y a des fichiers communs et d'autres qui ne le sont pas. Si certains sont intéressés, je peux compléter avec les codes que j'utilise pour ça.

Amusez-vous bien!