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

Вполне возможно, что при разработке приложений вам может понадобиться определение типа файла. И не всегда тип файла можно узнать по расширению. Если в ОС Linux это не составляет больших проблем, так как данная операционная система распознает тип файла не по расширению, а по содержимому, то вот в Windows отсутствие расширения иногда вызывает множество вопросов. Давайте попробуем понять, как можно определить тип файла с помощью Python.

68416866.jpg


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

Из сторонних модулей можно выделить magic. С ее помощью довольно точно можно узнать mime-тип файла. Вот только работа данного модуля завязана на библиотеку libmagic1. То есть, по сути это просто оболочка вокруг данной библиотеки. И для работы модуля требуется ее наличие. И если в Linux зачастую она установлена по умолчанию, то вот в Windows понадобиться установить библиотеки DLL. Давайте чуть подробнее рассмотрим, что нужно для того, чтобы работать с данным модулем, его установку и требования.


Что понадобиться?

Для начала необходимо установить сам модуль. Поэтому пишем в терминале команду:

pip install python-magic

В принципе, если вы работаете в Linux, установки библиотеки libmagic1 может и не понадобиться. К примеру, в Linux Mint данная библиотека установлена «из коробки». Однако, если, все же, у вас ее нет, то установка библиотеки в Ubuntu/Debian делается командой:

sudo apt-get install libmagic1

Если вы работаете в операционной системе Windows, то нужно установить DLL с помощью команды:

pip install python-magic-bin

В принципе, на этом все. Для работы модуля больше ничего особо не требуется.


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

Давайте напишем маленький скрипт, для примера, чтобы понять, как работает данная библиотека.
Импортируем в скрипт нужные модули:

Python:
from pathlib import Path

import magic

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

Python:
def mime_magic(path):
    print(magic.Magic(mime=True).from_file(path))

И теперь, в функции main, вызовем данную функцию и передадим в нее путь к файлу.

Python:
def main():
    path = input("Введите путь к файлу: ")
    if not Path(path).exists():
        print("Файла не существует!")
    mime_magic(path)


if __name__ == "__main__":
    main()

Python:
from pathlib import Path

import magic


def mime_magic(path):
    print(magic.Magic(mime=True).from_file(path))


def main():
    path = input("Введите путь к файлу: ")
    if not Path(path).exists():
        print("Файла не существует!")
    mime_magic(path)


if __name__ == "__main__":
    main()

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

01.png


Однако, как я уже писал ранее, данный модуль — это только обертка python над библиотекой libmagic1. И здесь, если вы пишите переносимое приложение, придется тащить за собой все остальные зависимости. Проверять, установлена ли библиотека или, в случае с Windows устанавливать библиотеки DLL.

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


Определение типа файла по его сигнатуре

Понятие сигнатура файла известно так же как «магическое число». Это целочисленная или текстовая константа, с помощью которой можно однозначно идентифицировать ресурс или данные. Само по себе, это число не несет никакого смысла. Примером такого магического числа может служить исполняемый файл Windows с расширением .exe. Он начинается с последовательности байт 0x4D5A, и это само по себе символично, так как соответствует ASCII-символам MZ, которые являются инициалами Марка Збиковски являющегося одним из создателей MS-DOS.

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

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

Для начала импортируем нужные модули в наш скрипт. Здесь нам понадобятся два стандартных модуля: sys и Path из библиотеки pathlib.

Python:
import sys
from pathlib import Path

Теперь инициализируем словарь с сигнатурами. Конечно же, если бы это был более масштабный проект, имело бы смысл вынести данные сигнатуры в отдельный модуль или json-файл. Но, так как, это лишь пример того, как определить сигнатуру, словарь маленький, а потому выносить его в отдельный модуль не имеет смысла.

Python:
signature = {
    "66 74 79 70 33 67": "3gp, 3gp2",
    "FF D8 FF E0": "jpg",
    "49 46 00 01": "jpeg",
    "89 50 4E 47 0D 0A 1A 0A": "png",
    "25 50 44 46 2D": "pdf",
    "4F 67 67 53": "ogg, oga, ogv",
    "52 49 46 46": "wav",
    "57 41 56 45": "wav",
    "41 56 49 20": "avi",
    "FF FB": "mp3",
    "FF F3": "mp3",
    "FF F2": "mp3",
    "49 44 33": "mp3",
    "66 4C 61 43": "flac",
    "1A 45 DF A3": "mkv, mka, mks, mk3d, webm",
    "47": "ts, tsv, tsa",
    "00 00 01 BA": "mpg, mpeg",
    "00 00 01 B3": "mpg, mpeg",
    "66 74 79 70 4D 53 4E 56": "mp4",
    "66 74 79 70 69 73 6F 6D": "mp4",
    "66 74 79 70 6D 70 34 32": "m4v"
}

Напишем небольшую функцию, в которой и будет происходить основное действие. Я назвал ее read_file(path). На входе она получает путь к файлу, тип которого требуется определить. А на выходе мы получаем тип файла или сообщение о невозможности определить сигнатуру.

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

Python:
    with open(path, 'rb') as f:
        file = f.read(256)
        hex_bytes = " ".join(['{:02X}'.format(byte) for byte in file])

Запустим цикл для итерации по словарю сигнатур. В данном цикле запустим еще один цикл для итерации по полученной сигнатуре с определенным смещением. Сигнатуры типов файлов, которые представлены в моем словаре имею либо нулевое, либо смещение 4 байта. С учетом пробелов, это будет 12 символов. И дальше сравниваем сигнатуру из словаря с текущим куском байт, со смещением имеющим длину текущей сигнатуры. Если сигнатура найдена, возвращаем сообщение с именем файла и его типом. Если же сигнатура не найдена — возвращаем сообщение о неизвестной сигнатуре.

Python:
        for hex_ch in signature:
            for i in [0, 12]:
                if hex_ch == str(hex_bytes[i:len(hex_ch) + i]):
                    return f'Файл: "{Path(path).name}" имеет сигнатуру: "{signature.get(hex_ch)}" файла'
                continue
        return "Неизвестная сигнатура"

Ну и функция main, в которой получаем путь к файлу и вызываем функцию read_file, в которую передаем полученный путь предварительно проверенный на существование.

Python:
def main():
    path = input("Введите путь к файлу: ")
    if not Path(path).exists():
        print("Файла не существует!")
        sys.exit(0)
    print(read_file(path))


if __name__ == "__main__":
    main()

Python:
import sys
from pathlib import Path

signature = {
    "66 74 79 70 33 67": "3gp, 3gp2",
    "FF D8 FF E0": "jpg",
    "49 46 00 01": "jpeg",
    "89 50 4E 47 0D 0A 1A 0A": "png",
    "25 50 44 46 2D": "pdf",
    "4F 67 67 53": "ogg, oga, ogv",
    "52 49 46 46": "wav",
    "57 41 56 45": "wav",
    "41 56 49 20": "avi",
    "FF FB": "mp3",
    "FF F3": "mp3",
    "FF F2": "mp3",
    "49 44 33": "mp3",
    "66 4C 61 43": "flac",
    "1A 45 DF A3": "mkv, mka, mks, mk3d, webm",
    "47": "ts, tsv, tsa",
    "00 00 01 BA": "mpg, mpeg",
    "00 00 01 B3": "mpg, mpeg",
    "66 74 79 70 4D 53 4E 56": "mp4",
    "66 74 79 70 69 73 6F 6D": "mp4",
    "66 74 79 70 6D 70 34 32": "m4v"
}


def read_file(path: str) -> str:
    """
    Получение сигнатуры файла. Итерация по словарю сигнатур и сравнение
    их с полученной сигнатурой в соответствии со смещением.

    :param path: Путь к файлу.
    :return: Строка, тип файла или сообщение о неизвестной сигнатуре.
    """
    with open(path, 'rb') as f:
        file = f.read(256)
        hex_bytes = " ".join(['{:02X}'.format(byte) for byte in file])
        for hex_ch in signature:
            for i in [0, 12]:
                if hex_ch == str(hex_bytes[i:len(hex_ch) + i]):
                    return f'Файл: "{Path(path).name}" имеет сигнатуру: "{signature.get(hex_ch)}" файла'
                continue
        return "Неизвестная сигнатура"


def main():
    path = input("Введите путь к файлу: ")
    if not Path(path).exists():
        print("Файла не существует!")
        sys.exit(0)
    print(read_file(path))


if __name__ == "__main__":
    main()

Для теста я выбрал файл mp3 без расширения и файл mp4 скачанный с YouTube. И вот что у меня получилось:


MP3-файл

02.png



M4V (MPEG-4)-файл

03.png



JPG-файл

04.png


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

А на этом пожалуй все.

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

Обучение наступательной кибербезопасности в игровой форме. Начать игру!