Scripts e automação de arquivos

Encontre arquivos duplicados por tamanho e SHA-256 antes de excluir qualquer coisa

Filtre arquivos por tamanho, compare digests SHA-256 e grave um CSV apenas para revisão sem alterar os arquivos de origem. Use um exemplo sintético com sete arquivos para verificar quais paths pertencem ao relatório e quais não.

Ver o sumário

A tradução foi feita com IA. Confira o código, as unidades e os valores junto com o original. A revisão por falantes nativos de cada idioma ainda não foi concluída. English

Para quem éEste guia é voltado a pessoas que querem identificar arquivos potencialmente redundantes mantendo as decisões de exclusão separadas do scan.

Preparação
  • Python 3.12 e um comando de terminal que inicie essa versão.
  • Uma pasta de trabalho em que você possa criar uma pasta outputs.
  • Permissão de leitura para os arquivos escaneados, sem outro processo alterá-los durante o scan.
  • É necessária apenas a biblioteca padrão do Python: csv, hashlib, os, pathlib e stat.

01Defina o que o relatório significa

Este workflow agrupa primeiro os files por byte size. Apenas size groups com vários paths precisam de hashing: files com tamanhos diferentes não podem conter exatamente os mesmos bytes. Dentro de cada size group, SHA-256 digests correspondentes identificam duplicate candidates. Matching names não são necessários nem suficientes.

Um candidate group não é uma instrução de exclusão. Conteúdo idêntico pode ter propósitos diferentes em folders diferentes, e um digest não é uma prova absoluta de igualdade. O report registra todos os matching paths sem escolher automaticamente um file para manter.

02Crie sete arquivos sintéticos

O dataset a seguir é sintético e foi criado para este artigo. Salve o código como create_duplicate_demo.py e execute-o a partir da sua working folder. Binary writes tornam o conteúdo exato: nenhum newline é adicionado, e os empty files contêm 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
Caminho relativoConteúdo de texto exatoBytes
archive/a_saved.txtabc3
empty_a.txtEmpty0
empty_b.txtEmpty0
notes/a.txtabc3
notes/a_copy.txtabc3
notes/other.txtxyz3
unique.txtsolo4

Os sete files contêm 16 bytes no total. Quatro têm size 3, mas notes/other.txt contém bytes diferentes dos três files abc. Isso testa deliberadamente por que tamanho igual, sozinho, é insuficiente.

03Mantenha o relatório fora da pasta escaneada

  1. Salve o próximo script como find_duplicate_files.py ao lado do setup script.
  2. Mantenha SOURCE como outputs/duplicate_demo neste exemplo.
  3. Mantenha outputs/duplicate_review separado do source. Essa destination ainda não deve existir.
  4. Execute o script a partir da mesma working folder usando o comando abaixo.
text
python find_duplicate_files.py

O script rejeita uma report destination dentro da resolved source folder. Isso impede que o scan inclua seu próprio output. Relative paths ainda dependem do working directory do terminal.

04Escaneie arquivos e grave o CSV de revisão

O scanner ignora symbolic links encontrados durante o traversal e ignora non-regular file entries. Traversal e read errors fazem a execução parar. Metadata comparisons em torno do hashing detectam algumas concurrent changes, mas não criam um 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()

05Compare os grupos esperados

O report esperado tem cinco data rows além do header. Os groups são ordenados por size e depois por digest; os paths dentro de cada group são ordenados. A tabela abaixo omite a coluna sha256 porque nenhum digest foi calculado aqui. O script preenche essa coluna quando executado.

group_idsize_bytesrelative_pathreview_status
G0010empty_a.txtUNREVIEWED
G0010empty_b.txtUNREVIEWED
G0023archive/a_saved.txtUNREVIEWED
G0023notes/a.txtUNREVIEWED
G0023notes/a_copy.txtUNREVIEWED

Seis files exigem hashing: os dois empty files e todos os quatro three-byte files. O four-byte file não tem size peer e é ignorado na etapa de hashing. O console text esperado foi derivado manualmente, não é um execution log.

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

06Revise o significado, não apenas bytes correspondentes

  1. Verifique se cada reported path ainda existe e se o source não mudou desde o scanning.
  2. Compare os candidate files byte for byte antes de uma destructive decision. Confirme que existe um backup separado e utilizável.
  3. Verifique folder purpose, ownership, references de outros files e application requirements. Conteúdo idêntico não estabelece interchangeability.
  4. Registre uma proposed action em uma review copy separada. Não interprete o primeiro path de um group como a cópia automaticamente preferida.

Empty files podem ser placeholders intencionais. Conteúdo correspondente não os torna desnecessários, e removê-los não eliminaria nenhum file-content byte.

07Verifique você mesmo as proteções

  • Confirme que ambos os empty files e todos os três files abc aparecem, enquanto notes/other.txt e unique.txt não aparecem.
  • Verifique se cada group tem um shared size e um shared 64-character SHA-256 hexadecimal value.
  • Execute novamente sem alterar OUTPUT_DIR. FileExistsError deve parar a execução antes que outro report seja gravado.
  • Experimente uma source separada contendo apenas unique sizes. Espere um header-only report e zero hashed files.

Estas são reader checks, não tests executados para este artigo. Um completed report com zero groups é diferente de um scan que parou com uma exception.

08Reconheça erros comuns

SintomaO que verificar
FileNotFoundErrorExecute primeiro o setup e verifique SOURCE em relação ao working directory do terminal.
FileExistsErrorRevise a destination anterior e escolha uma nova folder dentro de outputs. Existing empty destinations também são recusadas.
PermissionError or another read errorResolva access problems ou reduza deliberadamente o source. Não trate um interrupted scan como completo.
File changed before or during hashingPare as applications que modificam o source e faça o scan novamente usando uma nova output destination.
Unexpected byte sizesUm text editor pode ter adicionado newline ou alterado o encoding. Recrie o exemplo com o binary-writing setup script.
Empty folder or partial report after failureUm interrupted run não reverte sua destination. Mantenha esse result separado de completed reports.

09Entenda os limites

Hashing lê o content em chunks, mas o script armazena file metadata, paths e groups na memória. Isto não é um constant-memory inventory. Ele compara bytes completos, não visual similarity ou document meaning; documentos visualmente iguais podem ter digests diferentes.

O report conta paths, não independent physical copies. Hard links podem gerar vários reported paths sem armazenamento de conteúdo separado, portanto reported sizes não garantem disk savings. Windows junctions, mount points e hostile concurrent changes exigem tratamento adicional além dos symbolic-link checks.

Registro de execução e verificação

2026-09-20 · exemplo verificado manualmente · alvo: Python 3.12 · biblioteca padrão: csv, hashlib, os, pathlib, stat · sem execução

  • Foram contados manualmente sete files: dois de size 0, quatro de size 3 e um de size 4, totalizando 16 bytes.
  • Foram identificados os identical-content groups inspecionando as synthetic byte strings: dois empty files e três files abc.
  • Foi acompanhado o size filter até seis hashing candidates e o expected review report até dois groups contendo cinco paths.
  • Foram inspecionados exclusive destination creation, report placement, traversal error handling e a ausência de deletion operations.
  • O expected console text e a report row order foram derivados manualmente.
Limites da verificação
  • O código não foi executado pelo autor desta resposta; nenhum file ou report foi criado.
  • SHA-256 digests não foram calculados nem verificados. Os expected content groups foram determinados pela inspeção dos synthetic inputs.
  • Output collisions, permission failures, concurrent changes, links, interrupted writes e large directories não foram testados.
  • Reference pages não foram abertas nem verificadas ao vivo.

Princípios de redação e verificação de todo o site

Fontes de referência

As explicações e os exemplos são de elaboração própria. Os comportamentos e conceitos relacionados podem ser consultados nas fontes oficiais abaixo.