Пет-проект на Python: поиск одинаковых файлов без удаления

Пет-проект на Python: поиск одинаковых файлов без удаления
Содержание статьи

Хороший небольшой пет-проект на Python можно довести до законченного сценария: передать папку, найти файлы с одинаковым содержимым и получить отчёт. В этом разборе утилита ничего не удаляет и не перемещает. Решение о том, нужна ли копия, остаётся за человеком.

Здесь будут работа с файловой системой, чтение по частям, хеширование, обработка ошибок и тесты. На Mentoring.digital можно найти наставника для разбора собственного решения. Сам пример запускается на Python 3.12 и использует только стандартную библиотеку.

Что должен делать первый вариант проекта

Дубликатом будем считать совпадение байтов, даже если имена и расширения разные. Похожие фотографии, одинаковый текст в разных кодировках и документы с разными метаданными в эту задачу не входят. Для них нужны другие правила сравнения.

  • Вход: путь к существующей папке.
  • Выход: JSON с группами одинаковых файлов и отдельным списком ошибок чтения.
  • Файлы сравниваются по содержимому. Одинаковое имя не служит доказательством совпадения.
  • Символические ссылки пропускаются. Повторные жёсткие ссылки на один объект не считаются отдельными копиями, если файловая система предоставляет идентификатор.

Для проверки создайте отдельную учебную папку. В неё положите две копии небольшого файла, другой файл того же размера и пару пустых файлов. Не начинайте эксперимент со всего диска.

Почему недостаточно сравнить имена или SHA-256

Размер позволяет быстро исключить заведомо разные файлы: у побайтово одинаковых файлов он совпадает. Только для групп с одинаковым размером вычислим SHA-256. Совпавший хеш затем подтвердим сравнением байтов. Так логика проекта не зависит от предположения, что совпадение хеша всегда доказывает равенство.

Файл читается блоками по одному мегабайту. Это не загружает его целиком в оперативную память. Список путей всё равно хранится в памяти, поэтому пример не является готовым промышленным сканером многомиллионного архива.

Код утилиты поиска дубликатов

Сохраните следующий код в duplicates.py. Функция поиска возвращает данные, а вывод JSON находится отдельно. Это позволяет проверять алгоритм без запуска командной строки.

import argparse
import hashlib
import json
import os
from collections import defaultdict
from pathlib import Path

def digest(path):
    before = path.stat()
    h = hashlib.sha256()
    with path.open("rb") as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b""):
            h.update(block)
    after = path.stat()
    if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
        raise OSError("file changed during reading")
    return h.digest()

def same_bytes(left, right):
    with left.open("rb") as a, right.open("rb") as b:
        while True:
            x, y = a.read(1024 * 1024), b.read(1024 * 1024)
            if x != y:
                return False
            if not x:
                return True

def find_duplicates(root):
    root = Path(root).absolute()
    if root.is_symlink() or not root.is_dir():
        raise ValueError("root must be an existing directory, not a symlink")
    sizes, hashes = defaultdict(list), defaultdict(list)
    errors, seen = [], set()
    def failed(error):
        errors.append(str(error))
    for folder, dirs, names in os.walk(root, onerror=failed, followlinks=False):
        dirs[:] = sorted(d for d in dirs if not (Path(folder) / d).is_symlink())
        for name in sorted(names):
            p = Path(folder) / name
            try:
                if p.is_symlink() or not p.is_file():
                    continue
                stat = p.stat()
                identity = (stat.st_dev, stat.st_ino)
                if stat.st_ino and identity in seen:
                    continue
                if stat.st_ino:
                    seen.add(identity)
                sizes[stat.st_size].append(p)
            except OSError as error:
                failed(error)
    for paths in sizes.values():
        if len(paths) < 2:
            continue
        for p in paths:
            try:
                hashes[digest(p)].append(p)
            except OSError as error:
                failed(error)
    result = []
    for candidates in hashes.values():
        groups = []
        for p in candidates:
            try:
                for group in groups:
                    if same_bytes(group[0], p):
                        group.append(p)
                        break
                else:
                    groups.append([p])
            except OSError as error:
                failed(error)
        result.extend(sorted(str(p.relative_to(root)) for p in g)
                      for g in groups if len(g) > 1)
    return {"groups": sorted(result), "errors": errors}

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("folder")
    args = parser.parse_args()
    try:
        report = find_duplicates(args.folder)
    except ValueError as error:
        parser.error(str(error))
    print(json.dumps(report, ensure_ascii=False, indent=2))

Запуск из папки с программой:

python duplicates.py ./sample

Если хотите сохранить отчёт, направьте вывод в файл вне проверяемой папки:

python duplicates.py ./sample > duplicates-report.json

Не записывайте отчёт внутрь сканируемого дерева во время его обхода: файл будет изменяться, пока программа читает данные. Пути в группах указаны относительно выбранной папки. На Windows разделитель пути может отличаться от Linux.

Какие тесты действительно проверяют алгоритм

Основные случаи: разные имена при одинаковом содержимом; одинаковый размер при разных байтах; пустые файлы; несуществующая папка. Дополнительно принудительно подменим функцию хеширования одинаковым значением: разные файлы всё равно должны остаться разными. Ошибку чтения смоделируем отдельно, чтобы проверить попадание в отчёт.

Минимальный тест положите в test_duplicates.py рядом с программой:

import tempfile
import unittest
from pathlib import Path
from duplicates import find_duplicates

class DuplicateTests(unittest.TestCase):
    def test_equal_files(self):
        with tempfile.TemporaryDirectory() as folder:
            root = Path(folder)
            (root / "a.txt").write_bytes(b"same")
            (root / "b.bin").write_bytes(b"same")
            report = find_duplicates(root)
            self.assertEqual(report["groups"], [["a.txt", "b.bin"]])
            self.assertEqual(report["errors"], [])

if __name__ == "__main__":
    unittest.main()

Команда python -m unittest -v запускает тесты из текущей папки. Добавьте остальные случаи самостоятельно: это полезнее, чем только увидеть успешный запуск одного примера.

Где заканчиваются гарантии учебной версии

Изменение размера или времени модификации во время вычисления хеша фиксируется как ошибка. Но это не атомарный снимок файловой системы: другой процесс может изменить файл между проверками. Работайте с неподвижным набором учебных данных. Для рабочей системы понадобятся отдельные решения по снимкам, гонкам, специальным файлам и особенностям ссылок.

Программа не оценивает, какую копию можно удалить. Одинаковые файлы могут быть нужны разным проектам или служить резервными копиями. Список ошибок тоже важен: пустой результат при ошибках доступа не доказывает отсутствие дубликатов.

Как показать проект в портфолио

В README опишите критерий дубликата, команду запуска, небольшой пример отчёта и ограничения. Приложите тестовые данные, которые можно свободно распространять. Объясните, зачем нужны три этапа сравнения и почему удаление не включено в первую версию.

Следующие улучшения выбирайте по конкретной проблеме: исключение папок, прогресс, ограничение размера обхода, более подробные ошибки. Не добавляйте интерфейс и многопоточность только ради списка технологий: сначала измерьте, где именно тратится время.

Если пока сложно связать функции, коллекции и файловый ввод, пригодятся курсы Python с индивидуальным планом. Когда программа уже работает, наставник по Python поможет обсудить обработку ошибок, тестируемость и следующий обоснованный шаг в развитии проекта.

Похожие статьи по данной теме