近期在專案中使用到了Memcached,相比redis較為齊全的工具,這個非關係型資料庫客戶端只能通過telnet
與伺服器端進行互動,於是有了為這個資料編寫1個簡便的客戶端工具的想法。
redis-cli
,你會發現這個工具是多麼的方便,比如某個命令你忘記了其使用的方式,你可以通過如下的方式來檢視:
1 2 3 4 5 6 |
127.0.0.1:6379> help get GET key summary: Get the value of a key since: 1.0.0 group: string |
目標
於是,打算參考redis-cli
編寫1個Memcached的CLI,在這個版本中,我們要實現:
- 輸入正確命令和引數後立即返回對應的結果
- 1個提示幫助命令的功能
實現思路
為了實現這個命令列版本,我們需要考慮以下幾個方面:
- 支援修改連線伺服器的監聽地址和埠
- 對傳入的引數進行解析並呼叫對應的方法
- 呼叫對應命令並傳入引數後,如果引數不對能給出錯誤的提示
- 支援幫助選項
對於第1個問題,我們可以通過引數的方式來解決。如果使用者沒有傳入對應的引數,則使用預設的引數進行繫結。關於從命令列中解析引數的方式,Python提供了幾種方式,這裡我選用的是argparse模組來操作。
而對應後面3個問題,我們可以藉助標準庫中的cmd模組來實現。
選擇客戶端繫結
在Python的Memcached的客戶端實現中,有python-memcached
、pymemcache
以及pylibmc
等多種第3方庫,這裡我採用的是pymemcache來說明我們這個命令列的實現,主要原因在於它是純python實現中最快和異常處理比較好的1個庫。
實現
下面,我們正式開始實現這個命令列。由於我們一般不會直接例項化Cmd類。我們先定義1個MemcachedCLI
類,這個類繼承自cmd模組中的Cmd類。接著,我們會例項化1個Memcached類的例項。
1 2 3 4 5 6 |
from pymemcache.client.base import Client from cmd import Cmd class MemcachedCLI(Cmd): def __init__(self, host, port): self.client = Client((host, port)) |
之後,我們需要對使用者輸入的內容進行解析並呼叫其對應的方法。這方面,cmd模組已經幫助處理這方面的內容了,我們只需要在該類中實現1個do_*
的方法,當我們在命令中輸入的1個對應的命令時,比如hello,其將呼叫1個do_hello
的方法。
我們知道,在Memcached中有多個命令,如果我們對這些命令1個個的實現,不是1件容易的事情。比如,在memcached中有1個get方法用於獲取指定鍵名的數值,而在pymemcache的實現中,Client例項有1個get方法對應上述的這個指令。
因此,在這裡,我們採用動態獲取屬性的方式來簡化的工作量,即通過如下的方式來呼叫對應的get方法:
1 |
getattr(self.client,'get') |
我們將這個屬性的處理過程封裝在1個get_action
的方法中:
1 2 3 4 5 6 |
def get_action(self, client): for name in dir(client): if not name.startswith('_'): attr = getattr(client, name) if callback(attr): setattr(self.__class__, 'do_' + name, self._make_cmd(name)) |
在這裡,我們遍歷Client類例項的每個屬性,如果對應的屬性不以_
字元開頭,我們則獲取該屬性,如果該屬性可以呼叫,我們再進行屬性的設定,將其設定為該類的do_*
屬性,通過該類_make_cmd
方法返回對應的數值。
在這裡,我們將生成的cmd命令封裝在_make_cmd
中,在這裡我們將通過name屬性獲取到使用者在命令列中輸入的第1個引數:
1 2 3 4 5 6 7 8 |
def _make_cmd(self, name): def handler(self, line): parts = line.split() try: print(getattr(self.client, name)( *parts)) except Exception as e: print('Error:{0}'.format(e)) return handler |
在這裡我們需要對傳入的字串進行切分。比如,使用者輸入了set name 20
,那麼後面2個引數將以字串name 20
的形式傳入。之後,我們嘗試獲取Client類的該屬性,並傳入解包後的引數進行呼叫。如果傳入的引數不正確,將觸發1個異常而被捕獲,並直接輸出。最後,我們返回1個handler函式。
這樣,當我們輸入如下的命令時:
1 |
set name 20 |
這將呼叫Client例項的set方法,並將name和20以引數的形式傳入,從而實現設定對應鍵名及其鍵值。
這樣,我們就完成了我們前3個思路的工作。關於命令幫助的問題,在Python中存在1個docstring的東西,我們可以直接使用該類每個方法的__doc__
屬性來實現對其文件的獲取。而在cmd中提供了help_*
的方法來實現對某個命令幫助文件的呼叫。
而在pymemcache庫中這方面已經幫助我們做好了,因此我們可以直接使用,我們只需要在之前的get_action
方法中新增這麼幾行程式碼:
1 2 3 |
doc = (getattr(attr, '__doc__', '') or '').strip() if doc: setattr(self.__class__, 'help_' + name, self._make_help(doc)) |
我們將其進行判斷得到的對應文件是否為空字串,如果不是才設定其對應的方法。我將幫助文件的內容封裝在了1個_make_help
的方法中:
1 2 3 4 |
def _make_help(self, doc): def help(self): print(doc) return help |
在這裡,我們直接輸出文件的內容即可。
最後,我們來看實際的效果,首先是help列出所有可用的命令:
1 2 3 4 5 6 7 8 |
127.0.0.1:11211>help Documented commands (type help ): ======================================== EOF check_key delete_many get gets_many quit set_multi add close delete_multi get_many help replace stats append decr exit get_multi incr set touch cas delete flush_all gets prepend set_many version |
然後是獲取某個指令的說明:
1 2 3 4 5 6 7 8 9 |
127.0.0.1:11211>help get The memcached "get" command, but only for one key, as a convenience. Args: key: str, see class docs for details. Returns: The value for the key, or None if the key wasn't found. |
最後是獲取和設定對應的鍵值:
1 2 3 4 5 6 |
127.0.0.1:11211>get name None 127.0.0.1:11211>set name zhangsan True 127.0.0.1:11211>get name zhangsan |
由於時間的限制,有一些細節的功能就不一一實現了。最後,可以通過如下的方式安裝使用:
1 |
pip install memcached-cli |
參考文章:
https://pymemcache.readthedocs.io/en/latest/apidoc/modules.html
https://tghw.com/blog/cheeky-python-a-redis-cli
https://docs.python.org/2/library/cmd.html#module-cmd