Python之pexpect詳解
一、引子
Pexpect
程序主要用於人機對話的模擬,就是那種系統提問,人來回答yes/no,或者賬號登陸輸入用戶名和密碼等等的情況。因為這種情況特別多而且繁瑣,所以很多語言都有各種自己的實現。最初的第一個 Expect 是由 TCL 語言實現的,所以後來的 Expect 都大致參考了最初的用法和流程,整體來說大致的流程包括:
- 運行程序
- 程序要求人的判斷和輸入
- Expect 通過關鍵字匹配
- 根據關鍵字向程序發送符合的字符串
TCL 語言實現的 Expect 功能非常強大,我曾經用它實現了防火墻設備的完整測試平臺。也因為它使用方便、範圍廣,幾乎所有腳本語言都實現了各種各樣的類似與Expect的功能,它們叫法雖然不同,但原理都相差不大
pexpect 是 Python 語言的類 Expect 實現。從我的角度來看,它在功能上與 TCL 語言的實現還是有一些差距,比如沒有buffer_full 事件、比如沒有 expect before/after 事件等,但用來做一般的應用還是足夠了。
二、基本使用流程
pexpect
的使用說來說去,就是圍繞3個關鍵命令做操作:
- 首先用spawn來執行一個程序
- 然後用expect來等待指定的關鍵字,這個關鍵字是被執行的程序打印到標準輸出上面的
- 最後當發現這個關鍵字以後,根據關鍵字用send方法來發送字符串給這個程序
第一步只需要做一次,但在程序中會不停的循環第二、三步來一步一步的完成整個工作。掌握這個概念之後 pexpect 的使用就很容易了。當然 pexpect 不會只有這 3 個方法,實際上還有很多外圍的其他方法,我們一個一個來說明
三、API
spawn()-執行程序
spawn()
方法用來執行一個程序,它返回這個程序的操作句柄,以後可以通過操作這個句柄來對這個程序進行操作:
process = pexpect.spawn('df -h')
process 就是 spawn() 的程序操作句柄了,之後對這個程序的所有操作都是基於這個句柄的,所以它可以說是最重要的部分。盡量給它起個簡短點的名字,不然後面的程序要多打不少字的。-
註意: spawn() ,或者說 pexpect 並不會轉譯任何特殊字符 比如 | * 字符在Linux的shell中有特殊含義,但是在 pexpect 中不會轉譯它們,如果在 linux 系統中想使用這些符號的正確含義就必須加上 shell 來運行,這是很容易犯的一個錯誤。
正確的方式:
import pexpect
process = pexpect.spawn('df -h')
print(process.expect(pexpect.EOF)) # 打印index
timeout - 超時時間
默認值:30(單位:秒)
指定程序的默認超時時間。程序被啟動之後會輸出,我們也會在腳本中檢查出中的關鍵字是否是以知並處理的,如果指定時間內沒找到程序就會出錯返回。
maxread - 緩存設置
默認值:2000(單位:字符)
指定一次性試著從命令輸出中讀多少數據。如果設置的數字比較大,那麽從 TTY 中讀取數據的次數就會少一些。
設置為 1 表示關閉讀緩存。
設置更大的數值會提高讀取大量數據的性能,但會浪費更多的內存。這個值的設置與 searchwindowsize 合作會提供更多功能。
緩存的大小並不會影響獲取的內容,也就是說如果一個命令輸出超過2000個字符以後,先前緩存的字符不會丟失掉,而是放到其他地方去,當你用 self.before (這裏 self 代表 spawn 的實例)還是可以取到完整的輸出的。
searchwindowsize - 模式匹配閥值
默認值: None
searchwindowsize 參數是與 maxread 參數一起合作使用的,它的功能比較微妙,但可以顯著減少緩存中有很多字符時的匹配時間。
默認情況下, expect() 匹配指定的關鍵字都是這樣:每次緩存中取得一個字符時就會對整個緩存中的所有內容匹配一次正則表達式,你可以想像如果程序的返回特別多的時候,性能會多麽的低。
設置 searchwindowsize 的值表示一次性收到多少個字符之後才匹配一次表達式,比如現在有一條命令會出現大量的輸出,但匹配關鍵字是標準的 FTP 提示符 ftp> ,顯然要匹配的字符只有 5 個(包括空格),但是默認情況下每當 expect 獲得一個新字符就從頭匹配一次這幾個字符,如果緩存中已經有了 1W 個字符,一次一次的從裏面匹配是非常消耗資源的,這個時候就可以設置 searchwindowsize=10, 這樣 expect 就只會從最新的(最後獲取的) 10 個字符中匹配關鍵字了,如果設置的值比較合適的話會顯著提升性能。不用擔心緩存中的字符是否會被丟棄,不管有多少輸出,只要不超時就總會得到所有字符的,這個參數的設置僅僅影響匹配的行為。
這個參數一般在 expect() 命令中設置, pexpect 2.x 版本似乎有一個 bug ,在 spawn 中設置是不生效的。
logfile - 運行輸出控制
默認值: None
當給 logfile 參數指定了一個文件句柄時,所有從標準輸入和標準輸出獲得的內容都會寫入這個文件中(註意這個寫入是 copy 方式的),如果指定了文件句柄,那麽每次向程序發送指令(process.send)都會刷新這個文件(flush)。
這裏有一個很重要的技巧:如果你想看到spawn過程中的輸出,那麽可以將這些輸出寫入到 sys.stdout 裏去,比如:
process = pexpect.spawn("ftp sw-tftp", logfile=sys.stdout)
用這樣的方式可以看到整個程序執行期間的輸入和輸出,很適合調試。
還有一個例子:
process = pexpect.spawn("ftp sw-tftp")
logFileId = open("logfile.txt", 'w')
process.logfile = logFileId
註意: logfile.txt 文件裏,既包含了程序運行時的輸出,也包含了 spawn 向程序發送的內容,有的時候你也許不希望這樣,因為某些內容出現了2次,那麽還有 2 個很重要的 logfile 關聯參數:
logfile_read - 獲取標準輸出的內容
默認值: None
記錄執行程序中返回的所有內容,也就是去掉你發出去的命令,而僅僅只包括命令結果的部分:
process.logfile_read = sys.stdout
上面的語句會在屏幕上打印程序執行過程中的所有輸出,但是一般不包含你向程序發送的命令,不過大部分程序都有回顯機制,比如發命令的時候設備不光接收到命令字符串,還會反向在你的終端上把字符串顯示出來讓你明白哪些字符被輸入了,這種時候也是會被這個方法讀到的。只有那些不會回顯的情況 logfile_read 才會拿不到,比如輸入密碼的時候。
logfile_send - 獲取發送的內容
默認值: None
記錄向執行程序發送的所有內容
process.logfile_send = sys.stdout
四、pexpect實現ssh操作
# -*- coding: utf-8 -*-
#!/usr/bin/python
import pexpect
def login_ssh_password(port,user,host,passwd):
'''函數:用於實現pexpect實現ssh的自動化用戶密碼登錄'''
if port and user and host and passwd:
ssh = pexpect.spawn('ssh -p %s %s@%s' % (port,user, host))
i = ssh.expect(['password:', 'continue connecting (yes/no)?'], timeout=5)
if i == 0 :
ssh.sendline(passwd)
elif i == 1:
ssh.sendline('yes\n') # 交互認證
ssh.expect('password: ')
ssh.sendline(passwd)
index = ssh.expect (["#", pexpect.EOF, pexpect.TIMEOUT])
if index == 0:
print("logging in as root!")
ssh.interact()
elif index == 1:
print("logging process exit!")
elif index == 2:
print("logging timeout exit")
else:
print("Parameter error!")
def login_ssh_key(keyfile,user,host,port):
'''函數:用於實現pexpect實現ssh的自動化密鑰登錄'''
if port and user and host and keyfile:
ssh = pexpect.spawn('ssh -i %s -p %s %s@%s' % (keyfile,port,user, host))
i = ssh.expect( [pexpect.TIMEOUT,'continue connecting (yes/no)?'], timeout=2)
if i == 1:
ssh.sendline('yes\n')
index = ssh.expect (["#", pexpect.EOF, pexpect.TIMEOUT])
else:
index = ssh.expect (["#", pexpect.EOF, pexpect.TIMEOUT])
if index == 0:
print("logging in as root!")
ssh.interact()
elif index == 1:
print("logging process exit!")
elif index == 2:
print("logging timeout exit")
else:
print("Parameter error!")
def main():
'''主函數:實現兩種方式分別的登錄'''
login_ssh_password('22','root','10.211.55.12','admin')
# login_ssh_key(keyfile="/tmp/id_rsa",port='22',user='root',host='192.168.1.101')
if __name__ == "__main__":
main()
Python之pexpect詳解