pexpect 模塊(python expect)


pexpect 模塊(python expect)
2019/07/07 Chenxin

參考
https://pypi.org/project/pexpect/ 官網
https://pexpect.readthedocs.io/en/stable/install.html 官網文檔
https://www.cnblogs.com/primadonna/p/10329094.html 簡單實用
https://blog.csdn.net/appke846/article/details/80514020 入門使用
https://blog.csdn.net/zhongbeida_xue/article/details/78679601 詳細說明

概念與流程說明

流程
1.運行程序
2.程序要求人的判斷和輸入
3.Expect 通過關鍵字匹配
4.根據關鍵字向程序發送符合的字符串

三個關鍵指令
pexpect 的使用說來說去,就是圍繞3個關鍵命令做操作:
1.首先用 spawn 來執行一個程序
2.然后用 expect 來等待指定的關鍵字,這個關鍵字是被執行的程序打印到標准輸出上面的
3.最后當發現這個關鍵字以后,根據關鍵字用 send 方法來發送字符串給這個程序

第一步只需要做一次,但在程序中會不停的循環第二、三步來一步一步的完成整個工作。
當然 pexpect 不會只有這 3 個方法,實際上還有很多外圍的其他方法,我們之后來說明(見詳細部分的API部分).

主要方法說明
spawn()執行程序
spawn() 方法用來執行一個程序,它返回這個程序的操作句柄,以后可以通過操作這個句柄來對這個程序進行操作.

比如:
process = pexpect.spawn('ftp sw-tftp')
spawn() 中的字符串就是要執行的程序,打開一個到 sw-tftp 服務器的 ftp 連接。
spawn() 中的第一個元素就是要執行的命令,除此之外還可以指定一些其他參數,比如: pexpect.spawn('ftp sw-tftp', timeout=60) 就指定了超時時間,這些具體的會在后面講解。
process 就是 spawn() 程序操作句柄,之后對這個程序的所有操作都是基於這個句柄的,所以它可以說是最重要的部分。

注意: spawn() ,或者說 pexpect 並不會轉譯任何特殊字符, 比如 | * 字符在Linux的shell中有特殊含義,但是在 pexpect 中不會轉譯它們,如果在 linux 系統中想使用這些符號的正確含義就必須加上 shell 來運行,這是很容易犯的一個錯誤。
正確的方式:process = pexpect.spawn('/bin/bash –c "ls –l | grep LOG > log_list.txt"')

expect() - 匹配
當 spawn() 啟動了一個程序並返回程序控制句柄后,就可以用 expect() 方法來等待匹配到命令的輸出了。它最后會返回 0 表示匹配到了所需的關鍵字,如果后面的匹配關鍵字是一個列表的話,就會返回一個數字表示匹配到了列表中第幾個關鍵字,從 0 開始計算。

send() - 發送字符串
send() 作為3個關鍵操作之一,用來向程序發送指定的字符串,比如:
process.expect("ftp>")
process.send("by\n") # 這個方法會返回發送字符的數量.

sendline() - 發送帶回車符的字符串
只發送字符用send().若發送字符+回車,推薦用 sendline().它也會返回發送的字符數量.

sendcontrol() - 發送控制信號
sendcontrol() 向子程序發送控制字符.比如要向子程序發送 ctrl+G,那么就這樣寫:
process.sendcontrol('g')

示例說明
簡單示例(SSH登陸模擬)
command = 'ssh '+username+'@'+host
child = pexpect.spawn(command)
ret = child.expect([pexpect.TIMEOUT,'Are you sure you want to continue connecting','[P|p]assword']+PROMPT)
if ret == 0:
print('[-] Error Connecting')
return
# 0 :連接超時
# 1 :ssh有時候提示你是否確認連接
# 2 :提示輸入密碼
if ret == 1:
child.sendline('yes')
ret = child.expect([pexpect.TIMEOUT,'[p|P]assword'])
if ret == 0:
print('[-] Error Connecting')
return
if ret == 2:
send_command(password)
return
if ret == 2:
send_command(password)
return
return child

注:針對ssh遠程登錄,pexpect又派生出了pxssh類,在ssh會話操作上再做一層封裝.其常用方法是:
login() #建立ssh連接
logout() #斷開ssh連接
prompt() #等待系統提示符,用於等待命令執行結束

在本地Macpro上測試通過的SSH模擬腳本
import pexpect

PROMPT = ['# ', '>>> ', '> ', '$ ']

PROMPT = ['#', '>>>', '>', '$'] # 提示符后不能有空格

def send_command(process, cmd):
process.sendline(cmd)
process.expect(PROMPT)
# print(process.before)
return process.before # 緩存中剩下的內容展示出去(也就是上面send的指令的結果,比如系統上執行ls -al的結果)

def connect(user, host, password):
ssh_newkey = 'Are you sure you want to continue connecting'
connStr = 'ssh ' + user + '@' + host
print('連接信息是: ', connStr)
process = pexpect.spawn(connStr, timeout=10)
print('嘗試連接')
ret = process.expect([pexpect.TIMEOUT, ssh_newkey, '[P|p]assword:']) # 語法與含義就是以下
# 0 :連接超時(匹配到了pexpect.TIMEOUT)
# 1 :ssh有時候提示你是否確認連接(匹配到了ssh_newkey)
# 2 :提示輸入密碼(匹配到了[P|p]assword:)
# 3 :匹配到#號,表示命令已經執行完畢。沒用到
print('返回值:', ret)
if ret == 0:
print('[-] Error connecting')
return
if ret == 1:
print('SSH提示確認連接嗎,yes/no')
process.sendline('yes')
# if ret == 2:
# process.sendline(password)
# print('已經輸入密碼')
# process.expect(PROMPT)
# return process
# ret = process.expect([pexpect.TIMEOUT, ssh_newkey, '[P|p]assword:'])
# if ret == 0:
# print('[-] Error connecting')
# return
process.sendline(password)
process.expect(PROMPT)
return process

def main():
host = '127.0.0.1'
user = 'chanix'
password = 'che.....'
process = connect(user, host, password)
print(str(send_command(process, 'pwd'), 'utf-8')) # 將bytes類型轉為字符串格式
print('-----')
process = connect(user, host, password)
result = send_command(process, 'cd /home;ls -al')
print(type(result))
# print(result)
string = str(result, 'utf-8') # 將bytes類型轉為字符串格式
print(string)

if name == 'main':
main()

詳細方法說明-API

spawn() - 執行程序
spawn() 方法用來執行一個程序,它返回這個程序的操作句柄,以后可以通過操作這個句柄來對這個程序進行操作,比如:
process = pexpect.spawn('ftp sw-tftp')
上面 spawn() 中的字符串就是要執行的程序,這里我們打開一個到 sw-tftp 服務器的 ftp 連接。 spawn() 中的第一個元素就是要執行的命令,除此之外還可以指定一些其他參數,比如: pexpect.spawn('ftp sw-tftp', timeout=60) 就指定了超時時間,這些具體的會在后面講解。
process 就是 spawn() 的程序操作句柄了,之后對這個程序的所有操作都是基於這個句柄的,所以它可以說是最重要的部分。盡量給它起個簡短點的名字,不然后面的程序要多打不少字的。
注意: spawn() ,或者說 pexpect 並不會轉譯任何特殊字符 比如 | * 字符在Linux的shell中有特殊含義,但是在 pexpect 中不會轉譯它們,如果在 linux 系統中想使用這些符號的正確含義就必須加上 shell 來運行,這是很容易犯的一個錯誤。
正確的方式:
process = pexpect.spawn('/bin/bash –c "ls –l | grep LOG > log_list.txt"')
process.expect(pexpect.EOF)
spawn() 還有一種調用方式就是第一個參數是主程序,而下一個參數是主程序的參數,理解起來很麻煩?看看實際代碼吧:
cmd = "ls –l | grep LOG > log_list.txt"
process = pexpect.spawn("/bin/bash", ["-c", cmd])
process.expect(pexpect.EOF)
這些代碼和上面一個例子是相同的,是不是更清晰一些?

spawn 的選項包括下面這些:

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 關聯參數:

1.logfile_read - 獲取標准輸出的內容
默認值: None
記錄執行程序中返回的所有內容,也就是去掉你發出去的命令,而僅僅只包括命令結果的部分:
process.logfile_read = sys.stdout
上面的語句會在屏幕上打印程序執行過程中的所有輸出,但是一般不包含你向程序發送的命令,不過大部分程序都有回顯機制,比如發命令的時候設備不光接收到命令字符串,還會反向在你的終端上把字符串顯示出來讓你明白哪些字符被輸入了,這種時候也是會被這個方法讀到的。只有那些不會回顯的情況 logfile_read 才會拿不到,比如輸入密碼的時候。

2.logfile_send - 獲取發送的內容
默認值: None
記錄向執行程序發送的所有內容
process.logfile_send = sys.stdout
上面的語句僅僅在屏幕上打印向程序發送的內容。

cwd - 指定命令執行的目錄
默認值: None 或者說 ./
cwd 用來指定命令發送的命令在哪個路徑下執行,它一般是用在 send() 系列命令中,比如在 Linux 中,你想在 /etc 目錄下執行 ls –l 命令,那么完全不需要用 sendline("cd /etc && ls -l") 這樣的方式,而是用 sendline("ls –l", cwd="/etc") 就可以了。

env - 指定環境變量
默認值: None
指定環境變量的值,這個值是一個字典,如果你發送的命令要使用一些環境變量,那么可以在這里提供

ignore_sighup - 是否過濾 SIGHUP 信號
默認值: True
這個參數是 pexpect 3.1 開始引入的,在 3.1 之前(比如 pexpect 2.3),spawn 的子程序會過濾 SIGHUP 信號,也就是用 Ctrl+C 是不能終止子程序的,3.1的默認值也繼承了這個行為,但是如果設置 ignore_sighup = False 就可以改變這個行為。

delaybeforesend - 字符發送延時
默認值: 0.05
這是一個隱藏參數用來設置發送字符串之前的延時。增加這個參數的最大理由是因為很多人碰見這樣一個問題:
在 FTP 程序中登錄時如果用腳本輸入密碼時會直接顯示出來。這是基於一個一般人不可思議的事實:
當 FTP 登錄時,實際上服務器會先打印要求你輸入密碼的提示符,然后再發一個信號把回顯功能取消,當人使用鍵盤輸入的時候因為這個動作延時比較高所以不可能看到回顯的密碼,但腳本會在發現輸入密碼的提示符時立即發送,於是密碼就會在關閉回顯之前出現了。 Pexpect 為了解決這個問題在每次發送字符前默認等待 50 毫秒,如果你認為不必要的話就可以自己設置為 0 來取消這個行為。

expect() - 關鍵字匹配
當 spawn() 啟動了一個程序並返回程序控制句柄后,就可以用 expect() 方法來等待指定的關鍵字了。它最后會返回 0 表示匹配到了所需的關鍵字,如果后面的匹配關鍵字是一個列表的話,就會返回一個數字表示匹配到了列表中第幾個關鍵字,從 0 開始計算。
expect() 利用正則表達式來匹配所需的關鍵字。

它的使用方式:

pattern_list 正則表達式列表,表示要匹配這些內容

timeout 不設置或者設置為-1的話,超時時間就采用self.timeout的值,默認是30秒。也可以自己設置。

searchwindowsize 功能和 spawn 上的一樣,但是!請注意這個但是!下面會實際說明

process.expect(pattern_list, timeout=-1, searchwindowsize=None)
在這里的 searchwindowsize 是在 expect() 方法中真正生效的,默認情況下是 None,也就是每從子進程中獲取一個字符就做一次完整匹配,如果子進程的輸出很多的話……性能會非常低。如果設置為其他的值,表示從子進程中讀取到多少個字符才做一次匹配,這樣會顯著減少匹配的次數,增加性能。
經過測試,對於一個有 48100000 個字符的子進程,將 searchwindowsize 設置為 2000 時,完全處理完成需要 73.2730 秒;同樣的子進程將這個參數設置為 None 則需要 1949.6259 秒,Oh, my Lady GAGA…… 完全是指數上升啊。

最簡單的匹配方式
process.expect('[Nn]ame')
上面的代碼表示:匹配 process 這個句柄(spawn 方法的例子中我們啟動的 ftp 連接)中的 name 關鍵字,其中 n 不分大小寫。
上面的關鍵字一旦匹配,就會返回0表示匹配成功,但是如果一直匹配不到呢?默認是會一直等下去,但是如果設置了 timeout 的話就會超時。

匹配一系列輸出
實際上, expect() 可以匹配一系列輸出,通過檢查匹配到的輸出,我們可以做不同的事情。
比如之前 spawn 的 ftp 連接,如果我們輸入用戶名之后有不同的情況,就可以通過監控這些不同情況來做不同的動作,比如:
index = process.expect(['Permission Denied','Terminal type','ftp>',])
if index == 0:
print "Permission denied at host, can't login."
process.kill(0)
elif index == 1:
print "Login ok, set up terminal type…"
process.sendline('vty100')
process.expect("ftp>")
elif index == 2:
print "Login Ok, please send your command"
process.interact()
上面的代碼中,expect 方法中的是一個列表,列表中的每個元素都是一個關鍵字的正則表達式,也就是說我們期待這 3 種情況之一,而 expect 返回一個順序值來代表我匹配到了哪一個元素(也就是發生了哪種情況了),這個順序值是從 0 開始計算的。
當expect之后,下面的 if 語句就開始處理這 3 種情況了:
0:權限不足,我們就給用戶提示一下,然后殺掉這個進程.
1:登陸成功,但還要用戶指定終端模式才能真正使用,然后看是不是能真正使用了
2:還是登陸成功了,而且還可以直接輸入命令操作 ftp 服務器了,於是我們提示用戶,然后把操作權限交給用戶

另外有一種特殊情況,如果同時有2個被匹配到,那么怎么辦?簡單來說就是這樣:
原始流中,第一個被關鍵字匹配到的內容會被使用
匹配關鍵字列表中,最左邊的會被使用
給個例子:

如果流里面的內容是 "hello world"

index = process.expect(["hi", "hello", "hello world"])
返回的值是 1,也就是 'hello' 被匹配到了,哪怕真正最好的匹配是 "hello world" 但因為放在后面所以仍然無效。

使用技巧 批評EOF 或者 TIMEOUT,以及大小寫正則
如果要檢查或者匹配 expect.EOF 和 expect.TIMEOUT 這兩種情形,那么必須將它們放進匹配列表里面去,這樣可以通過檢查返回的數字來處理它們。如果沒放進列表的話,就會發生 EOF 或者 TIMEOUT 錯誤,程序就會中途停止了.
匹配規則中有些特殊語法,比如下面的規則中前 2 個匹配都是大小寫無關的,關鍵就是這個 (?i) 匹配規則,它相當於 re.IGNORE 或者 re.I 這個關鍵字,因為畢竟不是真正的正則表達式引擎,所以 pexpect 使用這樣特殊語法:
child.expect(['(?i)etc', '(?i)readme', pexpect.EOF, pexpect.TIMEOUT])

expect_exact() - 精確匹配
它的使用和 expect() 是一樣的,唯一不同的就是它的匹配列表中不再使用正則表達式。
從性能上來說 expect_exact() 要更好一些,因為即使你沒有使用正則表達式而只是簡單的用了幾個字符 expect() 也會先將它們轉換成正則表達式模式然后再搜索,但 expect_exact() 不會,而且也不會把一些特殊符號轉換掉。

expect_list() - 預轉換匹配
使用方式和 expect() 一樣,唯一不同的就是它里面接受的正則表達式列表只會轉換一次。
expect() 稍微有點笨,每調用一次它都會將內部的正則表達式轉換一次(當然也有其他辦法避免),如果你是在以后循環中調用 expect() 的話,多余的轉換動作就會降低性能,在這種情況下建議用 expect_list() 來代替。
使用方法:

timeout 為 -1 的話使用 self.timeout 的值

searchwindowsize 為 -1 的話,也使用系統默認的值

process.expect_list(pattern_list, timeout=-1, searchwindowsize=-1)

expect_loop()
用於從標准輸入中獲取內容,loop這個詞代表它會進入一個循環,必須要從標准輸入中獲取到關鍵字才會往下繼續執行。
使用方法:
expect_loop(self, searcher, timeout=-1, searchwindowsize=-1)

send() - 發送關鍵字
send() 作為3個關鍵操作之一,用來向程序發送指定的字符串,它的使用沒什么特殊的地方,比如:
process.expect("ftp>")
process.send("by\n")
這個方法會返回發送字符的數量。

sendline() - 發送帶回車符的字符串
sendline() 和 send() 唯一的區別就是在發送的字符串后面加上了回車換行符,這也使它們用在了不同的地方:
只需要發送字符就可以的話用send()
如果發送字符后還要回車的話,就用 sendline()
它也會返回發送的字符數量.

sendcontrol() - 發送控制信號
sendcontrol() 向子程序發送控制字符,比如 ctrl+C 或者 ctrl+D 之類的,比如你要向子程序發送 ctrl+G,那么就這樣寫:
process.sendcontrol('g')

sendeof() - 發送 EOF 信號
向子程序發送 End Of File 信號。

sendintr() - 發送終止信號
向子程序發送 SIGINT 信號,相當於 Linux 中的 kill 2 ,它會直接終止掉子進程。

interact() - 將控制權交給用戶
interact() 表示將控制權限交給用戶(或者說標准輸入)。
一般情況下 pexpect 會接管所有的輸入和輸出,但有的時候還是希望用戶介入,或者僅僅是為了完成一部分工作的時候, interact() 就很有用了。
比如:
登陸 ftp 服務器的時候,在輸入用戶密碼階段希望用戶手工輸入密碼,然后腳本完成剩余工作時(將用戶密碼寫在腳本中可不安全)只希望完成登陸工作,比如要 ssh 連接到一台遠方的服務器,但中間要經過好幾跳,用手工輸入實在太麻煩,所以就用腳本先跳到目的服務器上,然后再把控制權限還給用戶做操作。
使用方法:

escape_character 就是當用戶輸出這里指定的字符以后表示自己的操作完成了,將控制權重新交給 pexpect

process.interact(escape_character='\x1d', input_filter=None, output_filter= None) # \x1d,其中\x是格式要求,1d是ascii碼16進制表示,這里就是分組符"]".
通過設置 escape_character 的值,可以定義返回碼,默認是 ctrl+] 或者說 ^],當輸入了返回碼以后,腳本會將控制權從用戶那里重新拿回來,然后繼續向下執行。

close() - 停止應用程序
如果想中途關閉子程序,那么可以用 close 來完成,調用這個方法后會返回這個程序的返回值。
如果設置 force=True 會強行關閉這個程序,大概的過程就是先發送 SIGHUP 和 SIGINT 信號,如果都無效的話就發 SIGKILL 信號,反正不管怎么樣都會保證這個程序被關閉掉。
下面是實例:
process.close(force=True)

terminate() - 停止應用程序
可以看作是上面 close() 的別名,不管是功能還是使用方法都和close()一樣。

Kill() - 發送 SIGKILL 信號
向子程序發送 SIGKILL 的信號。

flush()
什么都不干,只是為了與文件方法兼容而已。

isalive() - 檢查子程序運行狀態
檢查被調用的子程序是否正在運行,這個方法是運行在非阻斷模式下面的。
如果獲得的返回是 True 表示子程序正在運行;返回 False 則表示程序運行終止。

isatty() - 檢查是否運行在 TTY 設備上
返回 True 表示打開和連接到了一個 tty 類型的設備,或者返回 False 表示未連接。

next() - 返回下一行內容
和操作文件一樣,這個方法也是返回緩存中下一行的內容。

read() - 返回剩下的所有內容
獲取子程序返回的所有內容,一般情況下我們可以用 expect 來期待某些內容,然后通過 process.before 這樣的方式來獲取,但這種方式有一個前提:那就是必須先 expect 某些字符,然后才能用 process.before 來獲取緩存中剩下的內容。
read() 的使用很不同,它期待一個 EOF 信號,然后將直到這個信號之前的所有輸出全部返回,就像讀一個文件那樣。
一般情況下,交互式程序只有關閉的時候才會返回 EOF ,比如用 by 命令關閉 ftp 服務器,或者用 exit 命令關閉一個 ssh 連接。
這個方法使用范圍比較狹窄,因為完全可以用 expect.EOF 方式來代替。當然如果是本機命令,每執行完一次之后都會返回 EOF ,這種情況下倒是很有用:
process = pexpect.spawn('ls –l')
output = process.read()
print output
看起來這么做有點無聊?但我想一定有什么理由支持這個方法。
可以用指定 read(size=-1) 的方式來設置返回的字符數,如果沒有設置或者設置為負數則返回所有內容,正數則返回指定數量的內容,返回的內容是字符串形式。

readline() - 返回一行輸出
返回一行輸出,返回的內容包括最后的\r\n字符。
也可以設置 readline(size=-1) 來指定返回的字符數,默認是負數表示返回所有的。

readlines() - 返回列表模式的所有輸出
返回一個列表,列表中的每個元素都是一行(包括\r\n字符)。

setecho() - 子程序響應模式
設置子程序運行時的響應方式,一般情況下向子程序發送字符的時候,這些字符都會在標准輸出上顯示出來,這樣你可以看到你發送出去的內容,但是有的時候,我們不需要顯示,那么就可以用這個方法來設置了。
注意,必須在發送字符之前設置,設置之后在之后的代碼中都一直有效。比如:
process = pexpect.spawn('cat')

默認情況下,下面的1234這個字符串會顯示2次,一次是pexpect返回的,一次是cat命令返回的

process.sendline("1234")

現在我們關閉pexpect()的echo功能

process.setecho(False)

下面的字符只會顯示一次了,這是由cat返回的

process.sendline("abcd")

現在重新開啟echo功能,就可以再次看到我們發送的字符了

process.setecho(True)

setwinsize() - 控制台窗口大小
如果子程序是一個控制台(TTY),比如 SSH 連接、 Telnet 連接這種通過網絡登陸到系統並發送命令的都算控制台,那么可以用這個方法來設置這個控制太的大小(或者說長寬)。
它的調用方式是 process.setwinsize(r, c)
默認值是 setwinsize(24, 80),其中 24 是高度,單位是行; 80 是寬度,單位是字符。
為什么要用它?想像下面的場景:
有的時候你通過pexpect登陸到某個ssh控制台之后,又用 interact() 來將控制權交給用戶,然后用戶到控制台里面寫自己的命令,如果命令比較長,就會發現當命令到屏幕邊緣之后不會自動換行,而是又返回到這一行的最前面重新覆蓋前面的字符;這不會影響命令的實際效果,但是很惱人。
這種情況用 setwinsize() 就可以解決,找到自己終端支持的長度,重新設置一下,比如 setwinsize(25, 96 ),如果設置的正確的話就可以解決了。

wait() - 執行等待
直到被調用的子程序執行完畢之前,程序都停止(或者說等待)執行。它不會從被調用的子程序中讀取任何內容。

waitnoecho() - 用於密碼輸入(不顯示明文)
它使用的地方比較特殊,唯一匹配的地方就是:當子程序的 echo 功能被設置為 False 時。
看起來很奇怪?其實這個功能是基於一個很讓人難以置信但的確是真實的情況:
在命令行模式下,很多要求輸入密碼的地方,比如 FTP/SSH 等,密碼實際上都會在你輸入之后又重新返回並打印出來的,但是為什么我們看不到我們自己輸入的密碼呢?這就是因為密碼在要打印出來之前被程序將 echo 功能設置為 False 了。
現在知道為什么有這么一個方法了吧?比如要進行一個 ssh 連接時,如何檢查是否要輸入密碼?用關鍵字 password 是一個方法,但還有一個方法就是這樣:

啟動ssh連接

process = pexpect.spawn("ssh user@example.com")

等待echo被設置為False,這就意味着本地不會有回顯

process.waitnoecho()
process.sendline('mypassword')
可以設置超時時間,默認是:waitnoecho(timeout=-1),表示和系統設置的超時時間相同,也可以設置為 None 表示永遠等待,直到回顯被設置為 False ,當然還可以設置其他的數字來表示超時時間。

write() - 發送字符串
類似於send()命令,只不過不會返回發送的字符數。

writelines() - 發送包含字符串的列表
類似於 write() 命令,只不過接受的是一個字符串列表, writelines() 會向子程序一條一條的發送列表中的元素,但是不會自動在每個元素的最后加上回車換行符。
與 write() 相似的是,這個方法也不會返回發送的字符數量。

特殊變量
pexpect.EOF - 匹配終止信號
EOF 變量使用范圍很廣泛,比如檢查 ssh/ftp/telnet 連接是否終止啊,文件是否已經到達末尾啊。 pexpect 大部分腳本的最后都會檢查 EOF 變量來判斷是不是正常終止和退出,比如下面的代碼:
process.expect("ftp>")
process.sendline("by")
process.expect(pexpect.EOF)
print "ftp connect terminated."

pexpect.TIMEOUT - 匹配超時信號
TIMEOUT 變量用來匹配超時的情況,默認情況下 expect 的超時時間是 60 秒,如果超過 60 秒還沒有發現期待的關鍵字,就會觸發這個行為,比如:

匹配pexpect.TIMEOUT的動作,只有超時事件發生的時候才會有效

index = process.expect(['ftp>', pexpect.TIMEOUT],)
if index == 1: # 這里應該是從0開始吧?
process.interactive() ; # 將控制權交給用戶
elif index == 2:
print "Time is out."
process.kill(0) ; # 殺掉進程

那么怎么改變超時時間呢?其實可以修改spawn對象里的timeout參數:

下面的例子僅僅加了一行,這樣就改變了超時的時間了

process.timeout = 300 ; # 注意這一行
index = process.expect(['ftp>', pexpect.TIMEOUT],)
if index == 1:
process.interactive() ; # 將控制權交給用戶
elif index == 2:
print "Time is out."
process.kill(0) ; # 殺掉進程

process.before/after/match - 獲取程序運行輸出
當 expect() 過程匹配到關鍵字(或者說正則表達式)之后,系統會自動給3個變量賦值,分別是 before, after 和 match.
process.before - 保存了到匹配到關鍵字為止,緩存里面已有的所有數據。也就是說如果緩存里緩存了 100 個字符的時候終於匹配到了關鍵字,那么 before 就是除了匹配到的關鍵字之外的所有字符.
process.after - 保存匹配到的關鍵字,比如你在 expect 里面使用了正則表達式,那么表達式匹配到的所有字符都在 after 里面.
process.match - 保存的是匹配到的正則表達式的實例,和上面的 after 相比一個是匹配到的字符串,一個是匹配到的正則表達式實例.
如果 expect() 過程中發生錯誤,那么 before 保存到目前位置緩存里的所有數據, after 和 match 都是 None

self.exitstatus | self.signalstatus
上面的2個值用來保存spawn子程序的退出狀態,但是注意:只有使用了 process.close() 命令之后這 2 個參數才會被設置。

其他說明
CR/LF約定(回車,換行 符號)
眾所周知的是:世界上有很多種回車換行約定,它們給我們造成了很多麻煩,比如:
windows 中用 \r\n 表示回車換行
Linux like 系統中用 \r 表示回車換行
Mac 系統中用 \n 表示回車換行
這種種回車換行約定對代碼移植造成了很大的困難,幾乎所有全平台支持的程序語言都有它們自己的解決方案,而 pexpect 的解決方案就是:
不管哪個平台,回車換行都替換成 \r\n
所以,如果我們要在expect中匹配回車換行符號的話,就必須這么做:
process.expect('\r\n')

想匹配一行里的最后一個單詞:

process.expect('\w+\r\n')

下面的匹配方式是錯誤的(在其他腳本語言中是正確的,比如TCL語言的Expect實現中,這也是很容易搞混淆的地方):

process.expect('\r')

$ * + 約定
正則表達式中, $ 符號表示從一行中的最后開始匹配——但是在 pexpect 中是無效的。如果要匹配一行的最后,那么必須有一行數據存在,也就是有回車換行符,但是 pexpect 的處理不是按行來進行的,它一次僅僅讀一個並且處理一個字符,而且不會處理【未來】的數據。
所以不管什么時候,都不要在 expect() 中用 $ 符號來匹配。
正因為pexpect一次僅僅處理一個字符,所以加號 (+) 、星號 (*) 的功能也無效了,比如:

無論何時,都只會返回一個字符

process.expect(".+")

無論何時,都只會返回空字符

process.expect(".*")

程序調試
如果要調試pexpect,那么可以使用下面的方式:
str(processHandle)

通過 pexpect.spawn() 可以創建一個進程,並通過操作這個進程的句柄來控制程序。

但是如果將這個句柄用 str() 函數重載一下呢?它會顯示這個控制句柄的一系列內部信息,比如:

process = pexpect.spawn("ftp sw-tftp")
print str(process)

<pexpect.spawn object at 0x2841cacc>

version: 2.3 ($Revision: 399 $)
command: /usr/bin/ftp
args: ['/usr/bin/ftp', 'sw-tftp']
searcher: searcher_re:
0: EOF
buffer (last 100 chars):
before (last 100 chars): was 14494 bytes in 1 transfers.
221-Thank you for using the FTP service on sw-tftp.
221 Goodbye.
after: <class 'pexpect.EOF'>
match: <class 'pexpect.EOF'>
match_index: 0
exitstatus: 0
flag_eof: True
pid: 50733
child_fd: 3
closed: False
timeout: 30
delimiter: <class 'pexpect.EOF'>
logfile: None
logfile_read: None
logfile_send: None
maxread: 2000
ignorecase: False
searchwindowsize: None
delaybeforesend: 0.05
delayafterclose: 0.1
delayafterterminate: 0.1
技巧和陷阱
循環匹配
Python 的 pexpect 模塊與 TCL 的 expect 相比有些功能明顯支持不足,其中就包括循環匹配。 TCL 的 expect 模塊可以給出一系列匹配關鍵字,然后通過 continue 語句的設置保證同一個 expect 可以在關鍵字列表中重新循環。
比如一個 expect 有 3 個關鍵字,其中匹配到第二個關鍵字的時候會碰見 continue 語句,那么下一次匹配就重復這個 expect 過程,這是一個很有用的功能,比如超時時間設置為 10 秒,然后重復 3 次才會真正超時的情況。
可惜的是 Python 的 pexpect 沒有這樣的功能。但是想模擬這種情況也不是不可以,可以通過 while 語句來完成,比如:
while True:
index = process.expect([ pexpect.TIMEOUT, pexpect.EOF, ])
if index == 0:
print "time is out"
continue # 重新從開始匹配
elif index == 1:
print "Terminate."
break # 終止循環

獲取 before 中內容與清空 buffer
絕大多數情況下我們都會利用before變量來獲取命令執行的結果。但是,你真的知道怎么用好 before 么?
before 中到底什么時候保存你所需的內容?
下面的 handle 是一個 spawn 后的句柄,而 prompt 則是 bash 的提示符。
步驟:
匹配提示符,以此判斷系統已經准備好接受命令
發送命令
獲取命令執行后的結果
handle.expect(prompt)
handle.sendline("ls –l")
handle.expect(prompt)
output = handle.before
一共 4 個語句,就可以獲取 ls –l 命令的結果了,但是且慢,是否發現有什么不合理的地方?
第一句和第二句分別是匹配系統提示符和發送命令,這都是比較正常的。
但是為什么第三句是再次匹配系統提示符?在一般的想像下,發送命令之后,設備就會執行並返回結果了,那么完全就可以用 handle.before 語句來獲取到這些內容了才對啊?
實際上,從 before 這個單詞就可以大概明白,它並不是實時生效的,它里面的內容,實際上是上一次 expect 匹配之后,除掉匹配到的關鍵字本身,系統緩存中剩余下來的全部內容。也就是說,如果第三句就是 output = handle.before的話,那么它里面的內容就是第一句的那個 expect 中去掉 prompt 內容后緩存中剩下來的內容。顯然,這里面不會包括后面 ls –l 命令的內容了。
那么想獲取 ls –l 的內容,唯一的辦法是再增加一個 expect 關鍵字匹配。這是非常關鍵的一點。

另外, pexpect 中的 buffer 是一個關鍵,但又不能被直接操作的變量,它保存的是運行過程中每一個 expect 之后的所有內容,隨時被更新。而 before/after 都是直接源於它的,而 expect 的關鍵字匹配本身也是在 buffer 中做匹配的。
正因為它的重要性,對這個變量中的內容需要特別的警惕。比如我們將登陸設備,發送命令,退出設備這3個步驟寫進3個函數的時候,最好保證每個步驟都不會影響下一個步驟,在每個步驟開始的時候,最好做這樣的操作:
handle.buffer = ""

代碼實例
FTP服務器的登陸
下面的代碼比較簡單,就是登陸到一個 FTP 服務器,並自動輸入密碼,等進入服務器以后,先輸入幾個預定義的命令,然后將控制權交還給用戶,用戶操作完成后按 ctrl+] 表示自己操作完成了,腳本再自動退出 ftp 登陸。

!/usr/bin/env python

import sys
import pexpect
ftpPrompt = 'ftp>' # FTP服務器的標准提示符
process = pexpect.spawn('ftp sw-tftp') # 啟動FTP服務器,並將運行期間的輸出都放到標准輸出中
process.logfile_read = sys.stdout
process.expect('[Nn]ame') # 服務器登陸過程
process.sendline('dev')
process.expect('[Pp]assword')
process.sendline('abcd1234')
cmdList = ("passive", 'hash') # 先自動輸入一些預定命令
for cmd in cmdList:
process.expect(ftpPrompt)
process.sendline(cmd)
process.expect(ftpPrompt)

在這里將FTP控制權交還給用戶,用戶輸入完成后按 ctrl+] 再將控制權還給腳本

ctrl+] 交還控制權給腳本是默認值,用戶還可以設置其他的值,比如 \x2a

就是用戶按星號的時候交還。這個值實際上是 ASCII 的16進制碼,它們的對應關系

可以自己去其他地方找一下,但是注意必須是16進制的,並且前綴是 \x

process.interact()

當用戶將控制權交還給腳本后,再由腳本退出ftp服務器

注意下面這個空的sendline()命令,它很重要。用戶將控制權交還給腳本的時候,

腳本緩存里面是沒任何內容的,所以也不可能匹配,這里發送一個回車符會從服務器取得

一些內容,這樣就可以匹配了。

process.sendline()

process.expect(ftpPrompt)
process.sendline('by')
process.expect(pexpect.EOF) # 最后的EOF是確認FTP連接完成的方法。

上面的腳本實際上缺少很多錯誤處理,比如登陸以后用戶名或者密碼錯誤,或者無法連接服務器之類的,但是核心動作是完整的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM