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.
Conteúdo verificado 2026.09.20Inclui arquivos de exemplo
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 relativo
Conteúdo de texto exato
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
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
Salve o próximo script como find_duplicate_files.py ao lado do setup script.
Mantenha SOURCE como outputs/duplicate_demo neste exemplo.
Mantenha outputs/duplicate_review separado do source. Essa destination ainda não deve existir.
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_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 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.
06Revise o significado, não apenas bytes correspondentes
Verifique se cada reported path ainda existe e se o source não mudou desde o scanning.
Compare os candidate files byte for byte antes de uma destructive decision. Confirme que existe um backup separado e utilizável.
Verifique folder purpose, ownership, references de outros files e application requirements. Conteúdo idêntico não estabelece interchangeability.
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
Sintoma
O que verificar
FileNotFoundError
Execute primeiro o setup e verifique SOURCE em relação ao working directory do terminal.
FileExistsError
Revise a destination anterior e escolha uma nova folder dentro de outputs. Existing empty destinations também são recusadas.
PermissionError or another read error
Resolva access problems ou reduza deliberadamente o source. Não trate um interrupted scan como completo.
File changed before or during hashing
Pare as applications que modificam o source e faça o scan novamente usando uma nova output destination.
Unexpected byte sizes
Um 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 failure
Um 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.
Percorra subpastas e registre caminhos, extensões, tamanhos e datas de modificação dos arquivos em uma tabela. Comece com 4 pequenos arquivos de exemplo, mantendo os originais e os resultados existentes intactos.
Combine em ordem arquivos CSV com a mesma estrutura de colunas e adicione uma coluna source_file. Use conjuntos de dados pequenos para verificar nomes de itens com vírgulas, colunas ausentes e saídas já existentes.