Encontrar archivos duplicados por tamaño y SHA-256 antes de eliminar nada
Filtra los archivos por tamaño, compara los digests SHA-256 y escribe un CSV solo para revisión sin modificar los archivos de origen. Utiliza un ejemplo sintético de siete archivos para comprobar qué rutas deben aparecer en el informe y cuáles no.
Contenido revisado 2026.09.20Incluye código de ejemplo
Ver el índice
La traducción se ha realizado con IA. Comprueba el código, las unidades y los valores junto con el original. La revisión por hablantes nativos de cada idioma aún no se ha completado. English
Para quién esEsta guía está dirigida a personas que quieren identificar archivos potencialmente redundantes manteniendo las decisiones de eliminación separadas del escaneo.
Preparación
Python 3.12 y un comando de terminal que inicie esa versión.
Una carpeta de trabajo donde puedas crear una carpeta outputs.
Permiso de lectura para los archivos escaneados, sin que otro proceso los modifique durante el escaneo.
Solo se requiere la biblioteca estándar de Python: csv, hashlib, os, pathlib y stat.
01Definir qué significa el informe
Este workflow agrupa primero los files por byte size. Solo los size groups que contienen varias paths necesitan hashing: files de tamaños distintos no pueden contener exactamente los mismos bytes. Dentro de cada size group, los SHA-256 digests coincidentes identifican duplicate candidates. Que los names coincidan no es necesario ni suficiente.
Un candidate group no es una instrucción de eliminación. Un contenido idéntico puede cumplir propósitos diferentes en carpetas distintas, y un digest no es una prueba absoluta de igualdad. El report registra cada matching path sin elegir automáticamente qué file conservar.
02Crear siete archivos sintéticos
El dataset siguiente es sintético y fue creado para este artículo. Guarda el código como create_duplicate_demo.py y ejecútalo desde tu working folder. Las binary writes hacen que los contents sean exactos: no se añade newline y los empty files contienen zero bytes.
python
from pathlib import Path
DEMO = Path("outputs") / "duplicate_demo"
FILES = {
"archive/a_saved.txt": b"abc",
"empty_a.txt": b"",
"empty_b.txt": b"",
"notes/a.txt": b"abc",
"notes/a_copy.txt": b"abc",
"notes/other.txt": b"xyz",
"unique.txt": b"solo",
}
DEMO.parent.mkdir(parents=True, exist_ok=True)
DEMO.mkdir() # Refuse to reuse an existing destination.
for relative_path, content in FILES.items():
target = DEMO / relative_path
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as stream:
stream.write(content)
text
python create_duplicate_demo.py
Ruta relativa
Contenido de texto exacto
Bytes
archive/a_saved.txt
abc
3
empty_a.txt
Empty
0
empty_b.txt
Empty
0
notes/a.txt
abc
3
notes/a_copy.txt
abc
3
notes/other.txt
xyz
3
unique.txt
solo
4
Los siete files contienen 16 bytes en total. Cuatro tienen size 3, pero notes/other.txt contiene bytes diferentes de los tres files abc. Esto comprueba deliberadamente por qué el mismo tamaño por sí solo es insuficiente.
03Mantener el informe fuera de la carpeta escaneada
Guarda el siguiente script como find_duplicate_files.py junto al setup script.
Mantén SOURCE como outputs/duplicate_demo para este ejemplo.
Mantén outputs/duplicate_review separado del source. Esa destination todavía no debe existir.
Ejecuta el script desde la misma working folder usando el comando siguiente.
text
python find_duplicate_files.py
El script rechaza una report destination situada dentro de la resolved source folder. Esto evita que el scan incluya su propio output. Las relative paths siguen dependiendo del working directory de la terminal.
04Escanear archivos y escribir el CSV de revisión
El scanner omite symbolic links encontrados durante el traversal e ignora non-regular file entries. Los traversal y read errors detienen la ejecución. Las metadata comparisons alrededor del hashing detectan algunos concurrent changes, pero no crean un locked snapshot.
python
import csv
import hashlib
import os
import stat
from pathlib import Path
SOURCE = Path("outputs") / "duplicate_demo"
OUTPUT_DIR = Path("outputs") / "duplicate_review"
CHUNK_BYTES = 1024 * 1024
def signature(info: os.stat_result) -> tuple[int, ...]:
return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns)
def raise_walk_error(error: OSError) -> None:
raise error
def file_digest(path: Path, expected: os.stat_result) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
before = os.fstat(stream.fileno())
if not stat.S_ISREG(before.st_mode) or signature(before) != signature(expected):
raise RuntimeError(f"File changed before hashing: {path}")
while chunk := stream.read(CHUNK_BYTES):
digest.update(chunk)
after = os.fstat(stream.fileno())
if signature(after) != signature(expected) or signature(path.lstat()) != signature(expected):
raise RuntimeError(f"File changed during hashing: {path}")
return digest.hexdigest()
def main() -> None:
root = SOURCE.resolve(strict=True)
if not root.is_dir():
raise ValueError("SOURCE must be a directory.")
if OUTPUT_DIR.resolve().is_relative_to(root):
raise ValueError("The report folder must be outside SOURCE.")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Stop if the destination already exists.
by_size = {}
scanned = 0
for directory, directories, filenames in os.walk(
root, followlinks=False, onerror=raise_walk_error
):
base = Path(directory)
directories[:] = sorted(
name for name in directories if not (base / name).is_symlink()
)
for name in sorted(filenames):
path = base / name
info = path.lstat()
if not stat.S_ISREG(info.st_mode):
continue
by_size.setdefault(info.st_size, []).append((path, info))
scanned += 1
groups = []
hashed = 0
for size, members in sorted(by_size.items()):
if len(members) < 2:
continue
by_hash = {}
for path, initial in members:
digest = file_digest(path, initial)
by_hash.setdefault(digest, []).append(path)
hashed += 1
for digest, paths in sorted(by_hash.items()):
if len(paths) > 1:
ordered = sorted(paths, key=lambda path: path.as_posix())
groups.append((size, digest, ordered))
report = OUTPUT_DIR / "duplicates.csv"
with report.open("x", encoding="utf-8", newline="") as stream:
writer = csv.writer(stream)
writer.writerow([
"group_id", "size_bytes", "sha256", "relative_path", "review_status"
])
for number, (size, digest, paths) in enumerate(groups, start=1):
for path in paths:
writer.writerow([
f"G{number:03d}", size, digest,
path.relative_to(root).as_posix(), "UNREVIEWED"
])
matched = sum(len(paths) for _, _, paths in groups)
print(f"Scanned {scanned} regular file paths; hashed {hashed}.")
print(f"Duplicate candidates: {len(groups)} groups, {matched} paths.")
print(f"Review CSV: {report.as_posix()}")
if __name__ == "__main__":
main()
05Comparar los grupos esperados
El informe tiene cinco filas de datos más el encabezado. Los grupos se ordenan por tamaño y hash, y las rutas de cada grupo están ordenadas. La tabla omite la columna SHA-256 para facilitar la lectura; el CSV generado la incluye y sus valores se comprobaron durante la ejecución.
group_id
size_bytes
relative_path
review_status
G001
0
empty_a.txt
UNREVIEWED
G001
0
empty_b.txt
UNREVIEWED
G002
3
archive/a_saved.txt
UNREVIEWED
G002
3
notes/a.txt
UNREVIEWED
G002
3
notes/a_copy.txt
UNREVIEWED
Se calculan hashes de seis archivos: los dos vacíos y los cuatro de tres bytes. El archivo de cuatro bytes se omite porque no tiene otro del mismo tamaño. La ejecución coincidió con la salida de consola esperada.
06Revisar el significado, no solo los bytes coincidentes
Comprueba que cada reported path siga existiendo y que el source no haya cambiado desde el scanning.
Compara los candidate files byte for byte antes de tomar una destructive decision. Confirma que exista un backup independiente y utilizable.
Comprueba folder purpose, ownership, references de otros files y application requirements. Los contents idénticos no establecen interchangeability.
Registra una proposed action en una review copy separada. No interpretes la primera path de un group como la copia automáticamente preferida.
Los empty files pueden ser placeholders intencionados. Que su contenido coincida no significa que sean innecesarios, y eliminarlos no reduciría ningún file-content byte.
07Comprobar personalmente las protecciones
Confirma que aparezcan los dos empty files y los tres files abc, mientras notes/other.txt y unique.txt no aparecen.
Comprueba que cada group tenga un único shared size y un único shared 64-character SHA-256 hexadecimal value.
Vuelve a ejecutar sin cambiar OUTPUT_DIR. FileExistsError debería detener la ejecución antes de escribir otro report.
Prueba una source separada que contenga únicamente unique sizes. Espera un header-only report y zero hashed files.
Estas son reader checks, no tests ejecutados para este artículo. Un completed report con zero groups es diferente de un scan que se detuvo con una exception.
08Reconocer errores comunes
Síntoma
Qué comprobar
FileNotFoundError
Ejecuta primero el setup y comprueba SOURCE frente al working directory de la terminal.
FileExistsError
Revisa la destination anterior y elige una carpeta nueva dentro de outputs. También se rechazan las existing empty destinations.
PermissionError or another read error
Resuelve los access problems o reduce deliberadamente el source. No consideres completo un interrupted scan.
File changed before or during hashing
Detén las applications que modifican el source y vuelve a escanear usando una nueva output destination.
Unexpected byte sizes
Un text editor puede haber añadido un newline o cambiado el encoding. Recrea el ejemplo con el binary-writing setup script.
Empty folder or partial report after failure
Un interrupted run no revierte su destination. Mantén ese result separado de los completed reports.
09Comprender los límites
El hashing lee el content en chunks, pero el script almacena file metadata, paths y groups en memoria. No es un constant-memory inventory. Compara bytes completos, no visual similarity ni document meaning; documentos que parecen iguales pueden tener digests diferentes.
El report cuenta paths, no independent physical copies. Los hard links pueden producir varias reported paths sin almacenamiento de contenido separado, por lo que los reported sizes no garantizan disk savings. Windows junctions, mount points y hostile concurrent changes requieren tratamiento adicional más allá de los symbolic-link checks.
Registro de ejecución y verificación
ChatGPT Python tool · Python 3.13.5 · standard library csv/hashlib/os/pathlib/stat
La preparación creó exactamente 7 archivos de origen normales con un total de 16 bytes: dos archivos de tamaño 0, cuatro de tamaño 3 y uno de tamaño 4.
La ejecución principal imprimió exactamente: 'Scanned 7 regular file paths; hashed 6.', 'Duplicate candidates: 2 groups, 5 paths.' y 'Review CSV: outputs/duplicate_review/duplicates.csv'.
duplicates.csv contenía exactamente 5 filas de datos además de su encabezado.
G001 contenía empty_a.txt y empty_b.txt, ambos de tamaño 0 y con SHA-256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
G002 contenía archive/a_saved.txt, notes/a.txt y notes/a_copy.txt, todos de tamaño 3 y con SHA-256 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.
notes/other.txt y unique.txt no aparecieron correctamente en el informe de duplicados.
Se calcularon hashes para seis archivos: los dos archivos de cero bytes y los cuatro archivos de tres bytes; unique.txt se omitió porque no tenía otro archivo del mismo tamaño.
El contenido y los hashes de los archivos de origen eran idénticos antes y después del análisis.
Una segunda ejecución se detuvo con FileExistsError porque outputs/duplicate_review ya existía.
Una fuente con archivos de 1, 2 y 3 bytes produjo 'hashed 0', cero grupos duplicados y un informe CSV que contenía solo el encabezado.
Colocar OUTPUT_DIR dentro de SOURCE produjo ValueError: 'The report folder must be outside SOURCE.'
Al añadir un archivo de enlace simbólico y un directorio de enlace simbólico al origen, el escáner siguió informando solo 7 rutas de archivos normales y los mismos 2 grupos duplicados.
Límites de la verificación
No se probaron fallos de permisos, mutaciones simultáneas de archivos durante el cálculo de hashes, contabilización de enlaces duros, junctions o mount points de Windows, escrituras interrumpidas, condiciones de carrera hostiles del sistema de archivos ni rendimiento con directorios grandes.
Fuentes de referencia
Las explicaciones y los ejemplos son de elaboración propia. Puedes consultar los comportamientos y conceptos relacionados en las siguientes fuentes oficiales.
Recorre las subcarpetas y registra en una tabla las rutas, extensiones, tamaños y fechas de modificación de los archivos. Empieza con 4 archivos de ejemplo pequeños, manteniendo intactos los originales y los resultados existentes.
Combina en orden archivos CSV con la misma estructura de columnas y añade una columna source_file. Usa conjuntos de datos pequeños para comprobar nombres de elementos con comas, columnas faltantes y salidas ya existentes.
Compara los nombres antiguos y nuevos en una tabla y comprueba conflictos. Solo un --apply explícito crea copias en una carpeta nueva, manteniendo los originales.