Recherche de doublons dans des dossiers.

Soyez le premier à donner votre avis sur cette source.

Snippet vu 9 139 fois - Téléchargée 16 fois

Contenu du snippet

Ce script permet de faire une recherche récursive dans un dossier pour y trouver les fichiers doublons.
Ici les fichiers recherchés sont sélectionner selon (.jpg|.png|.gif|.bmp)$
Lancer : lance la recherche
Reset : Remet à zéros l'affichage dans l'interface graphique.

Actions :
Supprimer : Supprime les éléments marqués dans la liste.
Auto. : Sélectionne automatiquement les doublons à supprimer.
Ouvrir : Ouvre tous les fichiers sélectionnés.
Inverser : Inverse la sélection.
Réinit. : Désélectionne tous les éléments.

J'ai utilisé les threads pour augmenter la vitesse d'exécution, et cela au dépend de la stabilité.
Si le nombre de fichiers scannés est supérieur à 1500, il y a 1/5 chances de bugger (Runtime Error).

Vitesse (approx.) :
150 fichiers : 0.65 sec.
250 fichiers : 2.38 sec.
1000 fichiers : 7,9 sec.
1500 fichiers : 12,2 sec.
8500 fichiers : entre 140 et 180 sec

Sous Linux :
Le bind <return> n'est pas supporté.
Aucun problème d'instabilité.
Les performances sont deux fois supérieures en dessous de 600 fichiers,
Par contre elles sont trois fois inférieurs au-dessus.

Merci de votre Feed-Back

Python3k

Source / Exemple :


#!/usr/bin/env python3.1

from tkinter import *
import tkinter.filedialog as filedialog
import tkinter.ttk as ttk
import tkinter.font as tkFont
import tkinter.messagebox as box
import os, stat, re, time, threading

class Interface (Tk) :
    def __init__ (self):
        Tk.__init__(self)
        self.grid ()

        self.col = ("OX","Nom","Groupe","Chemin","Taille","Date")                           # Titre des colonnes
        self.colWid = {"OX":15,"Nom":170,"Groupe":25,"Chemin":350,"Taille":80,"Date":140}   # Taille des colonnes
        self.app = Duplicate()      #Crée une instance de Duplicate, utilisée plus loin.
        
        self.d = threading.Thread() #Instancie un thread pour vérifier d.isAlive()

        ### FRAMES #######################################################
        self.Left = Frame(self, width=600)
        self.Right = Frame(self)
        self.Bottom = Frame (self)
        self.Bar = Frame(self,relief="sunken", width=600)
        
        self.Left.grid (row=1,column=0,columnspan=4, sticky=NW)
        self.Right.grid(row=0,column=3,sticky=NW)
        self.Bottom.grid(row=2,column=0,columnspan=2,sticky=NW)
        self.Bar.grid(row=3,column=0,columnspan=2,sticky=NW)

        ### Choix du Chemin ############################################
        self.EntryStr = StringVar ()
        self.EntryPath = Entry(self, textvariable=self.EntryStr,width=70)
        self.EntryPath.grid(row=0,column=0,sticky="NSEW")

        ### Boutons supérieurs #########################################
        Button(self, text='Lancer',command=self.Launch).grid(row=0,column=1,sticky='NEW')   # Exécution.
        Button(self, text='Reset',command=self.Reset).grid(row=0,column=2,sticky='NEW')     # Réinitialise StringVar et Treeview.

        ### Treeview - Multilist #######################################
        self.tree = ttk.Treeview(self.Left,columns=self.col, show="headings", height=30)
        vsb = ttk.Scrollbar(self.Left,orient="vertical", command=self.tree.yview)
        hsb = ttk.Scrollbar(self.Left,orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
        self.tree.grid(column=0, row=1, sticky='nsew')
        vsb.grid(column=1, row=1,sticky='nsw')
        hsb.grid(column=0, row=2, sticky='ew')

        # Crée les titres des colonnes :
        for col in self.col:
            self.tree.heading(col, text=col.title(),
                              command=lambda c=col: sortby(self.tree, c, 0))
            self.tree.column(col, width=self.colWid[col])

        ### INFO #######################################################
        # INFO Barre  Titles:
    
        self.Info_file_Titles = Label (self.Right, text="Fichiers",
                                       anchor=NW).grid(row=0,column=0, sticky=NE)
        self.Info_Original_Titles = Label (self.Right, text="Doublons",
                                           anchor=NW).grid(row=1,column=0, sticky=NE)
        self.Info_Size_Titles = Label (self.Right, text="Tailles",
                                       anchor=NW).grid(row=2,column=0, sticky=NE)
        
        # INFO Barre Values :
        Size_Info = 20
        
        self.Info_file_Str = StringVar ()           # Nombre d'éléments (selon le pattern de re.compile, ici fichiers images)
        self.Info_Original_Str = StringVar ()       # Nombre de groupes de doublons trouvés.
        self.Info_Size_Str = StringVar ()           # Taille totales des fichiers uniques '+' taille des doublons.
        
        self.Info_file = Label (self.Right, textvariable=self.Info_file_Str,
                                anchor=NW,
                                padx=10,
                                width=Size_Info,
                                relief='groove',
                                bg='blue',
                                fg="white",).grid(row=0,column=1, sticky=NW)
        self.Info_Original = Label (self.Right, textvariable=self.Info_Original_Str,
                                    anchor=NW,
                                    padx=10,
                                    width=Size_Info,
                                    relief='groove',
                                    bg='blue',
                                    fg="white").grid(row=1,column=1, sticky=NW)
        self.Info_Size = Label (self.Right, textvariable=self.Info_Size_Str,
                                anchor=NW,
                                padx=10,
                                width=Size_Info,
                                relief='groove',
                                bg='blue',
                                fg="white").grid(row=2,column=1, sticky=NW)

        ### Actions : ##################################################
        Label(self.Bottom,text='Actions : ').grid(row=0,column=0)
        Button(self.Bottom, text='Supprimer', command=self.DelSelected).grid(row=0,column=1)    # Supprimer
        Button(self.Bottom, text='Auto.',command=self.Auto).grid(row=0,column=2)                # Sélection automatique
        Button(self.Bottom, text='Ouvrir',command=self.OpenFile).grid(row=0,column=3)           # Ouvrir les objets sélectionnés
        Button(self.Bottom, text='Inverser', command=self.Reverse).grid(row=0,column=4)         # Inverser la sélection
        Button(self.Bottom, text='Réinit.',command=self.ResetTree).grid(row=0,column=5)         # 'O' pour tous (aucun élément sélecionné)

        ### Barre d'état ###############################################
        self.Bar_File_Str = StringVar ()
        self.Bar_File = Label(self.Bar, textvariable=self.Bar_File_Str)                         # Affichera le temps après exécution.
        self.Bar_File.grid(row=0,column=0)

        ### Images #####################################################
        self.check = PhotoImage(file="check.gif")                                               # Censé remplacer les caractères 'O' et 'X'.
        self.checked = PhotoImage(file="checked.gif")                                           # Foncionne pas encore.

        ### item.Tags ##################################################
        self.tree.tag_configure(0,background='#F2F2F2',foreground='black')                      # Tag-1 Eléments Impairs
        self.tree.tag_configure(1,background="#BDBDBD",foreground='black')                      # Tag-2 Eléments Pairs
        self.tree.tag_configure(2,background="#FF6600",foreground='black')                      # Tag-3 Eléments supprimés
        
        ### Bind #######################################################
        self.EntryPath.bind('<Return>',lambda event:self.Launch())                              # "Entrée" lance la recherche (comme le bouton lancer).
        self.EntryPath.bind('<Button-1>',lambda event:self.Path())                              # Cliquer dans la zone Entry lance le selecteur de dossier.
        self.tree.bind('<<TreeviewSelect>>',lambda event:self.Select())                         # Cliquer sur un item changera sa valeur -> val ? O : X.
        self.tree.bind('<Double-Button-1>',lambda event:self.OpenDir())                         # Un double clique sur un item ouvrira son emplacement dans son dossier et mettra le focus sur lui.

    def Insert (self) : # Remplis self.tree
        Row = list()
        group = 1
        for i in self.app.doublon :
            for a in i[1] :  
                F = File(a,ext=1)
                Row+=[['O', F.Name, group,
                       ' '+F.Dir, self.ParseOctect(F.len),
                       time.strftime("%H:%M:%S %d/%b/%Y",time.localtime(F.LastChange))]]
            group += 1

        for item in Row :
            if item[2]%2==0:d=0 
            else : d=1
            self.tree.insert('', 'end', values=item,tags=d)#,image=self.check)

        info = self.app.Info() # Récupère les informations de la session Duplicate (self.app)

        self.Info_file_Str.set(info[0])
        self.Info_Original_Str.set(info[1])
        self.Info_Size_Str.set('{0} + {1}'.format(self.ParseOctect(info[2]-info[3]),self.ParseOctect(info[3])))
        
        #self.app.Close()   # vérifier si Supprimer les variables est plus ou moins lent.
        self.bell()         # Jouer un son quand l'exécution est finie.
        
    def Path (self):
        """
        Si le thread n'est pas déjà actif
        on affiche le sélecteur de dossier.
        Le résultat est affiché dans self.EntryPath
        """
        if self.d.isAlive():
            return False
        self.EntryStr.set(filedialog.askdirectory())
        return True
    
    def Launch (self):
        """
        Si le thread n'est pas actif :
        self.app.time -> chronométrer.
        Fonction récursive sur le dossier,
        Thread secondaire (principal : Tk())
        """
        if self.d.isAlive():
            return False
        self.app.t = time.time()
        en = os.path.realpath(self.EntryPath.get())
        self.Reset ()
        self.EntryStr.set(en)
        if en == "":
            return 'Error'
        
        #self.app.SearchFile(en)
        self.d=threading.Thread(None,self.app.SearchDuplicate,None,[en])
        self.d.start()

    def Select (self):
        """
        Bind '<<TreeviewSelect>>'
        cliquer sur un élément alternera les valeurs 'O' - 'X',
        sauf si le fichier a été supprimé ('-')
        """
        
        item = self.tree.item(self.GetCur())["values"]
        check = item[0]

        if check == '-' : return
        elif check == 'O': check = 'X'
        else : check = 'O'
        item[0] = check
        self.tree.item(self.GetCur(), values=item)

    def ParseOctect (self,Oc) :
        """
        Parser de valeur octale.
        retourne : (\d*?,\d*)(o|Ko|Mo|Go)
        """
        if Oc < pow(10,3) :
            return '{0} o'.format(Oc)
        elif Oc > pow(10,3) and Oc < pow(10,6):
            return '{0} Ko'.format(Oc/pow(10,3))
        elif Oc >= pow(10,6) and Oc < pow(10,9):
            return '{0} Mo'.format(round(Oc/pow(10,6),3))
        else : 
            return '{0} Go'.format(round(Oc/pow(10,9),3))
        
    def GetCur (self):
        # Renvoie les items selectionnés, plus tellement utile.

        return self.tree.selection()

    def GetSelected (self) :
        # Retourne tous les éléments selectionné selon 'X'.
        checked = list()
        for i in self.tree.get_children() :
            if self.tree.item(i)["values"][0] =='X':
                checked+=[i]
        return checked
    
    def DelSelected (self):
        """
        Récupère la liste des éléments marqué 'X'
        Fenêtre dialogue affichant le nombre et le noms des éléments à supprimer.
        L'élémetns a supprimé est marqué '-', prend la couleur de tag-2
        """
        checked = self.GetSelected()
        if len(checked)==0 :
            box.showerror(title='Erreur',
                          message='Aucun item sélectionné pour suppression.')
            return
        
        show = str()
        for i in checked :
            show += '\n   {0}'.format(self.tree.item(i)["values"][1])
        if not box.askokcancel(title='Supprimer',
                               message='Vous allez supprimer {0} éléments.{1}'.format(len(checked),show)) : return
        
        for i in checked :
            item = self.tree.item(i)["values"]
            item[0] = '-'
            self.tree.item(i, tags=2,values=item)
            try :
                os.remove(item[3][1:]+os.sep+item[1])
            except : box.showerror('Erreur - Suppression','Impossible de supprimer : {0}'.format(item[1]))

    def Reverse (self):
        # Inverse toutes les valeurs de Check selon X ? O : X
        checked = self.tree.get_children()
        for i in checked :
            item = self.tree.item(i)["values"]
            if item[0]=='O': item[0]='X'
            elif item[0]=='X' : item[0] = 'O'
            self.tree.item(i,values=item)
            
    def ResetTree (self):
        # Toutes les valeurs de Check deviennent 'O'
        checked = self.tree.get_children()
        for i in checked :
            item = self.tree.item(i)["values"]
            if item[0]=='X': item[0]='O'
            else : continue
            self.tree.item(i,values=item)

    def Auto (self):
        '''
        Choix basé sur la personnalisation du fichier,
        le nom contenant le moins de chiffres est sélectionné.
        '''
        groupe = int() #Tab-2
        self.ResetTree()
        Sel = self.tree.get_children()
        for i in Sel :
            item = self.tree.item(i)["values"]
            itemNext = self.tree.item(self.tree.next(i))["values"]
            
            if groupe == item[2] : continue # Même groupe ?
            else : groupe = item[2]

            if item[0]=='-' or itemNext[0]=='-' : continue # Déjà supprimé ?
            
            if len(re.findall('(\d)',item[1])) >\
               len(re.findall('(\d)',itemNext[1])) :
                item[0]='X'
                self.tree.item(i,values=item)
            else : 
                itemNext[0]='X'
                self.tree.item(self.tree.next(i),values=itemNext)
                
    def OpenDir (self):
        # Double cliquer sur un éléments l'affiche dans explorer
        i = self.tree.item(self.GetCur())["values"]
        Iarg = i[3][1:]+os.sep+i[1]
        os.spawnl(os.P_NOWAIT,'C:/Windows/explorer','/n','/select,"{0}"'.format(Iarg))

    def OpenFile (self):
        # Ouvre tous les fichiers selectionné dans la liste.
        check = self.GetSelected()
        if len(check) ==0 :
            box.showerror(title='Erreur',message='Aucun item sélectionné à ouvrir.')
        for i in check :
            item = self.tree.item(i)["values"]
            f = item[3][1:]
            g = item[1]
            os.startfile('"'+f+os.sep+g+'"')
    
    def Reset (self) :
        # Remet à zéro la partie graphique.
        if len(self.tree.get_children())>0:
            for i in self.tree.get_children() :
                self.tree.delete(i)
            
        self.Info_file_Str.set('')
        self.Info_Original_Str.set('')
        self.Info_Size_Str.set('')
        self.Bar_File_Str.set('')
        self.EntryStr.set('')

class Duplicate () :
    def __init__ (self):
        self.found = list()

        #self.pat = re.compile ('(.jpg|.png|.gif|.bmp)$', re.I) # re.I -> Insensible à la casse.
        self.ext = ['.jpg','.png','.gif','.bmp']
        
    def SearchFile (self,path):
        # Obsolète
        dos = os.listdir(os.path.realpath(path))
        
        for i in dos :
            if os.path.isdir(path+os.sep+i) :
                self.SearchFile(path+os.sep+i)
            elif os.path.isfile(path+os.sep+i) and re.findall(self.pat,i):
                self.found.append(path+os.sep+i)
            #else : print (i)

        return len(self.found)

    def SearchDuplicate (self,path):
        self.doublon = {}
        self.Size= int()
        self.SizeDouble = int()
        self.preRes = list()

        for root, dirs, files in os.walk(os.path.realpath(path)):
            for f in files :
                if os.path.splitext(f)[1] in self.ext:
                    self.found.append(root+os.sep+f)                                            # Ajout  à la liste des fichiers
                    Fstream = open(root+os.sep+f,'rb')                                          
                    stat = os.stat(root+os.sep+f)                                               
                    self.preRes.append([str(stat.st_size)+str(Fstream.read(100)),root+os.sep+f])# Ajout à la liste temporaire
                    Fstream.close()
                    if len(self.found)%50==0:
                        app.Info_file_Str.set('fichiers {0}'.format(len(self.found)))
        
        self.Len = len(self.found)
        
        app.Info_Size_Str.set('Ok - {0}'.format(round(time.time()-self.t,5)))
        app.Info_file_Str.set('0 sur {0}'.format(self.Len))
        
        # calcul la taille totale en attendant.
        threading.Thread(None,self.DefSize,None,()).start()
        
        app.Info_file_Str.set('Ok - {0}'.format(round(time.time()-self.t,5)))       # Affiche Ok et le temps écoulé dans la case 'Fichiers'
        app.Info_Original_Str.set('0 sur {0}'.format(self.Len))
        
        count=0
        for i in self.preRes :
            count+=1
            if count%10==0:
                app.Info_Original_Str.set('{0} % - doublons : {1}'.format(int(count/self.Len*100),len(self.doublon)))  # tous les dix éléments on affiche le pourcentage calculé.

            if i[0] in self.doublon:
                self.doublon[i[0]].append(i[1])
            else:
                self.doublon[i[0]] = [i[1]]

        self.doublon = [k for k in self.doublon.items() if len(k[1])>1]

        for i in self.doublon: # Cacule la taille totale des doublons.
            for a in i[1]:
                f1 = File(a)
                self.SizeDouble += f1.len

        app.Info_Original_Str.set('Ok - {0}'.format(round(time.time()-self.t,5)))
        
        self.LenTotal = len(self.found)
        self.found = list()
            
        app.Bar_File_Str.set('{0} seccondes'.format(round(time.time()-self.t,5)))
        
        self.t = round(time.time()-self.t,5)        
        ### statistiques #################################################
        f = open('DoublonStat.txt','a+')
        #   >> Nbr d'éléments, Nbr doublons, Taille totale, Temps total
        f.write('\n{0},\t{1},\t{2},\t{3}'.format(self.Len,len(self.doublon),self.Size,self.t))
        f.close()
        
        app.Insert() # On reprend la classe principale pour y insérer les résultats.
        return True

    def DefSize (self):
        for i in self.found :
            f1 = File(i,ext=2)
            self.Size += f1.len

    def Info (self):
        return [self.LenTotal,      # Nb fichiers
                len(self.doublon),  # Nb doublons
                self.Size,          # Taille totale
                self.SizeDouble]    # Taille doublons

    def Close (self):
        self.found= list()
         
        del (self.LenTotal,
             self.doublon,
             self.Size,
             self.SizeDouble,
             self.t,
             self.Len,
             self.preRes)
        
class File :
    """ File, Ext =None
    """
    def __init__ (self, file, ext=None):
        file = os.path.realpath(file)
        if not os.path.isfile(file):
            raise IOError("Le fichier n'existe pas.\n{0}".format(file))

        self.ext = ext
        self.stat = os.stat(file)
        
        self.mode = self.stat[stat.ST_MODE]
        self.len = self.stat.st_size                    # Taille du fichier

        if ext == 1 :
            self.Name = os.path.basename(file)          # Nom du fichier
            self.Dir = os.path.dirname(file)            # Chemin du fichier
            self.LastChange = self.stat[stat.ST_ATIME]  # Date du fichier
            #self.LastAccess = self.stat[stat.ST_MTIME]  # Date de dernier accès du fichier (pas utilisé ici)
        elif ext == 0 :
            
            if self.len > pow(10,8) :   #100 000 000
                self.hash = False       # Si le fichier est supérieur à 100 Mo on ne s'en occupe pas.
                print(file)
            else :
                self.file = open (file,'rb')
                #self.hash = hash(self.file.read())
                self.hash = str(self.len)+str(self.file.read(100))
    
def sortby(tree, col, descending):
    """Sort tree contents when a column is clicked on."""
    # grab values to sort
    data = [(tree.set(child, col), child) for child in tree.get_children('')]

    # reorder data
    data.sort(reverse=descending)
    for indx, item in enumerate(data):
        tree.move(item[1], '', indx)

    # switch the heading so that it will sort in the opposite direction
    tree.heading(col,
        command=lambda col=col: sortby(tree, col, int(not descending)))
    
if __name__ == "__main__":
    app = Interface ()
    app.title('Recherche de doublons')
    app.resizable(False,False)
    try : app.iconbitmap("ErwIco.ico")
    except : pass
    app.mainloop()

A voir également

Ajouter un commentaire

Commentaires

Messages postés
382
Date d'inscription
mercredi 23 août 2006
Statut
Membre
Dernière intervention
8 novembre 2010
11
Dans ce cas je vais créer un petit code avec la procédure de test pour voir si le problème est matériel (Win only). En effet c'est peu être du a Tk.

Je te dé-conseil l'utilisation de py 3.x pour des raisons évidente de compatibilité, et même de stabilité, il n'a pas assez de passif ... même s'il règle plein de problème entre autre l'unicode !
Messages postés
11
Date d'inscription
dimanche 25 septembre 2005
Statut
Membre
Dernière intervention
6 décembre 2008

Personnellement je préfère py3k il est mieux construit et plus logique dans sa globalité, mais il y a tellement de modules non-adaptés que ça en devient handicapant.
Ici par exemple j'aurai préféré utiliser WxPy.

je suis sur un autre projet alors j'ai un peu de mal à me remettre à celui-ci,
normalement :
self.d=threading.Thread(None,self.app.SearchDuplicate,None,[en])
self.d.start()
ceci appel en tant que thread (unique) la fonction pour rechercher les doublons, au sein de celle-ci :
threading.Thread(None,self.DefSize,None,()).start()
ce thread sert juste à définir la taille totale des fichiers doublons, si les processus suivants se termine avant ce sous-thread le résultats dans self.Info_Size_Str sera erroné.

En fait le question du nombre d'octets a utiliser est très importantes car elle influence directement la reconnaissance bien sûr, mais surtout la vitesse du programme.
Après pas mal d'essais je me suis stabilisé avec 200 octets et ça semble convenir, cela couplé avec la vérification directe de la date et de la taille permet d'éviter les erreurs.

Pour ce qui est du bug, je crois que c'est à cause de l'instabilité de Tkinter, comme je manipule pas mal de StringVar et autres de ce genre à la voléee, le module me sort parfois des erreurs et crash le programme.

Je suis effectivement capable de tester le prog sous windows, merci de ton aide.
Messages postés
382
Date d'inscription
mercredi 23 août 2006
Statut
Membre
Dernière intervention
8 novembre 2010
11
Ah ok, bug CodeS-SourceS ou erreur de ma part, je n'ai pas eu d'alerte mail !

J'ai une question : il est bien le petit Python 3 000 ?

Effectivement impossible de passer en wx sous Py3K, ça devra attendre.

Pour revenir au bug, je n'ai pas vraiment encore étudier le code, mais quand tu dis "toute la fonction est le thread", ça veut dire quoi ? Que tu créer un thread par fichier ou un thread pour tous les fichiers ?

Avec 1000 octets tu devrais être plus sure pour la vérif.

Ton bug ressemble à un problème matériel. Je vais te créer si j'ai le temps un petit script pour vérifier tout ça. Mais avant il faut que je sache si tu es capable de tester le programme sous Windows ?
Messages postés
3
Date d'inscription
samedi 5 janvier 2013
Statut
Membre
Dernière intervention
5 janvier 2013

101 ans, j'exagère un peu mais comme mon premier ordinateur était un ZX81 (cf http://fr.wikipedia.org/wiki/ZX81) j'ai l'impression que cela fait un siècle, et puis passer du code binaire en hexa à python ça fait du chemein à parcourir...

Bon, ceci étant, dans ma mandriva, j'ai du du python 3.1.1, du 2.6.4 et du 2.4.5. Une commande "python" en console indique 2.6.4, donc faisons un peu de nettoyage en éliminant le 2.4.5.
Et puis en revenant à la console je teste à tout hazard "python3" qui me répond 3.1.1, alors je rejoue en ajoutant le nom du fichier, ce qui me donne quelques messages d'erreur concernant des .gif (pb connu même si les gif en question ne servent (pour le moment) à rien:-)), un message d'erreur sur "tkinter", ben oui il manque le paquetage en 3.1.1 ! Et puis le miracle se produit.

Un test rapide sur un répertoire donne quelques doublons "pas idiots" et j'en resterai là pour ce soir !
Bonne nuit et A+
Messages postés
11
Date d'inscription
dimanche 25 septembre 2005
Statut
Membre
Dernière intervention
6 décembre 2008

P.S. : xyz33 -> si t'as des questions pour mieux comprendre le programme, j'y répondrai avec plaisir.
Afficher les 17 commentaires

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.

Du même auteur (Rano Its)