Сериализация: как объект превращается в поток байт и обратно

Сериализация: как объект превращается в поток байт и обратно Полезное

Сериализация — это преобразование объекта из памяти программы в плоскую последовательность байт или текста, пригодную для хранения в файле, отправки по сети или записи в очередь. Обратная операция называется десериализация — восстановление объекта из этой последовательности. В памяти объект живет как связка ссылок и полей по разным адресам; чтобы сохранить его на диск или отдать другой программе, эту структуру нужно «разложить» в линейный поток. Ниже разберу форматы, критерии выбора и отдельно — почему десериализация недоверенных данных опасна.

Сериализация и десериализация — разведем термины

Эти два слова описывают направление, а не разные технологии. Держите цепочку в голове:

  • объект в памяти (например dict с полями и вложенным списком) ->
  • сериализация -> поток байт/текст (строка {"name": "Sam"} или бинарный блок) ->
  • передача или хранение ->
  • десериализация -> новый объект в памяти (уже другой экземпляр, не тот же самый).

Важная граница: после round-trip вы получаете копию с теми же значениями, а не исходный объект. Ссылки на внешние ресурсы (открытый файл, сетевое соединение, поток) в общем случае не сохраняются — сериализуются данные, а не «живые» дескрипторы. Поэтому у объектов часто выделяют, какие поля идут в поток, а какие восстанавливаются заново при загрузке.

Форматы сериализации

Форматы делятся на текстовые (человекочитаемые) и бинарные (компактные и быстрые). Выбор — это компромисс между читаемостью, размером, скоростью и наличием схемы.

Формат Тип Человекочитаемый Схема Кроссязычность Типичное применение
JSON текстовый да нет (опц. JSON Schema) высокая web-API, конфиги, логи
XML текстовый да да (XSD) высокая документы, SOAP, интеграции
YAML текстовый да нет средняя конфиги, CI-пайплайны
Protocol Buffers бинарный нет да (.proto) высокая gRPC, межсервисный обмен
MessagePack бинарный нет нет высокая кеш, компактный JSON-аналог
pickle бинарный нет нет нет (только Python) локальное состояние Python

Пояснения к границам:

  • JSON покрывает базовые типы (строки, числа, булевы, массивы, объекты, null), но из коробки не знает про даты, множества или свои классы — их кодируют строкой или отдельным полем.
  • Protocol Buffers требует заранее описать схему в .proto и сгенерировать код; за это дает малый размер и строгую типизацию между сервисами на разных языках.
  • pickle удобен внутри одного проекта на Python, но не переносим на другие языки и, как показано ниже, небезопасен для чужих данных.

Как выбрать формат

Отталкивайтесь от задачи, а не от привычки:

  1. Данные читают люди (конфиг, лог, публичный API) — берите текстовый формат, чаще JSON.
  2. Важны компактность и скорость (высоконагруженный обмен, кеш) — бинарный: Protocol Buffers или MessagePack.
  3. Нужен строгий контракт между командами и языками — формат со схемой: Protocol Buffers или XML с XSD.
  4. Данные приходят извне и им нельзя доверять — формат без выполнения кода при загрузке (JSON), но не pickle.

Минимальный пример на JSON

Начнем с полного рабочего примера round-trip на стандартной библиотеке Python. Здесь json.dumps сериализует словарь в строку, json.loads восстанавливает его обратно.

import json

data = {"name": "Sam", "age": 33, "skills": ["python", "sql"]}

# сериализация: объект -> строка JSON
text = json.dumps(data, ensure_ascii=False)
print(text)
# {"name": "Sam", "age": 33, "skills": ["python", "sql"]}

# десериализация: строка JSON -> новый объект
restored = json.loads(text)
print(restored["skills"][0])
# python

Флаг ensure_ascii=False оставляет не-ASCII символы как есть, иначе они экранируются вида \uXXXX — на корректность это не влияет, только на читаемость. Чтобы писать сразу в файл, используют парную запись json.dump(data, f) и json.load(f) — без суффикса s работают с файловым объектом, а не со строкой. Это частая путаница: dumps/loads — строка, dump/load — файл.

Пример на pickle и его граница

Модуль pickle сериализует почти любой объект Python, включая экземпляры своих классов, в байты. Это удобно для локального сохранения состояния.

import pickle

blob = pickle.dumps({"name": "Sam", "age": 33})
print(type(blob))
# <class 'bytes'>

restored = pickle.loads(blob)
print(restored["age"])
# 33

Граница применимости: формат pickle специфичен для Python и версии интерпретатора, другим языком его не прочитать. И, что важнее, он небезопасен при чтении чужих данных.

Безопасность: почему нельзя загружать чужой pickle

Ключевой риск сериализации — небезопасная десериализация недоверенных данных. У JSON он минимален: парсер строит только строки, числа, списки и словари, кода не исполняет. У форматов, которые умеют восстанавливать произвольные объекты (pickle в Python, небезопасный yaml.load в PyYAML, стандартная десериализация в Java), риск прямой: подготовленный вход может привести к выполнению постороннего кода в вашем процессе. Оговорка про YAML: сам формат данных безопасен, опасен именно небезопасный загрузчик — в PyYAML для внешних данных берут yaml.safe_load, который восстанавливает только базовые типы.

В pickle за восстановление объекта отвечает метод __reduce__. Загрузчик вызывает то, что этот метод вернет. Значит, злоумышленник может подсунуть в поток вызов произвольной функции:

import pickle
import os

class Evil:
    def __reduce__(self):
        # загрузчик выполнит os.system("echo vzlom") при pickle.loads
        return (os.system, ("echo vzlom",))

payload = pickle.dumps(Evil())

# на другой стороне, где доверяют входу:
pickle.loads(payload)

При вызове pickle.loads(payload) в консоль напечатается vzlom — команда echo выполнится в момент загрузки, до того как вы хоть как-то проверили данные. Вместо безобидного echo там может стоять что угодно. Отсюда защита:

  • Не десериализуйте pickle (и другие «исполняемые» форматы), полученный по сети, из загрузок пользователя, из кеша, к которому есть чужой доступ.
  • Для обмена с внешним миром используйте формат данных без выполнения кода — JSON, а для строгих контрактов Protocol Buffers со схемой. Для YAML из внешнего источника — только yaml.safe_load, не yaml.load.
  • Если формат с исполнением неизбежен — подписывайте полезную нагрузку (HMAC на секретном ключе) и проверяйте подпись до загрузки, храните данные в доверенном хранилище с ограниченным доступом.
  • Официальная документация Python прямо предупреждает: не распаковывайте данные из ненадежного источника. Это не «best practice на будущее», а условие безопасности процесса.

То же правило шире pickle: исторические уязвимости десериализации в Java (гаджет-цепочки в библиотеках) приводили к удаленному выполнению кода ровно по этой причине — формат восстанавливал произвольные объекты из недоверенного потока.

Где применяется сериализация

  • web-API. Клиент и сервер обмениваются телами запросов и ответов, чаще в JSON; строгие межсервисные каналы (gRPC) — в Protocol Buffers.
  • Кеш. Объект кладут в Redis или Memcached в виде байт (JSON, MessagePack) и достают обратно, экономя вычисления.
  • Очереди сообщений. Задачи в RabbitMQ или Kafka передаются как сериализованные сообщения между сервисами.
  • Файлы и состояние. Настройки в YAML/JSON, сохранение прогресса приложения, снимок модели данных на диск.

Выводы

  • Сериализация превращает объект в поток байт/текст, десериализация восстанавливает из него новый объект-копию; «живые» дескрипторы (файлы, соединения) не переносятся.
  • Форматы делятся на текстовые (JSON, XML, YAML — читаемые) и бинарные (Protocol Buffers, MessagePack, pickle — компактные и быстрые).
  • Формат выбирают по задаче: читаемость и совместимость — JSON; размер и скорость — бинарные; строгий контракт — схема (Protocol Buffers, XSD).
  • Десериализация недоверенных данных опасна: pickle и Java-десериализация могут выполнить чужой код; для внешнего обмена берите JSON и не загружайте pickle из непроверенных источников.

Где применяется / связь с практикой

Сериализация — базовый навык бэкенда: без нее не построить ни API, ни кеш, ни обмен между сервисами. На практике важно не только сохранить и загрузить объект, но и выбрать безопасный формат для данных, приходящих извне. Разобрать это на живых задачах — от работы с JSON до устройства модулей стандартной библиотеки — помогает системный курс по языку.

Освойте тему на практике

Освоить Python с нуля до уверенной работы со стандартной библиотекой можно на курсе Python Basic в Otus. Посмотреть формат занятий и задать вопросы преподавателю удобно на открытых уроках Otus.

FAQ

Чем сериализация отличается от сжатия и шифрования?
Сериализация меняет представление объекта (структура в памяти -> плоский поток), не скрывая и не уменьшая данные по смыслу. Сжатие уменьшает размер потока, шифрование скрывает его содержимое. Это независимые шаги: сериализованные данные можно затем сжать и/или зашифровать.

Можно ли прочитать данные, сериализованные в одном языке, другим языком?
Зависит от формата. JSON, XML, Protocol Buffers, MessagePack кроссязычны — их читают многие языки. pickle привязан к Python и его версии, для обмена между разными языками он не подходит.

Почему JSON не сохраняет дату или множество напрямую?
Модель JSON знает только строки, числа, булевы значения, массивы, объекты и null. Даты, множества и свои классы в нее не входят, поэтому их кодируют вручную: дату — строкой в формате ISO 8601, множество — массивом, объект — отдельными полями, а при загрузке собирают обратно.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)