Rozdział 23 · Część IV · Wdrażamy SafeSort

Znajdź grupy duplikatów

Jedna funkcja zamienia lista pliki w grupy potwierdzonych duplikatów i nic nie usuwa.

SafeSort · Część 4 z 6
Git i GitHubPlanowanieProjektWdrożenieTesty i CIPremiera

Za pomocą funkcji skrótu z poprzedniej strony, duplikowane wyszukiwanie grupuje pliki według rozmiar, a według digestu SHA-256. Wiele implementacji na tym kończy. SafeSort idzie o krok dalej: zanim rozważy potwierdzoną grupę, porównuje Pliki w nim są wykonane bajt po bajcie.

src/safesort/duplicates.py
def files_equal(path_a: Path, path_b: Path, chunk_size: int = DEFAULT_CHUNK_SIZE) -> bool:
    „Ostateczne potwierdzenie — zgodny skrót nie gwarantuje.”
    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

Jeden bucket o tej samej wielkości i digestie pozostaje jedynie zestawem kandydatów. Algorytm porównuje kandydata z przedstawicielem każdej już znalezionej grupy: jeśli dodaje się tam, w przeciwnym razie tworzy się nowa klasa. Dlatego nawet sztucznie przywołane Kolizja SHA-256 może wygenerować wiele niezależnych grup o dokładnej zawartości.

Test-punkt kontrolny: dwa identyczne pliki

tests/test_duplicates.py
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}
files_equal() jest taka sama zasada, jak sha256_file(): czytać w blokach
Uzgadnianie bajtów po bajcie odczytuje oba pliki w megabajtach i porównuje chunk do chunku, zamiast ładować całe pliki do pamięci — to samo rozważanie co na poprzedniej stronie o sha256_file(). Gdy tylko jeden z elementów nie pasuje, porównanie natychmiast ustaje: nie ma potrzeby dokończać czytania reszty wyraźnie różnych plików.
Puste pliki też mogą być duplikatami
Dwa pliki zerobajtowe są identyczne: po prostu nie mają bajtów. find_duplicates() nie robi wyjątków w tym przypadku — naturalnie należą do tej samej grupy pod względem wielkości (0) i do jednej grupy na podstawie pustego digestu treści. Sprawdzimy to później osobnym testem.

Rzeczywiste polecenia duplicates katalogu z trzema egzemplarzami Pary meczów:

~/safesort $ safesort duplicates ~/Downloads
Found 3 duplicate group(s):
Group 1: 2 files, 2097289 bytes each, sha256=44f70488694a224316549d0752fe6fdec636df12d58e85681eacc85e180b1f8c
Downloads/bigfile_b.dat
Downloads/bigfile_a.dat
Group 2: 2 files, 0 bytes each, sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Downloads/empty_b.bin
Downloads/empty_a.bin
Group 3: 2 files, 28 bytes each, sha256=f3b439399a805a21e826bbd4111ca4c4615e492934f53bbb2be042e7c97530cc
Downloads/copy_of_notes.txt
Downloads/notes.txt
duplicates informuje o grupach i nie usuwa plików
Pierwsza wersja programu nie usuwa żadnego pliku z znalezionej grupy, a w kodzie find_duplicates() nie ma ani jednego wywołania, które usuwa pliki, nawet wyłączone lub wykluczone. Automatyczne usuwanie duplikatów jest celowo wykraczające poza zakres pierwszej wersji: decyzja, który z identycznych plików zachować, wymaga kontekstu, którego program nie posiada.
Praktyka: Grupuj pliki w duplikaty
Interaktywny Laptop w przeglądarce: Python 3.14 przez Pyodide, bez instalacji
Otwórz praktykę →
Checkpoint · Issue #9
git commit -m "feat: add duplicate detection with byte-level confirmation"
Zlał się jak Pull Request #20. Trzy strony (23-17-23-19) książki odpowiadają jednemu PR w repozytorium: grupowanie według rozmiaru, hasza i potwierdzenia bajtów odnosi się do tego samego Issue, który jest wyjaśniony krok po kroku.
Status prawdziwego Project: Done