2024年4月20日 星期六

Python 學習筆記 : 用 pickle 讀寫二進位檔

常見的資料交換檔案格式 csv, xml, 與 json 等都是以人類可讀之字串形式儲存, 可透過相關套件的編解碼功能讀取後轉成 Python 的資料型別. Python 內建一個 pickle 套件可用來將 Python 原生資料型態以序列化 (serialized) 的方式寫入二進位檔 (通常使用 .pickle 或 .dat 為副檔名), 讀取時則依序直接還原成 Python 物件, 稱為反序列化 (先進先出).  




二進位檔若以純文字編輯軟體開啟會看到一堆亂碼. 


1. 檢視 pickle 套件內容 : 

pickle 是 Python 內建標準函式庫,可直接匯入使用 : 

>>> import pickle

使用下列自訂模組 members.py 來檢視其內容 :

# members.py
import inspect 
def varname(x): 
    return [k for k,v in inspect.currentframe().f_back.f_locals.items() if v is x][0]
def list_members(parent_obj):
    members=dir(parent_obj)
    parent_obj_name=varname(parent_obj)       
    for mbr in members:
        child_obj=eval(parent_obj_name + '.' + mbr) 
        if not mbr.startswith('_'):
            print(mbr, type(child_obj))  

將此函式存成 members.py 模組, 放在目前供作目錄下, 然後匯入其 list_members() 函式來檢視 pickle 模組 : 

>>> list_members(pickle)   
ADDITEMS <class 'bytes'>
APPEND <class 'bytes'>
APPENDS <class 'bytes'>
BINBYTES <class 'bytes'>
BINBYTES8 <class 'bytes'>
BINFLOAT <class 'bytes'>
BINGET <class 'bytes'>
BININT <class 'bytes'>
BININT1 <class 'bytes'>
BININT2 <class 'bytes'>
BINPERSID <class 'bytes'>
BINPUT <class 'bytes'>
BINSTRING <class 'bytes'>
BINUNICODE <class 'bytes'>
BINUNICODE8 <class 'bytes'>
BUILD <class 'bytes'>
BYTEARRAY8 <class 'bytes'>
DEFAULT_PROTOCOL <class 'int'>
DICT <class 'bytes'>
DUP <class 'bytes'>
EMPTY_DICT <class 'bytes'>
EMPTY_LIST <class 'bytes'>
EMPTY_SET <class 'bytes'>
EMPTY_TUPLE <class 'bytes'>
EXT1 <class 'bytes'>
EXT2 <class 'bytes'>
EXT4 <class 'bytes'>
FALSE <class 'bytes'>
FLOAT <class 'bytes'>
FRAME <class 'bytes'>
FROZENSET <class 'bytes'>
FunctionType <class 'type'>
GET <class 'bytes'>
GLOBAL <class 'bytes'>
HIGHEST_PROTOCOL <class 'int'>
INST <class 'bytes'>
INT <class 'bytes'>
LIST <class 'bytes'>
LONG <class 'bytes'>
LONG1 <class 'bytes'>
LONG4 <class 'bytes'>
LONG_BINGET <class 'bytes'>
LONG_BINPUT <class 'bytes'>
MARK <class 'bytes'>
MEMOIZE <class 'bytes'>
NEWFALSE <class 'bytes'>
NEWOBJ <class 'bytes'>
NEWOBJ_EX <class 'bytes'>
NEWTRUE <class 'bytes'>
NEXT_BUFFER <class 'bytes'>
NONE <class 'bytes'>
OBJ <class 'bytes'>
PERSID <class 'bytes'>
POP <class 'bytes'>
POP_MARK <class 'bytes'>
PROTO <class 'bytes'>
PUT <class 'bytes'>
PickleBuffer <class 'type'>
PickleError <class 'type'>
Pickler <class 'type'>
PicklingError <class 'type'>
PyStringMap <class 'NoneType'>
READONLY_BUFFER <class 'bytes'>
REDUCE <class 'bytes'>
SETITEM <class 'bytes'>
SETITEMS <class 'bytes'>
SHORT_BINBYTES <class 'bytes'>
SHORT_BINSTRING <class 'bytes'>
SHORT_BINUNICODE <class 'bytes'>
STACK_GLOBAL <class 'bytes'>
STOP <class 'bytes'>
STRING <class 'bytes'>
TRUE <class 'bytes'>
TUPLE <class 'bytes'>
TUPLE1 <class 'bytes'>
TUPLE2 <class 'bytes'>
TUPLE3 <class 'bytes'>
UNICODE <class 'bytes'>
Unpickler <class 'type'>
UnpicklingError <class 'type'>
bytes_types <class 'tuple'>
codecs <class 'module'>
compatible_formats <class 'list'>
decode_long <class 'function'>
dispatch_table <class 'dict'>
dump <class 'builtin_function_or_method'>
dumps <class 'builtin_function_or_method'>
encode_long <class 'function'>
format_version <class 'str'>
io <class 'module'>
islice <class 'type'>
load <class 'builtin_function_or_method'>
loads <class 'builtin_function_or_method'>
maxsize <class 'int'>
pack <class 'builtin_function_or_method'>
partial <class 'type'>
re <class 'module'>
sys <class 'module'>
unpack <class 'builtin_function_or_method'>
whichmodule <class 'function'>

常用的函式如下表 : 


 pickle 常用函式 說明
 dump(obj, file) 將物件 obj 寫入二進檔 file 中 (序列化)
 dumps(obj) 將物件 obj 轉成二進位 (Byte 型別) 傳回
 load(file) 從二進檔 file 依序 (先進先出)  讀取物件傳回
 loads(byte) 從 Byte 型別資料讀取, 傳回物件


其中最常用的是 load() 與 dump() 函式. 注意, 讀寫 pickle 檔時須使用 "rb" 或 "wb" 模式開啟檔案


2. 讀寫 pickle 檔 : 

呼叫 pickle.dump() 可將物件依序寫入 pickle 檔, 呼叫 pickle.load() 則按先進先出順序讀取 pickle 檔內的物件, 例如 :

>>> import pickle   
>>> with open('test.pickle', 'wb') as f:   
    pickle.dump(123, f)    
    pickle.dump([True, False], f)   
    pickle.dump({'a':1, 'b': 2}, f)   

上面程式碼依序將 123, [True, False], 與 {'a': 1, 'b': 2} 這三個物件寫入二進位檔 test.pickle,  完成後會在目前工作目錄下建立一個 test.pickle 檔, 檢視內容會發現這三個物件占用了 51 個 bytes :




如果用記事本編輯將會看到一堆亂碼 :




呼叫 pickle.load() 則可從 pickle 檔依序讀回物件 (先進先出) : 

>>> with open('test.pickle', 'rb') as f:   
    obj1=pickle.load(f)    
    obj2=pickle.load(f)   
    obj3=pickle.load(f)   
    print(obj1, type(obj1))   
    print(obj2, type(obj2))   
    print(obj3, type(obj3))   
    
123 <class 'int'>
[True, False] <class 'list'>
{'a': 1, 'b': 2} <class 'dict'>

可見呼叫 load() 時是從 pickle 檔中按先進先出順序將儲存的物件讀出還原為 Python 物件. 

pickle 檔唯一的缺點是無法得知裡面儲存了多少物件, 因此讀到檔尾時會拋出 EOFError 例外,例如上面的 pickle 檔內存有 3 個物件, 若做 4 次讀取動作就會出現例外 : 

>>> with open('test.pickle', 'rb') as f:   
    obj1=pickle.load(f)    
    obj2=pickle.load(f)   
    obj3=pickle.load(f)   
    obj4=pickle.load(f)
    print(obj1, type(obj1))   
    print(obj2, type(obj2))   
    print(obj3, type(obj3))   
    print(obj4, type(obj4))
    
Traceback (most recent call last):
  File "<pyshell>", line 5, in <module>
EOFError: Ran out of input  

因此, 保險的作法是用 try catch 捕捉讀寫例外, 例如 :

>>> objs=[]   
>>> with open('test.pickle', 'rb') as f:   
    while True:   
        try:   
            obj=pickle.load(f)   
        except Exception as e:   
            print(f'載入 {len(objs)} 個物件')   
            break    
        print(obj, type(obj))     
        objs.append(obj)    
        
123 <class 'int'>
[True, False] <class 'list'>
{'a': 1, 'b': 2} <class 'dict'>
載入 3 個物件

此例使用無窮迴圈來走訪 pickle 檔裡面讀取到的物件, 並將其存入一個串列, 當讀到檔尾拋出例外時便結束迴圈並計算已讀取的物件數量. 


3. 讀寫 Byte 資料 : 

pickle.loads() 與 pickle.dumps() 可用來讀寫 Byte 型態資料, 例如 :

>>> b1=pickle.dumps(123)   
>>> b1   
b'\x80\x04K{.'    
>>> type(b1)    
<class 'bytes'>   
>>> b2=[True, False]   
>>> b2=pickle.dumps([True, False])    
>>> b2   
b'\x80\x04\x95\x07\x00\x00\x00\x00\x00\x00\x00]\x94(\x88\x89e.'   
>>> type(b2)   
<class 'bytes'>   
>>> b3=pickle.dumps({'a':1, 'b': 2})   
>>> b3   
b'\x80\x04\x95\x11\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01a\x94K\x01\x8c\x01b\x94K\x02u.'  
>>> type(b3)     
<class 'bytes'>     

呼叫 pickle.loads() 並傳入上面用 dumps() 得到的 Bytes 物件就可以將其還原為原生的物件 :

>>> pickle.loads(b1)  
123    
>>> type(pickle.loads(b1))   
<class 'int'>
>>> pickle.loads(b2)   
[True, False]
>>> type(pickle.loads(b2))   
<class 'list'>
>>> pickle.loads(b3)    
{'a': 1, 'b': 2}   
>>> type(pickle.loads(b3))      
<class 'dict'>  

沒有留言:

張貼留言