Находим группы дубликатов
Одна функция превращает список файлов в группы подтверждённых дубликатов и ничего не удаляет.
С хеш-функцией с предыдущей страницы поиск дубликатов группирует файлы сначала по размеру, затем по дайджесту SHA-256. На этом многие реализации останавливаются. SafeSort делает ещё один шаг: прежде чем считать группу подтверждённой, он сравнивает файлы внутри неё побайтово.
def files_equal(path_a: Path, path_b: Path, chunk_size: int = DEFAULT_CHUNK_SIZE) -> bool:
"""Финальное подтверждение — совпадающий дайджест не гарантия."""
with path_a.open("rb") as file_a, path_b.open("rb") as file_b:
while True:
chunk_a = file_a.read(chunk_size)
chunk_b = file_b.read(chunk_size)
if chunk_a != chunk_b:
return False
if not chunk_a:
return True
def find_duplicates(files: list[FileInfo]) -> list[DuplicateGroup]:
by_size = defaultdict(list)
for file in files:
by_size[file.size].append(file)
groups = []
for size, candidates in by_size.items():
if len(candidates) < 2:
continue
by_digest = defaultdict(list)
for candidate in candidates:
digest = sha256_file(candidate.path)
by_digest[digest].append(candidate)
for digest, matched in by_digest.items():
if len(matched) < 2:
continue
exact_groups = []
for candidate in matched:
for exact_group in exact_groups:
if files_equal(exact_group[0].path, candidate.path):
exact_group.append(candidate)
break
else:
exact_groups.append([candidate])
for exact_group in exact_groups:
if len(exact_group) >= 2:
groups.append(DuplicateGroup(size=size, digest=digest, files=tuple(exact_group)))
return groups
Один bucket с одинаковыми размером и дайджестом остаётся только набором кандидатов. Алгоритм сравнивает кандидата с представителем каждой уже найденной группы: при равенстве добавляет его туда, иначе создаёт новый класс. Поэтому даже искусственно вызванная коллизия SHA-256 может дать несколько независимых групп точного содержимого.
Тест-чекпойнт: два одинаковых файла
def test_equal_content_forms_one_group(tmp_path):
a = tmp_path / "a.bin"
b = tmp_path / "b.bin"
a.write_bytes(b"same")
b.write_bytes(b"same")
groups = find_duplicates(scan(tmp_path, Config()))
assert len(groups) == 1
assert {item.path for item in groups[0].files} == {a, b}
sha256_file(). Как только один из кусков не совпал, сравнение сразу останавливается: незачем дочитывать оставшуюся часть заведомо разных файлов.find_duplicates() не делает для этого случая никакого исключения — они естественно попадают в одну группу по размеру (0) и в одну группу по дайджесту пустого содержимого. Позже мы проверим это отдельным тестом.Реальный вывод команды duplicates для каталога с тремя
парами совпадений:
find_duplicates() нет ни одного вызова, который удаляет файлы, даже отключённого или закомментированного. Автоматическое удаление дубликатов сознательно вынесено за рамки первой версии: решение о том, какой из одинаковых файлов оставить, требует контекста, которого у программы нет.