Как с помощью хеширования освободить место на диске

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

Рано или поздно место на диске склонно заканчиваться. Так однажды и произошло у меня дома: места на ноутбуке стало едва хватать для установления обновлений, а внешний диск на 1Тб заполнился на 95%. Стало очевидно - нужен новый внешний диск. Но сначала решил попробовать оптимизировать хранение файлов в текущих условиях. Бывает так, что делаешь резервную копию, а потом еще одну, но в другое время, и чтобы не рисковать потерей данных включаешь на всякий случай в список и папку, которую уже недавно копировал. Например, мой кот постоянно попадает в объектив, а потом я время от времени скидываю папку с фотографиями с телефона на жесткий диск. Таким образом появилась задача нахождения дубликатов файлов, и хорошо, что часть пространства на SSD моего ноутбука была занята не зря - пригодилась Anaconda, а для прототипа решения - стандартная Python-библиотека hashlib.

В процессе написания скрипта дополнительно возник вопрос, а какие еще (помимо хеша) атрибуты файла необходимо читать. Чтобы не усложнять и одновременно не удлинять продолжительность обработки, было решено брать только самое существенное: полное имя файла, название родительской папки, полный путь к файлу и его размер. Метод hashlib.algorithms_guaranteed возвращает список доступных алгоритмов хеширования. Среди них, в частности: 'sha256', 'sha1', 'sha512', 'md5', 'shake_256'. В дальнейшем использовался 'sha256'.

def calc_hash(file, block_size=2**18): """ Вычисление хэша для файла. Размер блока для обработки задан параметром block_size. Тип алгоритма хеширования (sha256) задан внутри. """ file_hash = hashlib.sha256() with open(file, 'rb') as f: fb = f.read(block_size) while len(fb) > 0: file_hash.update(fb) fb = f.read(block_size) return (file_hash.hexdigest())

Сведения об обработанных файлах записывались с помощью библиотеки logging в текстовый файл с разделителем точка с запятой.

def init_logging(log_name, filename, level): """ Создаем логгер для записи событий: обработки одного файла. """ logger = logging.getLogger(log_name) logger.setLevel(level) _log_format = f'%(asctime)s: [%(levelname)s]: %(message)s' _date_format = '%Y-%m-%d %H:%M:%S' file_handler = logging.FileHandler(filename, mode='w', encoding='utf8') file_handler.setLevel(level) file_handler.setFormatter(logging.Formatter(_log_format, _date_format)) logger.addHandler(file_handler) return logger

В итоге, обработав все разделы всех имеющихся у меня встроенных и переносных накопителей и выделив расширение файла в отдельный столбец для анализа, я получил таблицу, содержащую сводную информацию о своих файлах. Ключ для определения уникальности файла было решено задавать по двум текстовым столбцам -- Имя_файла + хеш_файла. Ура! Диски оказались очищены от дубликатов файлов. И фотографий котов, в частности :)

Но это -- только начало истории. Спустя некоторое время на работе возникла задача преобразования в текст большой коллекции pdf-документов. И коллега заметил, что внутри иерархической структуры папок есть похожие вложенные папки и похожие документы. При этом, похожесть внешне есть, но нет никаких гарантий, что файлы полностью идентичные. А объемы документов немаленькие -- общий размер папок измеряется гигабайтами. Т.е. возникла такая же задача поиска дубликатов в большом наборе файлов.

В этот момент вспомнилась моя домашняя наработка. После применения скрипта из Jupyter Notebook к нашей задаче общее количество документов сократилось в некоторых случаях в полтора-два раза, а для отдельных наборов данных количество файлов уменьшилось в 5-7 раз. После первых успехов в использовании в исходный скрипт были внесены доработки:

- для возможности обработки длинных имен файлов стали использоваться UNC-пути (Universal Naming Convention, универсальное соглашение об именовании). Использование префикса "\\?\" в начале полного пути к файлу позволило избежать ошибок при сочетании большой глубины вложенности папок вместе с длинными названиями файлов. Например, путь C:\Users\user-id начинает выглядеть как \\?\C:\Users\user-id и на его длину уже нет ограничения в 260 символов.

- расширен список рассматриваемых атрибутов файла: добавлены "Время создания", "Время изменения", "Время последнего доступа",

file_ctime = localtime(os.path.getctime(full_path)) # created at file_atime = localtime(os.path.getatime(full_path)) # accessed file_mtime = localtime(os.path.getmtime(full_path)) # modified

а также "Расширение файла" и "Размер в Мб"

- добавлена функция копирования только уникальных экземпляров файлов в отдельную папку с сохранением иерархической структуры папок источника с помощью функционала библиотеки pathlib.

def get_dest_name(source_f, dest_f, full_filename): """ В папке source_f находится структура, которую нужно сохранить при копировании в папку dest_f. Работаем с полным путем к одному из файлов -- full_filename. ---------- Возвращаем новое полное имя в целевой папке с учетом вложенности исходного пути. Возвращаем None, если переданы несовпадающие логические пути источника и находящегося в нем файла.""" try: path_sf = PurePath(source_f) path_file = PurePath(full_filename) second_part = path_file.relative_to(path_sf) except: # Напр, если путь к файлу не дочерний по отношению к папке-источнику return None return PurePath(dest_f).joinpath(second_part)

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

def csv_to_datafr_proc(file_in): """ Читаем путь к csv, обрабатываем, возвращаем обогащенный датафрейм. """ df = pd.read_csv(file_in, sep=';', encoding='utf8', header=None, names=['Folder', 'Full_path', 'Type', 'Size_bytes', 'Hash', 'ctime', 'atime', 'mtime']) try: # Приводим все расширения файлов к нижнему регистру df.Type = df.Type.apply(lambda x: str(x).lower()) df['Drive'] = df.Folder.apply(lambda x: PurePath(str(x)).parts[3] if len(PurePath(str(x)).parts)>3 else "-") df['File'] = df.Full_path.apply(lambda x: os.path.basename(str(x))) df['dir'] = df['Full_path'].apply(lambda x: os.path.dirname(str(x)).split('/')[-1]) # Ключ из двух полей: имя файла_хеш df['key_fn_hash'] = df['File'].astype('str') + '_' + df['Hash'] # Размер файла в Мб, округленный до десятых df['MegaBytes'] = np.round(df['Size_bytes'] / 1048576, 1) except: print('error') print('df.shape:', df.shape) return df

Преобразование csv-лога в датафрейм для последующего сохранения в формате Excel.

А полученную таблицу можно сохранить в формате Excel для удобной работы в дальнейшем:

<p>Первые несколько строк таблицы-результата, полученной в результате работы скрипта</p>

Первые несколько строк таблицы-результата, полученной в результате работы скрипта

Теперь при необходимости удаления дубликатов файлов в нашем отделе есть простой в использовании инструмент, а процесс распознавания документов значительно ускорился.

А недавно этот же скрипт я использовал для того чтобы понять, какие папки и файлы занимают ощутимую часть ограниченного на АРМ (и тем более ВАРМ) дискового пространства. Это позволило удалить старые ненужные объемные csv и убрать в архив пока еще нужные, но активно не используемые.

Ознакомиться с полной версией кода в формате Jupyter Notebook можно по ссылке.

3131 показ
1.4K1.4K открытий
Начать дискуссию