Предыстория


Всем привет. Совсем недавно я столкнулся с проблемой: по необьяснимым причинам карта памяти начала забрасывать все файлы в папку 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). Вы уже сейчас можете установить модуль с помощью стандартной python'овской утилиты pip:


pip install fleep

Также на GitHub странице проекта есть примеры использования и полный список поддерживаемых форматов файлов.


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

Комментарии (13)


  1. WebConn
    28.12.2017 19:44
    +1

    К слову, в *nix для этого есть утилита file вместе с libmagic. Выполняет похожие задачи.
    А для Windows сходу нашлась утилита TrID (http://mark0.net/soft-trid-e.html), бесплатная для некоммерческого использования. Судя по описанию, умеет не только сигнатуры, но и более хитрый анализ по содержимому. Последнее обновление чуть ли не сегодня.


    1. floyer Автор
      28.12.2017 20:14

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


      1. Fox_exe
        28.12.2017 20:28

        Вообщето для этого есть уже упомянутый LibMagic, к которой есть 100500 оберток под все популярные языки.


        1. floyer Автор
          28.12.2017 20:32

          В статье я упомянул реализацию обертки этой библиотеки под Python и указал на ее минусы.


          1. semifunctional
            29.12.2017 00:32

            Минус в том, что вам не захотелось её использовать, т. к. наличие дополнительной зависимости не устроило? Звучит странно.


            1. crazylh
              29.12.2017 13:18

              Мне приходит на ум только один аргумент — сложности с дистрибуцией.


              Если для питоновских либ все более менее стандартно, то где хранить dll и кто и как будет определять какую либу надо libmagic.so libmagic.dll


      1. redfs
        29.12.2017 10:43

        Мое решение в виде модуля подходит для тех случаев, когда проблема с определением формата файла должна решаться внутри другого продукта. Как вы понимаете, в таком случае внешние утилиты не подходят.
        Что-то я совсем запутался. Ваше решение чего? Какую проблему вы решали?

        Если вы решали проблему определения формата файлов на карте памяти и их переименования (как описали в заголовке статьи), то можно сказать, что вы написали свой велосипед. Мне потребовалось менее минуты, чтобы написать однострочник на bash+file+awk+sed для решения этой проблемы. В windows я слаб, но судя по сообщению коллеги WebConn и там она решается не сложнее.

        Ну а если вы решали проблему «попрактиковаться в программировании на Python» и нашли под нее задачу, то все хорошо и понятно. Только тогда ваша статья приобетает совсем другой смысл и обсуждение как мне кажется шло бы в другом ключе, во всяком случае отсылки к готовым решениям были бы «не в тему».


  1. berez
    28.12.2017 20:04

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

    В линуксах существует прекрасная утилита 'file', которая делает ровно то, что вам нужно: определяет тип файла по содержимому. И есть готовая ее сборка для windows. Правда, она выдает слишком многословный выхлоп, но жизнь можно упростить использованием ключа -i (выдавать mime-type вместо подробного описания).
    Вызываем ее как внешнюю команду из своего скрипта, парсим выхлоп и переименовываем по мере сил. По крайней мере, она умеет отличать файлы docx от зип-архивов. :)

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

    Некоторые форматы содержат нули далеко за пределами 32 байт. Например, в файлах ISO чуть ли не 16 килобайт с начала файла — нули. :)

    по этому я стал искать

    Поэтому — вместе х2.


  1. vin2809
    28.12.2017 20:08

    А мне нравится.

    Человек же не семечки грыз, запустив «file», и даже не «Hello, World!» вывел.
    Про 32 байта, это он, надеюсь, для начала написал. Распихает основные форматы, а потом допишет и другие.


    1. floyer Автор
      28.12.2017 20:16

      Да, 32 байта выбраны лишь потому, что при работе с текущим набором форматов больше не требуется. Когда потребуется, количество байт будет увеличено.


  1. Nokta_strigo
    28.12.2017 21:49

    Наполнение словаря сигнатур в таком проекте — самая сложная, кропотливая и долгая работа. Было бы логично не создавать свой набор сигнатур с нуля, а взять их из того же file/libmagic (их там очень много). Тогда внося новые сигнатуры можно было бы и в развитии libmagic помочь. Только надо посмотреть как лицензии соотносятся.
    P.S. Тут вроде получилось завести python-magic под Windows.


  1. ivashkevitch
    31.12.2017 08:08

    Все ок, спасибо за статью, интересно. Только "по этому" в обоих случаях пишется слитно. Исправьте, пожалуйста :)


  1. iklin
    31.12.2017 12:50

    Статья напомнила, что я подобную штуку лет 20 назад (в школе ещё) на борландовском Паскале писал. :) Типы архивов распознавал.
    Это я не к тому, что вона какой я старый и что статья фуфло. Вовсе нет. Просто напомнило.