Статья Кодирование/декодирование изображений и документов в формат base64 с помощью Python

Все, кто пользуется интернетом, так или иначе сталкивались с изображениями в формате base64. Но, обычно для пользователя это «столкновение» происходит незаметно, даже если он сохраняет такую картинку себе на жесткий диск. Браузер сам проводит необходимое декодирование. Другое дело, если вы используете какой-либо скрипт. В этом случае, перед сохранением изображения, его нужно декодировать. И тут уже задача полностью ложится на код и его возможности. Давайте посмотрим, как можно декодировать изображения в формате base64 с помощью Python, а также попробуем кодировать документы других типов, отличные от изображений.

000.png

Для начала давайте поймем, зачем нам это нужно. В случае с изображениями — это удобное встраивание в код. Уменьшение нагрузки на диск сервера, где лежит изображение. Так как происходит одно обращение к БД при загрузке страницы, а декодированием занимается браузер на стороне клиента. Ну и удобство хранения. Кодированное изображение или документ можно просто сохранить в базе данных в виде текста, а при необходимости достать его оттуда. Давайте для начала попробуем кодировать и декодировать документ. К примеру, PDF. Я буду делать это на статье Криса Касперски «ассемблер — экстремальная оптимизация». Она у меня сохранена в нужном формате. Ну и если это необходимо, она же будет доступна во вложении.


Что потребуется?

В данном случае установки сторонних библиотек не требуется. Все библиотеки будут использоваться «из коробки». В Python есть библиотека, которая подходит для наших целей — base64. Достаточно импортировать ее в код.

Импортируем нужные библиотеки. base64 нужна для кодирования/декодирования документов и изображений; sys — для корректного выхода из скрипта; Path — для обработки путей и имен файлов; ну и getpass — просто будем вежливыми и попрощаемся с пользователем с использованием имени его учетной записи.

Python:
import base64
import getpass
import sys
from pathlib import Path


Кодирование документа

Для того, чтобы кодировать документ в base64 необходимо учесть, что при кодировании информации о том, какое расширение имел файл до кодирования нигде не сохраняется. Поэтому, нам нужно самостоятельно позаботиться о том, чтобы указать при декодировании, в каком формате сохранять декодированный документ. А значит, при сохранении, дописывать в начало документа данные о его формате. В правильном виде это, скорее всего будет выглядеть примерно так: «data:image/gif;base64», то есть, указывать, что за приложение, какое у него расширение и то, что это формат base64.

Но я поступлю немного по «колхозному» и опущу все теги, кроме самого расширения файла. Для демонстрации этого будет вполне достаточно. Однако, если вы решите, что нужно хранить данные в таком формате, нужно будет позаботиться о том, чтобы сохранять правильный заголовок. А для этого подробнее почитать об этом. Мало ли, ваш кодированный файл решит открыть кто-то, с помощью специальной программы. Но, ближе к делу.

Создадим функцию encode_b64(ext: str, name: Path, path: str). На вход она принимает расширение файла, путь к файлу для сохранения и путь к документу, который нужно будет кодировать. Ну, а дальше все просто. Для начала открываем файл для записи в байтовом режиме, то есть, в данном случае создаем. Затем открываем файл для чтения в байтовом режиме, с помощью base64.b64encode кодируем его в нужный формат. А затем сразу же декодируем в utf-8. Добавляем в начало кодированного файла расширение и запятую. После чего сохраняем в файл, выводим в терминал сообщение о завершении кодирования и возвращаемся в функцию main.

Python:
def encode_b64(ext: str, name: Path, path: str):
    """
    Кодирование файла в base64 и сохранение в файл.
    :param ext: Расширение кодируемого файла.
    :param name: Полный путь для сохранения.
    :param path: Путь к кодируемому файлу.
    """
    with open(name, "w", encoding='utf-8') as f:
        with open(path, "rb") as file:
            f.write(f'{ext},{base64.b64encode(file.read()).decode("utf-8")}')
    print(f'Кодирование завершено. Файл сохранен: {name}')
    main()

Вот как это выглядит в терминале:

01.png


Декодирование файла

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

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

Python:
def decode_b64(name: str, path: str):
    """
    Декодирование файла из base64.
    :param name: Имя файла для декодирования без расширения.
    :param path: Путь к декодируемому файлу.
    """
    with open(path, "r", encoding='utf-8') as f:
        txt = f.read()
        if ext := txt.split(",")[0]:
            name = f'{name}.{ext}'
        dec = base64.decodebytes(txt.split(",")[1].encode())
        with open(Path(path).parent / name, 'wb') as file:
            file.write(dec)
    print(f'Декодирование завершено. Файл сохранен: {Path(path).parent / name}')
    main()

В терминале это выглядит следующим образом:

02.png


Обработка пользовательского ввода

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

Python:
def main():
    """
    Обработка пользовательского ввода.
    """
    try:
        print(f"\n{'-'*26}\nКодирование файла в base64\n{'-'*26}")
        user_ch = input("[~] Выберите действие:\n    "
                        "[1] Кодирование файла\n    "
                        "[2] Декодирование файла\n    "
                        "[3] Выход\n    "
                        ">>> ")
        if user_ch == "1":
            path = input("[~] Введите путь к кодируемому файлу: ")
            if not Path(path).suffix:
                encode_b64("", Path(path).parent / f'{Path(path).name}.dat', path)
            else:
                encode_b64(Path(path).suffix[1:],
                           Path(path).parent /
                           f'{Path(path).name.split(Path(path).suffix)[0]}.dat', path)
        elif user_ch == "2":
            path = input("[~] Введите путь к декодируемому файлу: ")
            if not Path(path).suffix:
                decode_b64(Path(path).name, path)
            else:
                decode_b64(Path(path).name.split(Path(path).suffix)[0], path)
        elif user_ch == "3":
            raise KeyboardInterrupt
        else:
            print(f'{getpass.getuser()} - ваш выбор непонятен! Еще разок...\n')
            main()
    except KeyboardInterrupt:
        print(f"\nДо свидания, {getpass.getuser()}!")
        sys.exit(0)

Python:
import base64
import getpass
import sys
from pathlib import Path


def encode_b64(ext: str, name: Path, path: str):
    """
    Кодирование файла в base64 и сохранение в файл.
    :param ext: Расширение кодируемого файла.
    :param name: Полный путь для сохранения.
    :param path: Путь к кодируемому файлу.
    """
    with open(name, "w", encoding='utf-8') as f:
        with open(path, "rb") as file:
            f.write(f'{ext},{base64.b64encode(file.read()).decode("utf-8")}')
    print(f'Кодирование завершено. Файл сохранен: {name}')
    main()


def decode_b64(name: str, path: str):
    """
    Декодирование файла из base64.
    :param name: Имя файла для декодирования без расширения.
    :param path: Путь к декодируемому файлу.
    """
    with open(path, "r", encoding='utf-8') as f:
        txt = f.read()
        if ext := txt.split(",")[0]:
            name = f'{name}.{ext}'
        dec = base64.decodebytes(txt.split(",")[1].encode())
        with open(Path(path).parent / name, 'wb') as file:
            file.write(dec)
    print(f'Декодирование завершено. Файл сохранен: {Path(path).parent / name}')
    main()


def main():
    """
    Обработка пользовательского ввода.
    """
    try:
        print(f"\n{'-'*26}\nКодирование файла в base64\n{'-'*26}")
        user_ch = input("[~] Выберите действие:\n    "
                        "[1] Кодирование файла\n    "
                        "[2] Декодирование файла\n    "
                        "[3] Выход\n    "
                        ">>> ")
        if user_ch == "1":
            path = input("[~] Введите путь к кодируемому файлу: ")
            if not Path(path).suffix:
                encode_b64("", Path(path).parent / f'{Path(path).name}.dat', path)
            else:
                encode_b64(Path(path).suffix[1:],
                           Path(path).parent /
                           f'{Path(path).name.split(Path(path).suffix)[0]}.dat', path)
        elif user_ch == "2":
            path = input("[~] Введите путь к декодируемому файлу: ")
            if not Path(path).suffix:
                decode_b64(Path(path).name, path)
            else:
                decode_b64(Path(path).name.split(Path(path).suffix)[0], path)
        elif user_ch == "3":
            raise KeyboardInterrupt
        else:
            print(f'{getpass.getuser()} - ваш выбор непонятен! Еще разок...\n')
            main()
    except KeyboardInterrupt:
        print(f"\nДо свидания, {getpass.getuser()}!")
        sys.exit(0)


if __name__ == "__main__":
    main()


Декодирование изображений с веб-страницы

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


Что потребуется?

На данном этапе потребуется установить несколько библиотек. Это bs4 для скрапинга данных, lxml — парсер, а также requests, для выполнения запросов к страницам.
Устанавливаем все нужные библиотеки с помощью команды:

pip install bs4 lxml requests

После того, как библиотеки будут установлены, импортируем в скрипт все необходимое. А потребуются нам в данном коде base64 — для декодирования изображений. sys — для корректного завершения работы скрипта. Path — для обработки путей и имен файлов. requests — для выполнения запросов к веб-страницам, ну и BeautifulSoup для скрапинга данных.

Python:
import base64
import sys
from pathlib import Path

import requests
from bs4 import BeautifulSoup

Также, чтобы у сервера не возникало вопросов, что наш скрипт делает с его страницами, используем заголовки браузера, которые будем передавать в запросе к странице:

Python:
headers = {
    "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 "
                  "YaBrowser/22.11.3.838 Yowser/2.5 Safari/537.36",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
              "application/signed-exchange;v=b3;q=0.9"
}


Получение html-кода страницы

Напишем функцию, с помощью которой будем получать html-код страницы. Назовем ее get_img(url: str) -> (str, bool). На вход она получает ссылку на страницу, а возвращает полученный текст страницы, в случае успеха или False, в случае, если страница недоступна. Или при получении статус-кода, к примеру, 403.

Делаем запрос, передаем в него ссылку и заголовки. Проверяем статус-код. Если он равен 200, возвращаем текст страницы. Если же нет, возвращаем False, также поступаем при возникновении исключения.

Python:
def get_img(url: str) -> (str, bool):
    """
    Получение текста html-страницы.
    :param url: Ссылка на страницу.
    :return: Текст страницы или False в случае исключения.
    """
    try:
        rs = requests.get(url=url, headers=headers)
        if rs.status_code == 200:
            return rs.text
        return False
    except Exception:
        return False


Поиск изображений в формате base64, декодирование и сохранение на диск

Создадим функцию finder_base64_img(txt: str), которая принимает на входе html-код страницы. Создаем объект BeautifulSoup, передаем в него полученный текст и указываем парсер, с помощью которого будем получать данные. Ищем все теги «img» на странице и итерируемся по ним в цикле. Для того, чтобы сохранять изображения с разными названиями, добавим enumerate, для получения индекса элемента из списка. Проверяем, входит ли формат расширения в список с форматами изображений. Если да, получаем расширение, пытаемся получить название изображения из тега «alt», где содержится альтернативное описание. Если такого тега нет, присваиваем имени изображения «image».

Далее декодируем текст в формате base64 и передаем в функцию для декодирования из base64 в формат изображения. Сохраняем на диск с полученным расширением.

Python:
def finder_base64_img(txt: str):
    """
    Поиск всех тегов "img"; проверка, является ли содержимое
    данного тега base64-изображением; декодирование изображения;
    сохранение в файл.
    :param txt: Текст html-страницы.
    """
    soup = BeautifulSoup(txt, "lxml")
    for num, img in enumerate(soup.find_all("img")):
        if img.get('src').split(",")[0].split(";")[0].split("/")[1] in ["png", "jpg", "jpeg", "gif", "webp"]:
            ext = img.get('src').split(",")[0].split(";")[0].split("/")[1]
            try:
                name = img.get('alt') if img.get('alt') else "image"
            except AttributeError:
                name = "image"
            img = img.get('src').split(",")[1].encode()

            with open(Path.cwd() / f'{name}{num}.{ext}', 'wb') as file:
                file.write(base64.decodebytes(img))


Обработка пользовательского ввода и запуск функций

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

Python:
def main():
    """
    Обработка пользовательского ввода.
    """
    # https://www.websiteoptimization.com/speed/tweak/inline-images/folder-test.html
    path = input("Введите ссылку на страницу с изображениями base64: ")
    if path.startswith("http"):
        if txt := get_img(path):
            finder_base64_img(txt)
        else:
            print("Не удалось получить текст страницы")
            sys.exit(0)
    else:
        print("Введенная строка не является адресом")


if __name__ == "__main__":
    main()


Для демонстрации я нашел небольшой сайт, где в формате base64 представлены изображения папок:

Вот результат работы скрипта:

03.png


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

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

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

Вложения

  • enc_dec.zip
    2,4 КБ · Просмотры: 142
  • assemble.pdf.zip
    1,2 МБ · Просмотры: 176
Мы в соцсетях:

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