python

Определение формата файла с помощью Python

  • пятница, 29 декабря 2017 г. в 03:12:35
https://habrahabr.ru/post/345822/
  • Python


Предыстория


Всем привет. Совсем недавно я столкнулся с проблемой: по необьяснимым причинам карта памяти начала забрасывать все файлы в папку LOST.DIR без расширений. За долгое время там накопилось более 500 файлов разного типа: картинки, видео, аудио, документы. Самостоятельно понять формат файла было невозможным, по этому я стал искать способ решения этой проблемы программным путем.


Поиск решений


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


  1. Отсутствие поддержки со стороны разработчика
  2. Излишний функционал
  3. Отсутствие поддержки новых версий Python'a
  4. Излишняя усложненность кода

Из множества библиотек сильно выделялась python-magic (почти 1000 звезд на ГитХабе), которая является оберткой библиотеки libmagic. Но использование ее под Windows невозможно без DLL для Unix'овой библиотеки. Меня такой вариант не устроил.


Решение задачи


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


Сигнатура файла представляет собой набор байтов, обеспечивающий определение формата файла. Сигнатура имеет следующий вид в шестнадцатеричной системе счисления:


50 4D 4F 43 43 4D 4F 43

К счастью, в интернете есть два хороший сайта, на которых размещены множество сигнатур разных форматов. Целью стали самые распространенные форматы.
Как оказалось, некоторые сигнатуры подходят под разные форматы файлов, как, например, сигнатура файлов Microsoft Office. Исходя из этого, в некоторых случаях надо будет возвращать список подходящих расширений файла.


print(get("D:\\some_ms_office_document")) # выведет ['doc', 'ppt', 'xls']

Также нередко сигнатуры имеют смещение от начала файла, например, файлы мультимедийного контейнера 3GP.


1. Составление списка данных


В виде списка данных решено использовать JSON файл, с объектом 'data', значением которого будет массив объектов следующего вида:


{"format": "jpg", "offset": 0, "signature": ["FF D8 FF E0", "FF D8 FF E1", "FF D8 FF E2", "FF D8 FF E8"]}

Где:
format — формат файла;
offset — смещение сигнатуры от начала файла;
signature — массив подходящих сигнатур под указанный формат файла.


2. Написание утилиты


Импортируем необходимые модули:


import os
import json

Считываем список данных:


abspath = os.path.abspath(os.path.dirname(__file__))
data = json.loads(open(os.path.join(abspath, "data.json"), "r", encoding="utf-8").read())["data"]

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


file = open("path_to_the_file", "rb").read(32)

Если вывести переменную file, то мы увидим что-то похожее на это:


\x90\x00\x03\x00\x00\x00\x04

Теперь считанные байты надо перевести в шестнадцатеричную систему:


hex_bytes = " ".join(['{:02X}'.format(byte) for byte in file])

Далее мы создаем список, в который будут добавляться подходящие форматы:


out = []

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


for element in data:
        for signature in element["signature"]:
            offset = element["offset"]*2+element["offset"]
            if signature == hex_bytes[offset:len(signature)+offset].upper():
                out.append(element["format"])

Относительно данной строки:


offset = element["offset"]*2+element["offset"]

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


print(out) # выведет ['формат_1', 'формат_2'] или пустой массив, если совпадений не найдено

Заключение


Как оказалось, различные проектов сталкиваются с необходимостью распознавания формата файла, по этому я решил выпустить мое решение в open-source в виде модуля для Python'a под названием fleep (ссылка на страницу GitHub). Модуль поддерживает почти 60 самых распространенных форматов (полный список доступен на странице GitHub). Вы уже сейчас можете установить модуль с помощью стандартной python'овской утилиты pip:


pip install fleep

Простой пример использования:


import fleep
print(fleep.get(path="PATH_TO_THE_FILE"))

Вы также можете помочь проекту, добавляя новый функционал и новые поддерживаемые форматы файлов.


Спасибо за внимание!