| sebb.info |
"Write programs that handle text streams, because that is the universal interface."
Creadores del sistema Unix. |
| Inicio | Informática | Letras | Links | Acerca de |
|
> Buscar archivos duplicados con bash - Algoritmo de cubetas y comparación de archivos con hash MD5.
Un script de Bash que permite encontrar archivos duplicados basándose en el hash MD5. Se implementa un algoritmo de cubetas para acelerar la búsqueda. Escribí este script para encontrar archivos duplicados de música en mis discos duros. Se puede utilizar para buscar cualquier otro tipo de archivos. La comprobación se hace por MD5, es decir que se encuentran archivos iguales a nivel binario, el hash ha de ser idéntico; no se trata de buscar archivos cuyo tamaño o nombre sean idénticos o parecidos, han de ser realmente iguales en cuanto a contenido. Se puede bucar de 6 maneras distintas:
Existen programas escritos en C y otros lenguages que implementan una técnica parecida, pero no conozco ninguno en Bash. Soy un usuario habitual de Slax, lo cual me permite usar Bash para muchas tareas fuera de mi casa. Finalmente, no veo por la red muchas implementaciones de algoritmos de cubetas en shell, así que va uno. Tales algoritmos separan los resultados en varios grupos (cubetas) para hacer más rápida la búsqueda. El script requiere el uso de Bash 3 o superior. Todo el trabajo se hace sobre matrices. Las búsquedas en cadenas no se hacen con grep sino gracias a los tests con expresiones condicionales implementados en Bash 3, lo cual es unas 20 veces más rápido. Los resultados se ven en pantalla y aparecen también en un archivo log. El algoritmo de cubetas se basa en la observación de que los hash MD5 se reparten más o menos en tres grupos idénticos:
En otra prueba con los 71755 archivos variados de un disco duro en el que está instalado Debian, obtuve los siguientes resultados:
Se observa que se reparten casi por tercios y que siempre hay más del primer tipo y menos del último. Usamos esta observación para acelerar el proceso en el script. Para realizar estas estadísticas, usé un script que se encuentra abajo de la página
#!/bin/bash
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# comparazic 0.8 permite encontrar archivos duplicados.
#
# Gracias al cómputo de un hash MD5, la comparación es exacta,
# sin error posible, o casi, ver:
# http://www.infosec.sdu.edu.cn/paper/md5-attack.pdf
#
# La búsqueda se puede efectuar en un directorio, una lista de
# directorios o una estructura de directorios con más o menos
# "profundidad".
#
# Se emplea una suerte de algoritmo de cubetas que separa en
# tres partes los hashes MD5
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# Preliminares
# # # # # # # # # # # # # # # # # # # # # #
clear
# Verificar la versión de Bash para poder usar los tests con expresiones regulares
echo "$BASH_VERSION" | egrep "^3" > /dev/null
if [ "$?" -ne "0" ] ; then
echo "Necesitas Bash versión 3 o superior para correr este script.
La versión actual es $BASH_VERSION.
Saliendo, lo siento."
exit 1
fi
empieza=`date +%s`
IFS="
"
# Archivos objetos de la búsqueda
archivos="*mp3"
[ "$archivos" = "*" ]&&iname=""||iname="-iname \"$archivos\""
# Resultados
salida="Resultados.`date +"%s"`.txt"
# Temporal
eltemporal="Temporal.`date +"%s"`.txt"
# Ruta
# inicial="/mnt/hdd5/mp3"
# inicial="/mnt/hdd5/"
inicial="/home/"
lista_subdirectorios=""
[ "$lista_subdirectorios" != "" ]&&lista_subdirectorios="$inicial $lista_subdirectorios"
# 0: No baja a subdirectorios
# 1: Baja a subdirectorios
# 2: Baja a subdirectorios $profundidad niveles
# 3: Coge lista de subdirectorios pero no busca en sus subdirectorios
# 4: Coge lista de subdirectorios y busca en sus subdirectorios
# 5: Coge lista de subdirectorios y busca $profundidad niveles en sus subdirectorios
como=1
# 0: todos / 1: cwd / 2: un nivel más / 3: dos niveles más, etc
profundidad=0
# No baja a subdirectorios
[ "$como" -eq 0 ]&&buscar='find $inicial $iname -maxdepth 1 -type f -printf "%s;%p\n"'
# Baja a subdirectorios
[ "$como" -eq 1 ]&&buscar='find $inicial $iname -type f -printf "%s;%p\n"'
# Baja a subdirectorios $profundidad niveles
[ "$como" -eq 2 ]&&buscar='find $inicial $iname -maxdepth $profundidad -type f -printf "%s;%p\n"'
# Coge lista de subdirectorios pero no busca en sus subdirectorios
[ "$como" -eq 3 ]&&buscar='find $lista_subdirectorios $iname -maxdepth 1 -type f -printf "%s;%p\n"'
# Coge lista de subdirectorios y busca en sus subdirectorios
[ "$como" -eq 4 ]&&buscar='find $lista_subdirectorios $iname -type f -printf "%s;%p\n"'
# Coge lista de subdirectorios y busca $profundidad niveles en sus subdirectorios
[ "$como" -eq 5 ]&&buscar='find $lista_subdirectorios $iname -maxdepth $profundidad -type f -printf "%s;%p\n"'
# Crea matrices con hash;inode;ruta/nombre ordenadas por 3 criterios: empieza por
# dos números, empieza por una letra o empieza por un número seguido de una letra.
# Con este orden se gana tiempo.
declare -a empiezan_por_dos_numeros
declare -a empiezan_por_un_numero_seguido_de_una_letra
declare -a empiezan_por_una_letra
declare -a duplicados
declare -a final
echo -e "Buscando duplicados en \"$inicial\"\n"
echo "***************************************************************************
* Duplicados en \"$inicial\" a `date` *
***************************************************************************\n" >> $salida
echo -e "Ubicación de archivos, cómputo MD5 y comparación de resultados.
Por favor, espere, este proceso puede tardar.\n"
# # # # # # # # # # # # # # # # # # # # # #
# Primera parte: cómputo del md5 y Separación por montoncitos.
# # # # # # # # # # # # # # # # # # # # # #
cuantos=`eval $buscar | wc -l`
echo "Se procesarán $cuantos archivos de tipo $archivos en $inicial"
echo "$cuantos archivos de tipo $archivos procesados en $inicial" >> $salida
for i in `eval $buscar`
do
hash="`echo "$i" | cut -d";" -f2-`"
hash=`md5sum $hash | cut -d" " -f1`
# Separación por montoncitos
# Los que "Empiezan por dos números"
if [[ "$hash" =~ "^[[:digit:]][[:digit:]]" ]] ; then
if [[ "${empiezan_por_dos_numeros[@]}" =~ "$hash" ]] ; then
duplicados[${#duplicados[*]}]="$hash;$i"
else
empiezan_por_dos_numeros[${#empiezan_por_dos_numeros[*]}]="$hash;$i"
fi
continue 1
fi
# Los que "Empiezan por una letra"
if [[ "$hash" =~ "^[[:alpha:]]" ]] ; then
if [[ "${empiezan_por_una_letra[@]}" =~ "$hash" ]] ; then
duplicados[${#duplicados[*]}]="$hash;$i"
else
empiezan_por_una_letra[${#empiezan_por_una_letra[*]}]="$hash;$i"
fi
continue 1
fi
# Los que "Empiezan por un número seguido de una letra"
if [[ "$hash" =~ "^[[:digit:]][[:alpha:]]" ]] ; then
if [[ "${empiezan_por_un_numero_seguido_de_una_letra[@]}" =~ "$hash" ]] ; then
duplicados[${#duplicados[*]}]="$hash;$i"
else
empiezan_por_un_numero_seguido_de_una_letra[${#empiezan_por_un_numero_seguido_de_una_letra[*]}]="$hash;$i"
fi
continue 1
fi
done
# # # # # # # # # # # # # # # # # # # # # #
# Segunda parte: encontrar los huérfanos de la primera vuelta
# # # # # # # # # # # # # # # # # # # # # #
# Agrupar los md5sum de duplicados conocidos
function agrupa {
for g in ${duplicados[@]}
do
echo "$g"
done | cut -d";" -f1 | sort -u
}
agrupados=`agrupa`
# Recuperar en cada lista el archivo único
# que se quedó huérfano después de la separación
for cada in $agrupados
do
# Si $cada empieza por dos números
if [[ "$cada" =~ "^[[:digit:]][[:digit:]]" ]] ; then
# Encuentra el único valor posible en la lista correspondiente y sale
for dosnumeros in ${empiezan_por_dos_numeros[@]}
do
echo $dosnumeros | grep $cada > /dev/null
if [ "$?" -eq "0" ] ; then
final[${#final[*]}]="$dosnumeros"
fi
done
continue 1
fi
# Si $cada empieza por una letra
if [[ "$cada" =~ "^[[:alpha:]]" ]] ; then
# Encuentra el único valor posible en la lista correspondiente y sale
for letra in ${empiezan_por_una_letra[@]}
do
echo $letra | grep $cada > /dev/null
if [ "$?" -eq "0" ] ; then
final[${#final[*]}]="$letra"
fi
done
continue 1
fi
# Si $cada empieza por un número seguido de una letra
if [[ "$cada" =~ "^[[:digit:]][[:alpha:]]" ]] ; then
# Encuentra el único valor posible en la lista correspondiente y sale
for numeroyletra in ${empiezan_por_un_numero_seguido_de_una_letra[@]}
do
echo $numeroyletra | grep $cada > /dev/null
if [ "$?" -eq "0" ] ; then
final[${#final[*]}]="$numeroyletra"
fi
done
continue 1
fi
done
# Concatena las dos matrices
todosya=( ${duplicados[@]} ${final[@]} )
# Presenta archivos duplicados
for dups in ${todosya[@]}
do
echo "$dups"
done | sort > $eltemporal
cuenta=0
total_tam_dups=0
for uu in $agrupados
do
cuenta=$((cuenta+1))
# Contar y medir archivos duplicados
tam=`grep $uu $eltemporal | head -1 | cut -d";" -f3`
tam=`ls -s $tam | cut -d" " -f1`
echo -e "\nArchivo duplicado Nº $cuenta:" >> $salida
echo -e "MD5: $uu\n$tam bytes." >> $salida
grep $uu $eltemporal | cut -d";" -f3 > $eltemporal.2
cat $eltemporal.2 >> $salida
num_dups=`wc -l $eltemporal.2 | cut -d" " -f1`
tam_dups=$(((num_dups-1)*tam))
total_tam_dups=$((total_tam_dups+tam_dups))
done
echo -e "\nHay $cuenta archivos duplicados y $total_tam_dups bytes innecesarios." >> $salida
cat $salida
echo -e "\n ==== FIN ====\n\nLos resultados se encuentran en el archivo \"$salida\"" | tee -a $salida
# Para estadísticas
#echo -e "\n\n${#empiezan_por_dos_numeros[@]} Empiezan por dos numeros\n------------------------"
#echo -e "\n\n${#empiezan_por_una_letra[@]} Empiezan_por_una_letra\n++++++++++++++++++++++++++"
#echo "
#${#empiezan_por_un_numero_seguido_de_una_letra[@]} Empiezan_por_un_numero_seguido_de_una_letra
#========================="
unset empiezan_por_dos_numeros
unset empiezan_por_un_numero_seguido_de_una_letra
unset empiezan_por_una_letra
unset duplicados
unset final
unset todosya
termina=`date +%s`
rm $eltemporal $eltemporal.2
dura=$((termina-empieza))
echo "El proceso ha durado alrededor de $dura segundos." | tee -a $salida
# ## FIN ######################################################
# Encontrar peso de archivos:
# total=0
# for i in `find /mnt/hdd5/Directorio -iname "*txt" -print0 | xargs -0 ls -la | awk -F" " '{ print $5 }'`
# do total=$((total+i))
# done
# totalKB=$((total/1024))
# totalMB=$((totalKB/1024))
# echo "$total"Bytes "- $totalKB"KB - "$totalMB"MB
Para las pruebas sobre repartición de los MD5, usé el script siguiente: #!/bin/bash find / -type f -print | xargs md5sum | tee -a listaMD5 total=`wc -l listaMD5 | cut -d" " -f1` pordosnum=0 porletra=0 pornumletra=0 for hash in `cat listaMD5` do [[ "$hash" =~ "^[[:digit:]][[:digit:]]" ]]&&pordosnum=$((pordosnum+1)) [[ "$hash" =~ "^[[:alpha:]]" ]]&&porletra=$((porletra+1)) [[ "$hash" =~ "^[[:digit:]][[:alpha:]]" ]]&&pornumletra=$((pornumletra+1)) done echo -e " Empiezan por dos números: $pordosnum - $( echo "scale=2; $pordosnum*100/$total" | bc -l)%\n \ Empiezan por una letra: $porletra - $( echo "scale=2; $porletra*100/$total" | bc -l)%\n Empiezan por un \ número y una letra: $pornumletra - $( echo "scale=2; $pornumletra*100/$total" | bc -l)%\n Total: $total archivos." |