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()
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.