Сериализация — это преобразование объекта из памяти программы в плоскую последовательность байт или текста, пригодную для хранения в файле, отправки по сети или записи в очередь. Обратная операция называется десериализация — восстановление объекта из этой последовательности. В памяти объект живет как связка ссылок и полей по разным адресам; чтобы сохранить его на диск или отдать другой программе, эту структуру нужно «разложить» в линейный поток. Ниже разберу форматы, критерии выбора и отдельно — почему десериализация недоверенных данных опасна.
Содержание
Сериализация и десериализация — разведем термины
Эти два слова описывают направление, а не разные технологии. Держите цепочку в голове:
- объект в памяти (например
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, но не переносим на другие языки и, как показано ниже, небезопасен для чужих данных.
Как выбрать формат
Отталкивайтесь от задачи, а не от привычки:
- Данные читают люди (конфиг, лог, публичный API) — берите текстовый формат, чаще JSON.
- Важны компактность и скорость (высоконагруженный обмен, кеш) — бинарный: Protocol Buffers или MessagePack.
- Нужен строгий контракт между командами и языками — формат со схемой: Protocol Buffers или XML с XSD.
- Данные приходят извне и им нельзя доверять — формат без выполнения кода при загрузке (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, множество — массивом, объект — отдельными полями, а при загрузке собирают обратно.



