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 archivos 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 report esperado tiene cinco data rows más el header. Los groups se ordenan por size y después por digest; las paths dentro de cada group se ordenan. La tabla siguiente omite la columna sha256 porque aquí no se calcularon digests. El script rellena esa columna al ejecutarse.
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
Seis files requieren hashing: los dos empty files y los cuatro three-byte files. El four-byte file no tiene ningún size peer y se omite en la etapa de hashing. El console text esperado se obtuvo manualmente, no es un execution log.
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
2026-09-20 · ejemplo revisado manualmente · objetivo: Python 3.12 · biblioteca estándar: csv, hashlib, os, pathlib, stat · sin ejecución
Se contaron manualmente siete files: dos de size 0, cuatro de size 3 y uno de size 4, con un total de 16 bytes.
Se identificaron los identical-content groups inspeccionando las synthetic byte strings: dos empty files y tres files abc.
Se siguió el size filter hasta seis hashing candidates y el expected review report hasta dos groups que contienen cinco paths.
Se revisaron exclusive destination creation, report placement, traversal error handling y la ausencia de deletion operations.
Se obtuvieron manualmente el expected console text y el report row order.
Límites de la verificación
El código no fue ejecutado por el autor de esta respuesta; no se crearon files ni reports.
Los SHA-256 digests no se calcularon ni comprobaron. Los expected content groups se determinaron inspeccionando los synthetic inputs.
No se probaron output collisions, permission failures, concurrent changes, links, interrupted writes ni large directories.
Las reference pages no se abrieron ni comprobaron en línea.
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.