sitio personal de Rodrigo Garcia Saenz.

Utilidades python 1

permalink.

Conjunto de utilidades python de pruebas 1

Actualizado - 10 September 2024

En este post compartiré un útil script en python para mostrar archivos duplicados, eliminarnos o "mantenerlos" creando un enlace simbólico para no perder el acceso y ahorrar espacio.

Es una mejora del script presentado antes en un post anterior de utilidades bash Utilidades-bash-1.

Actualización: Este script es un ejercicio, una utilidad más eficiente y recomendada para buscar y eliminar archivos duplicados en el sistema es fdupes.

La idea ahora es que además de comprobar el contenido de los archivos en un directorio y mostrar cúales son duplicados, permitir eliminar los archivos repetidos o alternativamente crear un enlace simbólico en lugar de los archivos que se han eliminado por ser repetidos.

Primero veremos una muestra conceptual de cómo funciona y luego mostraremos el script.

Gráficamente tenemos este directorio:

/tmp/dir
├── d1
│   ├── 1.pdf
│   └── docs
│       ├── 1.pdf
│       └── 90.odt
├── d2
│   ├── 4.odt
│   └── 90.odt
└── d3
    ├── docs
    │   └── 90.odt
    └── ruta.png

Aplicando el script:

python3 archivosDuplicados.py /tmp/dir

Muestra que existen tres archivos duplicados en este caso: 1.pdf se repite una vez y 90.pdf dos veces.

Eliminando duplicados

Cuando se usa con la opción -d elimina los duplicados en este caso quedaría:

/tmp/dir
├── d1
│   ├── 1.pdf
│   └── docs
├── d2
│   ├── 4.odt
│   └── 90.odt
└── d3
    ├── docs
    └── ruta.png

Creando enlaces simbólicos

Cuando se usa con la opción -s elimina los duplicados y crea un enlace simbólico a un archivo con el contenido similar.

/tmp/dir
├── d1
│   ├── 1.pdf
│   └── docs
│       ├── 1.pdf -> /tmp/dir/d1/1.pdf
│       └── 90.odt -> /tmp/dir/d2/90.odt
├── d2
│   ├── 4.odt
│   └── 90.odt
└── d3
    ├── docs
    │   └── 90.odt -> /tmp/dir/d2/90.odt
    └── ruta.png -> /tmp/dir/d2/4.odt

ArchivosDuplicados.py

El script actualizado se encuentra en https://notabug.org/strysg/duplicados.py, aquí una versión funcional:

#!/usr/bin/python3
'''
Buscador de archivos duplicados en un árbol de directorios
'''
import hashlib
import os
import sys

# Funciones de ayuda
def getList(directory="."):
''' retorna una lista con los nombres de todos los archivos dentro el 
directorio actual.
    * basado en https://stackoverflow.com/questions/120656/directory-tree-listing-in-python#120701
    '''
    files = []
    for dirname, dirnames, filenames in os.walk(directory):
        # for subdirname in dirnames:
        #     files.append(os.apth.join(dirname, subdirname))
        for filename in filenames:
            files.append(os.path.join(dirname,filename))
    return files

def digest(filename, algorithm='sha1'):
    ''' returns hexdigest of the given filename using the given algorithm '''
    with open(filename, 'r+b') as fil:
        if (algorithm == 'sha1'):
            return hashlib.sha1(fil.read()).hexdigest()
        elif (algorithm == 'sha224'):
            return hashlib.sha224(fil.read()).hexdigest()
        elif (algorithm == 'sha256'):
            return hashlib.sha256(fil.read()).hexdigest()
        elif (algorithm == 'sha384'):
            return hashlib.sha384(fil.read()).hexdigest()
        elif (algorithm == 'sha512'):
            return hashlib.sha512(fil.read()).hexdigest()
        elif (algorithm == 'md5'):
            return hashlib.md5(fil.read()).hexdigest()
        print ('Invalid algorithm')
        return ""

def digests(fileList, algorithm='sha1'):
    ''' Retorna un diccionario con los digestos calculados de la lista de
    archivos `fileList'.
    '''
    dict = {}
    for file in fileList:
        d = digest(file)
        if d in dict:
            l = dict[d]
            l.append(file)
            dict[d] = l

    for file in fileList:
        d = digest(file)
        if d in dict:
            # duplicado encontrado
            l = dict[d]
            l.append(file)
            dict[d] = l
        else:
            l = []
            l.append(file)
            dict[d] = l
    return dict

def eliminarArchivo(archivo):
    ''' elimina el archivo con ruta absoluta 
    -- return: True si lo logra.
    '''
    if os.path.exists(archivo):
        os.remove(archivo)
        return True
    else:
        print('El archivo no existe:', archivo)
        return False

def crearEnlaceSimbolico(fuente, destino):
    ''' crea un enlace simbolico que hace que `destino' apunte a `fuente' 
    -- return: True si lo logra.
    '''
    if not os.path.exists(fuente):
        print ('El archivo no existe:', fuente)
        return False
    if not os.path.exists(destino):
        print ('El archivo no existe:', destino)
        False
    try:
        os.symlink(fuente, destino)
        print ('creado enlace simbolico')
        return True
    except ex:
        print ('Excepcion generada:', str(ex))
        return False

def eliminarYCrearEnlaceSimbolico(fuente, destino):
    ''' Elimina el archivo `destino' y en el mismo directorio crea un enlace simbolico
    que apunta al archivo `fuente'.
    -- return: True si lo logra.
    '''
    if not eliminarArchivo(destino):
        return False
    if not crearEnlaceSimbolico(fuente, destino):
        return False
    return True

def use():
    print ('Obtiene un lista de archivos duplicados desde un directorio raíz')
    print ('Cada linea contiene los archivos que se ha detectado iguales')
    print ()
    print ('  python3 duplicados.py [opcion] [DIR] [ALGORITMO]')
    print ()
    print (' - DIR: Directorio raíz donde realizar la búsqueda usa "." por defecto')
    print (' - ALGORITMO: Algoritmo para obtener digestos "sha1" por defecto')
    print ('              permitidos; md5,sha1,sha224,sha256,sha384,sha512')
    print (' opcion:')
    print ('   d: Elimina los archivos duplicados dejando solo un archivo fuente.')
    print ('   s: Elimina los archivos duplicados y crea un enlace simbolico hacia el archivo fuente en lugar de los archivos eliminados.')
    print ('EJEMPLO')
    print ('    python3 duplicados.py /tmp/dir1')
    print ('    python3 duplicados.py -d /tmp/dir1')
    print ('    python3 duplicados.py -s /tmp/dir1')

################################ main ####################
files = []
directory='.'
hashAlgorithm = "sha1"

eliminar_duplicados = False
crear_enlaces_simbolicos = False

# Examinando opciones introducidas al llamar al script
if (len(sys.argv) > 1):
    if (sys.argv[1] != ''):
        if (sys.argv[1]=='-h' or sys.argv[1]=='--help'):
            use()
            exit(0)
        if (sys.argv[1]=='-d'):
            eliminar_duplicados = True
            directory=sys.argv[2]
        elif (sys.argv[1]=='-s'):
            crear_enlaces_simbolicos = True
            directory=sys.argv[2]
        else:
            directory=sys.argv[1]

# Comprobando si se especifica algoritmo para obtener digestos
if (len(sys.argv) > 3):
    if (eliminar_duplicados or crear_enlaces_simbolicos and sys.argv[3] != ''):
        if (sys.argv[3]=='sha1' or sys.argv[3]=='md5' or sys.argv[3]=='sha224'
            or sys.argv[3]=='sha256' or sys.argv[3]=='sha512'):
            hashAlgorithm = sys.argv[3]
        else:
            use()
            exit(0)

############## Programa principal
files = getList(directory)
digests = digests(files, hashAlgorithm)
duplicados = 0
completados = []
erroneos = []

# Examinando la lista de digestos y archivos
for digest, lista in digests.items():
    if len(lista) > 1:
        # significa que hay archivos duplicados para este digesto calculado
        listaDuplicados = []
        duplicados += 1
        fuente = lista[0]
        c = 0
        for file in lista:
            listaDuplicados.append(file)
            if eliminar_duplicados and c > 0:
                if eliminarArchivo(file):
                    completados.append(file)
                else:
                    erroneos.append(file)
            if crear_enlaces_simbolicos and c > 0:
                if eliminarYCrearEnlaceSimbolico(fuente, file):
                    completados.append(file)
                else:
                    erroneos.append(file)
            c += 1
        # mostrando
        for file in listaDuplicados:
            print (file)
        print (len(listaDuplicados))
        print ()

    print ('')
    print ('# completados satisfactoriamente #')
    for file in completados:
        print (file)
    print ('')
    print ('# errores generados #') 
    for file in erroneos:
        print (file)
    print ('----')
    print (' - Total Duplicados:', str(duplicados))
    print (' - Completados:', str(len(completados)))
    print (' - Erroneos:', str(len(erroneos)))
    exit(0)

La utilidad de este script se puede aplicar a directorios grandes ya que examina los subdirectorios también. Sin embargo hay que considerar que como por cada archivo y para comprobar que está o no repetido, se comprueba su contenido ignorando su nombre. La velocidad del cálculo del digesto depende del algoritmo que se use por defecto se usa el algoritmo sha1.

Consejo del día

Apoya el desarrollo y uso de software libre. El software libre respeta los derechos de quienes lo usan y quienes lo desarrollan, tiene el principio de ayudar al prójimo compartiendo tecnología y conocimiento.