我們最近想要對爬蟲拿到的下載結果進行存檔,這個結果是一個Python物件(我們不想簡單的存一個HTML或json,想要讓整個下載過程可以還原) ,於是想到了用Python內建的pickle庫(醃黃瓜庫),序列化物件成bytes,需要的時候可以反序列化。
透過下面的程式碼可以簡單地了解pickle的用法和功能。
In [2]: import pickle In [3]: class A: pass In [4]: a = A() In [5]: a.foo = 'hello' In [6]: a.bar = 2 In [7]: pick_ed = pickle.dumps(a) In [8]: pick_ed Out[8]: b'\x80\x03c__main__\nA\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00fooq\x03X\x05\x00\x00\x00helloq\x04X\x03\x00\x00\x00barq\x05K\x02ub.' In [9]: unpick = pickle.loads(pick_ed) In [10]: unpick Out[10]: <__main__.A at 0x10ae67278> In [11]: a Out[11]: <__main__.A at 0x10ae67128> In [12]: dir(unpick) Out[12]: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slotnames__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo'] In [13]: unpick.foo Out[13]: 'hello' In [14]: unpick.bar Out[14]: 2
可以看到pickle的用法和json有點像,但是有幾點根本區別:
json是跨語言的通用的一種資料交換格式,一般用文字表示,人類可讀。 pickle是用來序列化Python對象,只是針對Python的,序列化的結果是二進位數據,人類不可讀。而且json預設只可以序列化一部分的內建類型,pickle可以序列化相當多的資料。
另外還有一個古老的marshal也是內建的。但這個庫主要是針對.pyc檔的。不支援自訂的類型,也不完善。例如不能處理循環應用,如果有物件引用了自己,那麼用marshal的話Python解釋器就掛了。
版本相容問題
由於pickle是針對Python的,Python有不同的版本(而且2與3之間差異非常大),所以就要考慮到序列化出來的物件能不能被更高(或低?)版本的Python反序列化出來。
目前一共有5個pickle的協定版本,版本越高對應Pyhton的版本越高,0-2針對Python2,3-4針對Python3.
Protocol version 0 is the original “human-readable” protocol and is backwards compatible with earlier versions of Python.Protocol version 1 is an old binary format which is also compatible with earlier versions of Python.Protocol version 2 was introduced in Python 2.3. It provides much more efficient pickling of new-style classes. Refer to PEP 307for information about improvements brought by protocol 2. (从这个版本往后,性能有显著提高)Protocol version 3 was added in Python 3.0. It has explicit support for bytes objects and cannot be unpickled by Python 2.x.This is the default protocol, and the recommended protocol when compatibility with other Python 3 versions is required.Protocol version 4 was added in Python 3.4. It adds support for very large objects, pickling more kinds of objects, and some data format optimizations. Refer to PEP 3154 for information about improvements brought byprotocol 4.
pickle的大多數入口函數(例如dump(),dumps(),Pickler建構器)都接受一個協定版本的參數,其中內建了兩個變數:
pickle.HIGHEST_PROTOCOL目前是4
pickle. DEFAULT_PROTOCOL目前是3
用法
和內建的json模組介面類似,dumps()用於返回序列化結果,dump()用於序列化然後寫入文件。同理也有load()和loads()。其中,序列化dump(s)的時候可以指定協定的版本,反序列化的時候就不用了,會自動辨識版本。這個和zip指令很像。
內建類型的序列化
大多數內建的類型都支援序列化和反序列化。需要特殊注意的是函數。函數的序列化只是取其名字和所在的module。函數的程式碼和屬性(Python的函數是第一等對象,可以有屬性)都不會被序列化。這就要求函數所在的模組在unpickle的環境中必須是可以import的,否則會出現ImportError或AttributeError。
這裡有個地方很有意思:所有的lambda函數都是不可Pickle的。因為它們的名字都叫做
自訂類型的序列化
如同本文開頭的實驗程式碼一樣,對我們自訂的物件在大部分情況下都不需要額外的操作就可以實現序列化/反序列化的操作。要注意的是,在反序列化的過程中,並不是呼叫class的__init__()來初始化出一個對象,而是新建出一個未被初始化的實例,然後恢復它的屬性(非常巧妙)。偽代碼如下:
def save(obj): return (obj.__class__, obj.__dict__) def load(cls, attributes): obj = cls.__new__(cls) obj.__dict__.update(attributes) return obj
如果希望在序列化的過程中做一些額外操作,例如保存物件的狀態,可以使用pickle協定的魔術方法,最常見的是__setstate__()和__getstate__( )。
安全性問題(!)
pickle文件的開頭就說:千萬不要unpickle一個未知來源的二進位。考慮下面的程式碼:
>>> import pickle >>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.") hello world 0
這段程式碼unpickle出來就是導入了os.system()然後呼叫echo。並沒有啥副作用。但如果是rm -rf /·呢?
文件給的建議是在Unpickler.find_class()裡面實作檢查邏輯。函數方法在要求全域變數的時候必然呼叫。
import builtins import io import pickle safe_builtins = { 'range', 'complex', 'set', 'frozenset', 'slice', } class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads(s): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load()
壓縮
pickle之後並不會自動壓縮的,我覺得這個設計非常好,解耦,pickle就乾pickle的事情,壓縮交給別的函式庫去做。而你自己也可以發現,pickle之後的文件儘管不可讀,但內容依然是以ascii碼呈現的,並不是亂碼。需要呼叫壓縮庫的compress。實測壓縮之後,體積是之前的1/3左右,非常可觀。
總結
全域變數要保持可以導入的,這有點難。要面對的問題是:我今天pickle的東西,將來某一天需要打開,是否還能打開呢?
這裡有幾個版本:專案的版本、python的版本,pickle協定版本,專案依賴的套件版本。其中python的版本和pickle版本我覺得可以放心依賴他們的向後相容,容易解決。主要是專案和版本和依賴的版本,如果要Pickle的物件非常複雜,那麼很可能舊版的備份無法相容新版本。可能的解決辦法就是對所有的依賴完全鎖定,例如記錄他們的hash值。如果要還原某一個二元序列,那麼就還原出當時的特定依賴、專案的特定commit。
但目前來說,我們的需求基本上就是pickle一個requests.Response物件。我覺得是可以依賴它們的向後相容的。如果有一天requests有了breaking change,那麼就算我們的pickle能相容,程式碼也不可能相容了,那時候可以考慮別的策略。
以上是Python內建的pickle庫的物件序列化與反序列化的詳細內容。更多資訊請關注PHP中文網其他相關文章!