Scripts y automatización de archivos

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.

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 relativaContenido de texto exactoBytes
archive/a_saved.txtabc3
empty_a.txtEmpty0
empty_b.txtEmpty0
notes/a.txtabc3
notes/a_copy.txtabc3
notes/other.txtxyz3
unique.txtsolo4

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

  1. Guarda el siguiente script como find_duplicate_files.py junto al setup script.
  2. Mantén SOURCE como outputs/duplicate_demo para este ejemplo.
  3. Mantén outputs/duplicate_review separado del source. Esa destination todavía no debe existir.
  4. 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_idsize_bytesrelative_pathreview_status
G0010empty_a.txtUNREVIEWED
G0010empty_b.txtUNREVIEWED
G0023archive/a_saved.txtUNREVIEWED
G0023notes/a.txtUNREVIEWED
G0023notes/a_copy.txtUNREVIEWED

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.

text
Scanned 7 regular file paths; hashed 6.
Duplicate candidates: 2 groups, 5 paths.
Review CSV: outputs/duplicate_review/duplicates.csv

06Revisar el significado, no solo los bytes coincidentes

  1. Comprueba que cada reported path siga existiendo y que el source no haya cambiado desde el scanning.
  2. Compara los candidate files byte for byte antes de tomar una destructive decision. Confirma que exista un backup independiente y utilizable.
  3. Comprueba folder purpose, ownership, references de otros files y application requirements. Los contents idénticos no establecen interchangeability.
  4. 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íntomaQué comprobar
FileNotFoundErrorEjecuta primero el setup y comprueba SOURCE frente al working directory de la terminal.
FileExistsErrorRevisa la destination anterior y elige una carpeta nueva dentro de outputs. También se rechazan las existing empty destinations.
PermissionError or another read errorResuelve los access problems o reduce deliberadamente el source. No consideres completo un interrupted scan.
File changed before or during hashingDetén las applications que modifican el source y vuelve a escanear usando una nueva output destination.
Unexpected byte sizesUn 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 failureUn 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.

Criterios de redacción y verificación de todo el sitio

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.