2024年3月23日 星期六

Python 學習筆記 : 用內建函式 hash() 與模組 hashlib 計算雜湊值

所謂雜湊 (hash) 是指透過一些編碼演算法 (例如 MD5, SHA1, SHA256, SHA512 等) 將資料轉成一段固定長度的雜湊值, 用來代表該資料的摘要 (digest), 由於不同的資料的雜湊值不同 (資料內容小小的變化雜湊值也會改變), 因此可用來驗證資料內容是否一致或有否被改變; 其次是無法從雜湊值逆推資料內容, 因此具有資料安全性. 

Python 內建函式 hash() 與內建模組 hashlib 來計算雜湊值, hash() 函式較簡單, 輸出的雜湊值為 9 位數的整數; 而 hashlib 模組則較複雜, 提供豐富的雜湊值演算法, 輸出值為由16 進位的 0~f  字元組成之字串, 兩者均無須安裝可直接呼叫或匯入使用.


一. 使用 hash() 函式計算雜湊值 :

hash() 可傳入一個基本型態物件 (數值, 字串, 布林值) 或元組來計算其雜湊值, 如果是整數或布林值, 其雜湊值是可預測的 (就是它們本身), 例如 : 

>>> hash(1)   
1
>>> hash(2)
2
>>> hash(3)  
3
>>> hash(123)  
123
>>> hash(False)  
0
>>> hash(True)  
1
>>> hash(1.2345e5)   
123450

傳入浮點數會傳回 9 位數的整數 : 

>>> hash(1.01)   
23058430092136961
>>> hash(1.2345e-3)   
2846563194874305

但傳入小數部分為 0 的浮點數, 其雜湊值與等值之整數一樣 :

>>> hash(1.0)  
1
>>> hash(2.0)  
2
>>> hash(2.00)   

傳入字串會傳回 9 位數整數 : 

>>> hash('abc')  
-1521791763672297916
>>> hash('abcd')   
8876414069658089904

結構型物件只能傳入元組 (tuple), 因為它是 immutable 的資料 : 

>>> hash((1, 2, 3))  
529344067295497451
>>> hash(('a', 'b', 'c'))  
-6512037586358940145
>>> hash((1.1, 2.2, 3.3))   
2823207737123308454

所以看起來只要是 immutable 的資料都可以計算雜湊值, 而像是串列, 字典, 與集合等 mutable 的物件無法計算雜湊值, 但不能說 mutable 的資料都無法計算雜湊值, 這個例外是類別, 自定義類別的物件是 mutable 的, 但只要類別有定義一個 __hash__() 方法, 則其物件仍然可以計算雜湊值, 例如 :

>>> class MyClass:  
    def __hash__(self):  
        return 1  
    
>>> a=MyClass()     
>>> hash(a)    
1

參考 : 



二. 使用 hashlib 模組計算雜湊值 :

內建模組 hashlib 提供較多的演算法來計算雜湊值, 且其輸出為十六進位字元  0~f 組成之雜湊值, 碰撞率微乎其微. 首先匯入 hashlib 用 dir() 來檢視內容 :

>>> import hashlib 
>>> dir(hashlib) 
['__all__', '__block_openssl_constructor', '__builtin_constructor_cache', '__builtins__', '__cached__', '__doc__', '__file__', '__get_builtin_constructor', '__loader__', '__name__', '__package__', '__spec__', '_hashlib', 'algorithms_available', 'algorithms_guaranteed', 'blake2b', 'blake2s', 'md5', 'new', 'pbkdf2_hmac', 'scrypt', 'sha1', 'sha224', 'sha256', 'sha384', 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', 'sha512', 'shake_128', 'shake_256']

但 dir() 只顯示模組內的成員名稱, 如果要知道哪些是函式, 哪些是類別, 可用下列自訂模組 members 之 list_members() 函式 :

# 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() 函式來檢視 openai 套件 : 

>>> from members import list_members     
>>> list_members(hashlib)    
algorithms_available <class 'set'>
algorithms_guaranteed <class 'set'>
blake2b <class 'type'>
blake2s <class 'type'>
md5 <class 'builtin_function_or_method'>
new <class 'function'>
pbkdf2_hmac <class 'builtin_function_or_method'>
scrypt <class 'builtin_function_or_method'>
sha1 <class 'builtin_function_or_method'>
sha224 <class 'builtin_function_or_method'>
sha256 <class 'builtin_function_or_method'>
sha384 <class 'builtin_function_or_method'>
sha3_224 <class 'builtin_function_or_method'>
sha3_256 <class 'builtin_function_or_method'>
sha3_384 <class 'builtin_function_or_method'>
sha3_512 <class 'builtin_function_or_method'>
sha512 <class 'builtin_function_or_method'>
shake_128 <class 'builtin_function_or_method'>
shake_256 <class 'builtin_function_or_method'>

可見 hashlib 提供了例如 md5, sha1/224/256/384 .... 等許多計算雜湊值的演算法函式, 其中較常用的式 md5() 與 sha1(). 用 hashlib 計算雜湊值需先呼叫 md5() 或 sha1() 等演算法函式來建立 HASH 物件 : 

>>> import hashlib 
>>> md5_hash=hashlib.md5()   # 建立 MD5 演算法之 HASH 物件
>>> type(md5_hash)   
<class '_hashlib.HASH'>
>>> sha1_hash=hashlib.sha1()    # 建立 SHA1 演算法之 HASH 物件
>>> type(sha1_hash)    
<class '_hashlib.HASH'>

用上面的 list_members() 來檢視 HASH 物件內容 :

>>> list_members(md5_hash)   
block_size <class 'int'>
copy <class 'builtin_function_or_method'>
digest <class 'builtin_function_or_method'>
digest_size <class 'int'>
hexdigest <class 'builtin_function_or_method'>
name <class 'str'>
update <class 'builtin_function_or_method'>

計算雜湊值會用到的 HASH 物件方法如下表 : 


 HASH 物件的常用方法 說明
 update(data) 傳入 Byte 類型資料 data 更新 HASH 物件
 digest() 計算 HASH 物件內資料之雜湊值 (傳回 Byte 型別)
 hexdigest() 計算 HASH 物件內資料之雜湊值 (傳回 16 進位數值字串)


注意, 傳入 update() 的資料必須為 Byte 類型字串, 這可在字串前面加 b'  達成, 如果是非英語系文字, 則要先呼叫字串的 encode('utf-8') 轉成 UTF-8 編碼格式, 例如 : 

>>> md5_hash.update(b'Hello World!')   # 英數字等 ASCII 字元前面冠 b' 轉成 Byte   
>>> md5_hash.digest()           # 傳回位元組形式的 MD5 雜湊值
b'\xed\x07b\x87S.\x866^\x84\x1e\x92\xbf\xc5\r\x8c'   
>>> md5_hash.hexdigest()     # 傳回字串形式的 MD5 雜湊值
'ed076287532e86365e841e92bfc50d8c'   

可見 HASH 物件產生的雜湊值都是 32 個 0~f 字元組成. 

如果是非英語系資料, 則在傳入 update() 之前須先呼叫字串物件的 encode() 方法將資料轉成 UTF-8 編碼 : 

>>> md5_hash.update('醒醒吧,你沒有女朋友'.encode('utf-8'))   
>>> md5_hash.digest()           # 傳回位元組形式的 MD5 雜湊值
b'\xb7O\xc6\xb49AS=\x16s\xdf\xb5\x10E\xd5-'
>>> md5_hash.hexdigest()     # 傳回字串形式的 MD5 雜湊值
'b74fc6b43941533d1673dfb51045d52d'

雜湊值最常用來辨別文件內容是否有變化, 例如文字檔或爬蟲的標的網頁是否有被編輯過, 如果是文字檔, 則讀取時可以直接用 'rb' 模式開啟 (讀取為 Byte 類型), 例如將下列內容以 utf-8 編碼存成 HTML 文字檔 hash.htm 於目前工作目錄下 :

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title></title>
</head>
<body>
  <h1>醒醒吧! 你沒有女朋友</h1>
</body>
</html>

然後用 with open() 開啟此檔案計算其雜湊值 :

>>> with open('hash.htm', 'rb') as fp :    
    data=fp.read()   
    md5_hash.update(data)   

>>> print(md5_hash.hexdigest())   
03db7e36e6363bbeb15f2646f573bf85
>>> print(md5_hash.hexdigest())   
03db7e36e6363bbeb15f2646f573bf85   

只要檔案內容不變, 則雜湊值也不變. 若將上面的網頁檔內容 "醒醒吧! 你沒有女朋友" 後面添加一個驚嘆號為 "醒醒吧! 你沒有女朋友!" : 

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title></title>
</head>
<body>
  <h1>醒醒吧! 你沒有女朋友!</h1>
</body>
</html>

存檔後再次讀取檔案內容, 則計算出來的雜湊值就會不一樣了 : 

>>> with open('hash.htm', 'rb') as fp :    
    data=fp.read()   
    md5_hash.update(data)   

>>> print(md5_hash.hexdigest()) 
ba5d2b91cb7e8ae33cfc0cdea06d752d

參考 :


沒有留言:

張貼留言