2022年1月10日 星期一

Python 學習筆記 : Firebase 即時資料庫 (二)

在上一篇測試中已完成 Firebase 資料庫的建立與線上資料新增與刪除等操作, 本篇主要是測試如何利用 python-firebase 套件連線 Firebase 即時資料庫並進行 CRUD (Create, Retrieve, Update, Delete) 操作. 

本系列上一篇文章參考 :

 

六. 安裝 python-firebase 套件 : 

python-firebase 為第三方套件, 須先安裝才能使用, 我先用 pip 指令安裝 :

pip install python-firebase 

C:\Users\User>pip install python-firebase   
Collecting python-firebase
  Downloading python-firebase-1.2.tar.gz (10 kB)
  Preparing metadata (setup.py) ... done
Requirement already satisfied: requests>=1.1.0 in c:\python37\lib\site-packages (from python-firebase) (2.21.0)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase) (1.24.1)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase) (3.0.4)
Requirement already satisfied: idna<2.9,>=2.5 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase) (2.8)
Requirement already satisfied: certifi>=2017.4.17 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase) (2018.11.29)
Building wheels for collected packages: python-firebase
  Building wheel for python-firebase (setup.py) ... done
  Created wheel for python-firebase: filename=python_firebase-1.2-py3-none-any.whl size=11534 sha256=3f46bf6f132adf9c4e4dcab2eed24f211fc75ad48b807a4b7ee2eac16fa6cbfc
  Stored in directory: c:\users\user\appdata\local\pip\cache\wheels\0f\6d\06\32ba434ed7b712403f944a537c25e5b9560d766b58d172c8e6
Successfully built python-firebase
Installing collected packages: python-firebase
Successfully installed python-firebase-1.2

但是匯入 firebase.firebase 模組時卻出現語法錯誤 : 

>>> from firebase import firebase    
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
  File "C:\Python37\lib\site-packages\firebase\__init__.py", line 3
    from .async import process_pool    
              ^
SyntaxError: invalid syntax

經查詢找到下面這篇 Stackoverflow 文章 :


原來是 python-firebase 的作者似乎沒將修正過的版本同時更新到 PyPi 網站, 導致 pip 從 PyPi 下載安裝到有 bug 的套件, 解決辦法是用 pip 指令直接從 python-firebase 的 GitHub 寄存庫下載套件來安裝 : 

pip install git+https://github.com/ozgur/python-firebase

C:\Users\User>pip install git+https://github.com/ozgur/python-firebase     
Collecting git+https://github.com/ozgur/python-firebase
  Cloning https://github.com/ozgur/python-firebase to c:\users\user\appdata\local\temp\pip-req-build-vtrzu0_q
  Running command git clone --filter=blob:none -q https://github.com/ozgur/python-firebase 'C:\Users\User\AppData\Local\Temp\pip-req-build-vtrzu0_q'
  Resolved https://github.com/ozgur/python-firebase to commit 0d79d7609844569ea1cec4ac71cb9038e834c355
  Preparing metadata (setup.py) ... done
Requirement already satisfied: requests>=1.1.0 in c:\python37\lib\site-packages (from python-firebase==1.2.1) (2.21.0)
Requirement already satisfied: certifi>=2017.4.17 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase==1.2.1) (2018.11.29)
Requirement already satisfied: idna<2.9,>=2.5 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase==1.2.1) (2.8)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase==1.2.1) (1.24.1)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in c:\python37\lib\site-packages (from requests>=1.1.0->python-firebase==1.2.1) (3.0.4)
Building wheels for collected packages: python-firebase
  Building wheel for python-firebase (setup.py) ... done
  Created wheel for python-firebase: filename=python_firebase-1.2.1-py3-none-any.whl size=12612 sha256=3ac6b887368946f2e6518e3c14ac262d76d90aa72521031ac6c1ce32292f2951
  Stored in directory: C:\Users\User\AppData\Local\Temp\pip-ephem-wheel-cache-xqlzf6r5\wheels\e7\7e\19\4f87f6d9a85bccbedc016446ef2314ed1f3fff01879f8f2be9
Successfully built python-firebase
Installing collected packages: python-firebase
  Attempting uninstall: python-firebase
    Found existing installation: python-firebase 1.2
    Uninstalling python-firebase-1.2:
      Successfully uninstalled python-firebase-1.2
Successfully installed python-firebase-1.2.1

果然重新從 GitHub 安裝後就可以順利匯入了.


七. python-firebase 套件的 CRUD 操作 : 

安裝好後先來看看此套件的內容, 我們會用到的是 firebase 套件裡的 firebase 模組 : 

>>> from firebase import firebase    
>>> type(firebase)    
<class 'module'>
>>> dir(firebase)   
['FirebaseApplication', 'FirebaseAuthentication', 'FirebaseTokenGenerator', 'FirebaseUser', 'JSONEncoder', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'http_connection', 'json', 'make_delete_request', 'make_get_request', 'make_patch_request', 'make_post_request', 'make_put_request', 'process_pool', 'urlparse']

可以用內建函式 dir(), type(), 以及 eval() 來過濾 firebase 模組中的成員 :

>>> members=dir(firebase)   
>>> for mbr in members:                 # 走訪 execjs 模組成員
    obj=eval('firebase.' + mbr)          # 用 eval() 求值取得 firebase 成員之參考
    if not mbr.startswith('_'):             # 走訪所有不是 "_" 開頭的成員
        print(mbr, type(obj))
        
FirebaseApplication <class 'type'>
FirebaseAuthentication <class 'type'>
FirebaseTokenGenerator <class 'type'>
FirebaseUser <class 'type'>
JSONEncoder <class 'type'>
http_connection <class 'function'>
json <class 'module'>
make_delete_request <class 'function'>
make_get_request <class 'function'>
make_patch_request <class 'function'>
make_post_request <class 'function'>
make_put_request <class 'function'>
process_pool <class 'firebase.lazy.LazyLoadProxy(function)'>
urlparse <class 'module'>

其中 FirebaseApplication 類別就是用來與 Firebase 即時資料庫建立連線的工具, 可以用它的建構子 FirebaseApplication() 來建立一個 FirebaseApplication 物件, 語法如下 : 

firebase.FirebaseApplication(url, None)  

第一參數 url 為資料庫的網址 (即 Firebase 即時資料庫主控台上方的資料庫根節點網址), 第二參數用來指定物件建立失敗時的傳回值, 預設為 None. 

>>> url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'      
>>> fbapp=firebase.FirebaseApplication(url, None)     
>>> type(fbapp)      
<class 'firebase.firebase.FirebaseApplication'>   
>>> dir(fbapp)    
['NAME_EXTENSION', 'URL_SEPERATOR', '__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__', '__str__', '__subclasshook__', '__weakref__', '_authenticate', '_build_endpoint_url', 'authentication', 'delete', 'delete_async', 'dsn', 'get', 'get_async', 'patch', 'patch_async', 'post', 'post_async', 'put', 'put_async'] 

同樣可用內建函式 dir(), type(), 以及 eval() 來過濾 FirebaseApplication 物件中的成員 :

>>> members=dir(fbapp)   
>>> for mbr in members:          # 走訪 execjs 模組成員
    obj=eval('fbapp.' + mbr)       # 用 eval() 求值取得 FirebaseApplication 成員之參考
    if not mbr.startswith('_'):      # 走訪所有不是 "_" 開頭的成員
        print(mbr, type(obj))
        
NAME_EXTENSION <class 'str'>
URL_SEPERATOR <class 'str'>
authentication <class 'NoneType'>
delete <class 'method'>
delete_async <class 'method'>
dsn <class 'str'>
get <class 'method'>
get_async <class 'method'>
patch <class 'method'>
patch_async <class 'method'>
post <class 'method'>
post_async <class 'method'>
put <class 'method'>
put_async <class 'method'>

其中常用的四個物件方法為 post(), get(), put(), 以及 delete(), 分別對應資料庫存取中最常用的 CRUD (Create, Retrieve, Update, Delete) 操作, 用法摘要如下表 :


 物件方法 說明
 get(url, None) 讀取指定 url 節點之資料, 傳回值為 dict 物件 [Retrieve]
 post(url, data) 在指定 url 節點新增一筆資料 data (dict 物件) [Create]
 delet(url + id, None) 刪除指定 url 節點下鍵為 id 之資料 [Delete]
 put(url, name, data) 於 url 節點新增或更新資料, 鍵=name 值=data [Create/Update]


其中 put() 方法與 post() 方法都可以新增資料, 它們的差別有三個, 其一為 post() 無法指定 ID (即 key), 而是由系統指定一個隨機 ID, 而 put() 方法則可以自行指定 ID, 若該 ID 已存在則更新該 ID 內的資料, 所以 put() 方法兼具 Create 與 Update 功能, 這是它們的第二個差別. 其三是 post() 的傳回值是一個鍵為 'name', 值為所新增之資料的 ID; 而 put() 的傳回值為所新增的資料 (值). 


1. 用 get() 方法讀取資料 : 

呼叫 FirebaseApplication 物件的 get(url, None) 方法可讀取指定 url 節點下的全部資料, 成功就以 dict 型態傳回這些資料, 失敗就傳回第二個參數 (通常設為 None). 在上一篇測試中, 我們已經透過 Firebase 即時資料庫主控台在 tony1966test 這個資料庫中手動建立如下資料 : 




在 get() 中傳入資料庫根節點將傳回此資料庫內所有資料, 例如 : 

>>> from firebase import firebase
>>> import time 
>>> url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'      
>>> fbapp=firebase.FirebaseApplication(url, None) 
>>> for k, v in data.items():       
    print(f'id={k} value={v}')      
    time.sleep(1)      
    
id=motto value={'motto1': '處事宜帶春風, 律己宜帶秋氣 (林則徐)', 'motto2': '物質生活向下看, 精神生活向上看 (胡適)'}
id=motto1 value=處事宜帶春風, 律己宜帶秋氣 (林則徐)

此處匯入 time 模組是為了讓每次存取 Firebase 有時間間隔 (例如每秒存取一次), 避免傳輸延遲問題. 上面是讀取資料庫根節點的結果, 可見 id=motto 因為底下還有一層, 因此其值為一個 dict 物件; 而 id=motto1 因為是直接放在根節點下, 所以其值是一個 str 物件. 由於 Firebase 資料庫支援 RESTful 存取, 因此如果將 url 指定為根節點的下一層節點, 則 get() 將傳回該節點下的所有資料, 例如 :  

>>> from firebase import firebase
>>> import time 
>>> url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'      
>>> fbapp=firebase.FirebaseApplication(url, None) 
>>> data=fbapp.get(url + "/motto", None)    
>>> data      
{'motto1': '處事宜帶春風, 律己宜帶秋氣 (林則徐)', 'motto2': '物質生活向下看, 精神生活向上看 (胡適)'}
>>> for k, v in data.items():    
    print(f'id={k} value={v}')       
    time.sleep(1)   
    
id=motto1 value=處事宜帶春風, 律己宜帶秋氣 (林則徐)
id=motto2 value=物質生活向下看, 精神生活向上看 (胡適)

可見傳入 get() 的 url 參數若加上鍵的路徑, 就可以取得該節點下的全部資料, 例如此處是取得根節點的下一層 id=motto 節點下的全部資料. 


2. 用 post() 方法新增資料 : 

呼叫 post(url, data) 方法可在指定節點 url 底下新增資料, 由於是純粹的 Create 操作, 新增的資料會被指定一個隨機 ID 以避免鍵的衝突. post() 的傳回值是一個 dict 物件, 其鍵固定為 'name', 其值為該新增資料的鍵 ID. 

如果第二參數為一個基本類型物件 (數值或字串等), 則此新增資料會被放在根節點下, 例如 : 

>>> from firebase import firebase
>>> import time 
>>> url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'      
>>> fbapp=firebase.FirebaseApplication(url, None) 
>>> data=fbapp.post(url, "初念淺, 轉念深")       
>>> data      
{'name': '-MsxZcjlmRCZQxXo9miT'}      # post() 的傳回值為一個字典物件

這時去看主控台會發現在根節點下已經新增了一筆 ID 為自動指派的資料 : 




如果傳入 post(url, data) 的第二參數 data 是一個字典, 則這筆資料不會放在根節點下, 而是放在下一層,  Firebase 會先指派一個隨機 ID 建立一個目錄節點, 然後將這筆資料放在這節點下面, 其 key 就是 dict 物件的 key. 傳回值同樣是一個鍵為 'name' 的字典, 值為目錄節點之 ID, 例如 :  

>>> data=fbapp.post(url, {'motto3': '初念淺, 轉念深'})    
>>> data   
{'name': '-MsxcUA65p_Q_kYtnd5S'}     

這時在主控台就可以看到這筆掛在根節點的下一層, 自動指派 ID 目錄節點下的新增資料了 : 




如果要將資料放在指定之目錄節點, 例如 /motto 節點下面, 則 post() 的 url 參數應該指定為 url + '/motto', 且 data 參數應該直接傳入字串, 而非 dict 物件, 例如 : 

>>> data=fbapp.post(url + '/motto', '初念淺, 轉念深')    # url 加掛 /motto, data=字串
>>> data   
{'name': '-MszU-El7e99pqBDTFZC'}

這時在主控台就可以看到這筆新增資料是直接掛在 /motto 節點之下 : 




如果 data 參數是傳入字典, 那麼這筆資料就不會直接放在 /motto 下, 而是 /motto 的下一層, 自動指派 ID 的目錄節點下, 例如 : 

>>> data=fbapp.post(url + '/motto', {'motto3': '初念淺, 轉念深'})     
>>> data      
{'name': '-Mt0Uq9CEa93xZoyhrbn'}

新增後到主控台就可以看見 /motto 下新增了一個自動指派 ID 的目錄節點, 新增資料是掛在此節點下, 而不是直接掛在 /motto 下 : 




由上面測試可知, post() 是個純粹的 Create 操作, 所新增的資料都會由系統指派一個隨機 ID, 無法自行指定, 若想自行指派 ID 必須使用 put() 方法. 


 3. 用 put() 方法新增或更新資料 : 

put(url, name, data) 兼具新增 (Create) 與更新 (Update) 操作功能, 它有三個參數, name 是 ID (鍵), 而 data 是值. 注意, 使用關鍵字參數時 name 與 data 位置可互換, 但若使用位置參數時, 第二參數為 name, 第三參數為 data. 傳回值為所新增的值 (即 data), 例如 : 

>>> from firebase import firebase
>>> import time 
>>> url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'      
>>> fbapp=firebase.FirebaseApplication(url, None) 
>>> data=fbapp.put(url + '/motto', name='motto3', data='初念淺, 轉念深')   
>>> data     
'初念淺, 轉念深'

此處呼叫 put() 時指定 url 為根節點的下一層目錄節點 /motto, 並以關鍵字參數 name 指定資料的 ID (鍵) 為 motto3, 以 data 指定資料的值, 由於 /motto 下面並沒有 ID=motto3 的資料, 故此為一新增資料的操作, put() 的傳回值為所新增資料的值. 

這時到主控台就可看到 /motto 節點下這筆鍵為 motto3 的新增資料 : 




上面若使用位置參數也是可以的, 結果完全一樣 :

>>> data=fbapp.put(url + '/motto', 'motto3', '初念淺, 轉念深')    
>>> data  
'初念淺, 轉念深' 

由於前面已經用 put() 新增了 ID=motto3, 所以上面這個使用位置參數的 put() 其實是 Update 的更新動作, 因為值相同所以看不出有甚麼更新, 下面用不一樣的值就看得出來了 :

>>> data=fbapp.put(url + '/motto', name='motto3', data='天下沒有白吃的午餐')     
>>> data   
'天下沒有白吃的午餐'

這時去看主控台就會發現 /motto 下的 motto3 內容已經被更新了 : 



 
在物聯網應用中, 若要保存歷史資料就用 post(), 新的資料不會覆蓋舊資料; 否則就用 post() 不斷更新一個固定的 ID. 


4. 用 delete() 方法刪除資料 : 

呼叫 delete(url + id, None) 可刪除指定 ID 的那筆資料, 其中第二參數為傳回值, 一般設為 None. 如果所指定的 ID 不存在也不會出現錯誤訊息, 一樣傳回 None, 例如刪除 /motto 下的 motto3 :

>>> from firebase import firebase
>>> import time 
>>> url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'      
>>> fbapp=firebase.FirebaseApplication(url, None) 
>>> data=fbapp.delete(url + '/motto/motto3', None)    
>>> print(data)    
None

這時查看主控台會發現 /motto 底下的 motto3 已經被刪除了 :




要刪除用 post() 新增的資料, 同樣也是將那看似亂數的隨機 ID 帶在 url 後面即可, 例如刪除根節點底下 ID=-MsxZcjlmRCZQxXo9miT 的這筆資料 : 

>>> id='-MsxZcjlmRCZQxXo9miT'  
>>> data=fbapp.delete(url + id, None)   
>>> print(data)  
None

檢視主控台可知  ID=-MsxZcjlmRCZQxXo9miT 這筆資料已被刪除 : 




接下來 ID='-MsxcUA65p_Q_kYtnd5S' 這筆資料, 其值是一個鍵為 motto3 的 JSON, 刪除它會將整個資料刪除, 例如 :  

>>> id='-MsxcUA65p_Q_kYtnd5S'    
>>> data=fbapp.delete(url + id, None)    
>>> print(data)   
None

檢查主控台該筆資料已被刪除 : 




如果刪除某個節點, 此節點底下不管有多少層, 所有資料都會被刪除. 例如上圖若刪除 /motto 這節點, 它底下全部資料都會全部刪除. 


5. 用迴圈批次新增資料 : 

最後來測試用迴圈批次新增資料的方法, 這些資料是放在一個字典串列中 : 

motto=[{ "motto1" : "處事宜帶春風, 律己宜帶秋氣 (林則徐)"},
             { "motto2" : "物質生活向下看, 精神生活向上看 (胡適)"},
             { "motto3" : "初念淺, 轉念深"},
             { "motto4" : "天下沒有白吃的午餐"}]

做法是用迴圈依序讀取串列中的字典, 然後用 put() 或 post() 寫到資料庫裡. 首先用 delete() 或在主控台手動清空所有資料 :




首先用 post() 來新增資料, 如上所述, post() 無法自訂節點名稱 (name), 因此若將串列中的字典寫入 /motto 下的話, 會新增一層隨機名稱的節點來存放該鍵值對, 完整程式如下 :

# firbase-batch-create-data-post-1.py
from firebase import firebase

mottos=[{"motto1" : "處事宜帶春風, 律己宜帶秋氣 (林則徐)"},
        {"motto2" : "物質生活向下看, 精神生活向上看 (胡適)"},
        {"motto3" : "初念淺, 轉念深"},
        {"motto4" : "天下沒有白吃的午餐"}]

url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'
fbapp=firebase.FirebaseApplication(url, None)
for motto in mottos:
    fbapp.post(url + '/motto', data=motto)

結果如下 : 




可見資料如預期地被放在 /motto 的下一層隨機名稱節點下. 如果想去掉多出來的一層, 只好犧牲原始資料中字典的鍵 (motto1, motto2, ...), 直接使用隨機名稱為鍵, 這樣就不能將字典傳給 post() 的第二參數, 而是只取出字典中的值傳入即可. 同樣先清空資料庫, 然後再執行下列程式 : 

# firbase-batch-create-data-post-2.py
from firebase import firebase

mottos=[{"motto1" : "處事宜帶春風, 律己宜帶秋氣 (林則徐)"},
        {"motto2" : "物質生活向下看, 精神生活向上看 (胡適)"},
        {"motto3" : "初念淺, 轉念深"},
        {"motto4" : "天下沒有白吃的午餐"}]

url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'
fbapp=firebase.FirebaseApplication(url, None)
for motto in mottos:
    value=list(motto.values())[0]
    fbapp.post(url + '/motto', data=value) 

此處呼叫字典的 values() 方法會傳回一個狀似串列的 dict_values 物件, 此物件內容不能使用索引存取, 故必須先用 list() 將其轉成串列才行, 結果如下 : 





如果想要自訂節點名稱, 則必須使用 put() 方法, 但必須先從原始資料中取出字典中的鍵與值, 分別傳給 put() 的參數 name 與 daya. 先將資料庫清空再執行下列程式 :

# firbase-batch-create-data-put.py
from firebase import firebase

mottos=[{"motto1" : "處事宜帶春風, 律己宜帶秋氣 (林則徐)"},
        {"motto2" : "物質生活向下看, 精神生活向上看 (胡適)"},
        {"motto3" : "初念淺, 轉念深"},
        {"motto4" : "天下沒有白吃的午餐"}]

url='https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app/'
fbapp=firebase.FirebaseApplication(url, None)
for motto in mottos:
    key=list(motto.keys())[0]
    value=list(motto.values())[0]
    fbapp.put(url + '/motto', name=key, data=value)

結果如下 : 




我把資料匯出如下 : 

{
  "motto" : {
    "motto1" : "處事宜帶春風, 律己宜帶秋氣 (林則徐)",
    "motto2" : "物質生活向下看, 精神生活向上看 (胡適)",
    "motto3" : "初念淺, 轉念深",
    "motto4" : "天下沒有白吃的午餐"
  }
}

可見匯出的 Firebase 資料庫為 JSON 格式, 也可以用 URL 直接取得 (若存取權限 read=True 的話), 其 URL 結構為 :

https://App網址/資料庫名稱.json  

例如上面的 motto 資料庫網址為 :


注意, 以前使用 firebaseio.com 網域 (很多網路資料都是舊的), 但後來改成 firebasedatabase.app 了, 若用舊網域會得到如下錯誤回應 : 


{
  "correctUrl" : "https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app",
  "error" : "Database lives in a different region. Please change your database URL to https://tony1966test-default-rtdb.asia-southeast1.firebasedatabase.app"
}

參考 : 


沒有留言 :