請(qǐng)不要轉(zhuǎn)載
背景介紹
Expect 程序主要用于人機(jī)對(duì)話的模擬郑原,就是那種系統(tǒng)提問奇昙,人來回答 yes/no 纤控,或者賬號(hào)登錄輸入用戶名和密碼等等的情況欢瞪。因?yàn)檫@種情況特別多而且繁瑣活烙,所以很多語言都有各種自己的實(shí)現(xiàn)。最初的第一個(gè) Expect 是由 TCL 語言實(shí)現(xiàn)的遣鼓,所以后來的 Expect 都大致參考了最初的用法和流程啸盏,整體來說大致的流程包括:
- 運(yùn)行程序
- 程序要求人的判斷和輸入
- Expect 通過關(guān)鍵字匹配
- 根據(jù)關(guān)鍵字向程序發(fā)送符合的字符串
TCL 語言實(shí)現(xiàn)的 Expect 功能非常強(qiáng)大,我曾經(jīng)用它實(shí)現(xiàn)了防火墻設(shè)備的完整測(cè)試平臺(tái)骑祟。也因?yàn)樗褂梅奖慊嘏场⒎秶鷱V气笙,幾乎所有腳本語言都實(shí)現(xiàn)了各種各樣的類似與Expect的功能,它們叫法雖然不同粉怕,但原理都相差不大
pexpect 是 Python 語言的類 Expect 實(shí)現(xiàn)健民。從我的角度來看抒巢,它在功能上與 TCL 語言的實(shí)現(xiàn)還是有一些差距贫贝,比如沒有buffer_full 事件、比如沒有 expect before/after 事件等蛉谜,但用來做一般的應(yīng)用還是足夠了稚晚。
基本使用流程
pexpect 的使用說來說去,就是圍繞3個(gè)關(guān)鍵命令做操作:
- 首先用 spawn 來執(zhí)行一個(gè)程序
- 然后用 expect 來等待指定的關(guān)鍵字型诚,這個(gè)關(guān)鍵字是被執(zhí)行的程序打印到標(biāo)準(zhǔn)輸出上面的
- 最后當(dāng)發(fā)現(xiàn)這個(gè)關(guān)鍵字以后客燕,根據(jù)關(guān)鍵字用 send 方法來發(fā)送字符串給這個(gè)程序
第一步只需要做一次,但在程序中會(huì)不停的循環(huán)第二狰贯、三步來一步一步的完成整個(gè)工作也搓。掌握這個(gè)概念之后 pexpect 的使用就很容易了。當(dāng)然 pexpect 不會(huì)只有這 3 個(gè)方法涵紊,實(shí)際上還有很多外圍的其他方法傍妒,我們一個(gè)一個(gè)來說明
API
spawn() - 執(zhí)行程序
spawn() 方法用來執(zhí)行一個(gè)程序,它返回這個(gè)程序的操作句柄摸柄,以后可以通過操作這個(gè)句柄來對(duì)這個(gè)程序進(jìn)行操作颤练,比如:
process = pexpect.spawn('ftp sw-tftp')
上面 spawn() 中的字符串就是要執(zhí)行的程序,這里我們打開一個(gè)到 sw-tftp 服務(wù)器的 ftp 連接驱负。 spawn() 中的第一個(gè)元素就是要執(zhí)行的命令嗦玖,除此之外還可以指定一些其他參數(shù),比如: pexpect.spawn('ftp sw-tftp', timeout=60)
就指定了超時(shí)時(shí)間跃脊,這些具體的會(huì)在后面講解宇挫。
process 就是 spawn() 的程序操作句柄了,之后對(duì)這個(gè)程序的所有操作都是基于這個(gè)句柄的酪术,所以它可以說是最重要的部分捞稿。盡量給它起個(gè)簡(jiǎn)短點(diǎn)的名字,不然后面的程序要多打不少字的拼缝。-
注意: spawn() 娱局,或者說 pexpect 并不會(huì)轉(zhuǎn)譯任何特殊字符 比如 | * 字符在Linux的shell中有特殊含義,但是在 pexpect 中不會(huì)轉(zhuǎn)譯它們咧七,如果在 linux 系統(tǒng)中想使用這些符號(hào)的正確含義就必須加上 shell 來運(yùn)行衰齐,這是很容易犯的一個(gè)錯(cuò)誤。
正確的方式:
process = pexpect.spawn('/bin/bash –c "ls –l | grep LOG > log_list.txt"')
process.expect(pexpect.EOF)
spawn() 還有一種調(diào)用方式就是第一個(gè)參數(shù)是主程序继阻,而下一個(gè)參數(shù)是主程序的參數(shù)耻涛,理解起來很麻煩废酷?看看實(shí)際代碼吧:
cmd = "ls –l | grep LOG > log_list.txt"
process = pexpect.spawn("/bin/bash", ["-c", cmd])
process.expect(pexpect.EOF)
這些代碼和上面一個(gè)例子是相同的,是不是更清晰一些抹缕?
spawn 的選項(xiàng)包括下面這些:
timeout - 超時(shí)時(shí)間
默認(rèn)值: 30 (單位:秒)
指定程序的默認(rèn)超時(shí)時(shí)間澈蟆。程序被啟動(dòng)之后會(huì)有輸出,我們也會(huì)在腳本中檢查輸出中的關(guān)鍵字是否是已知并處理的卓研,如果指定時(shí)間內(nèi)沒找到程序就會(huì)出錯(cuò)返回趴俘。
maxread - 緩存設(shè)置
默認(rèn)值: 2000 (單位:字符)
指定一次性試著從命令輸出中讀多少數(shù)據(jù)。如果設(shè)置的數(shù)字比較大奏赘,那么從 TTY 中讀取數(shù)據(jù)的次數(shù)就會(huì)少一些寥闪。
設(shè)置為 1 表示關(guān)閉讀緩存。
設(shè)置更大的數(shù)值會(huì)提高讀取大量數(shù)據(jù)的性能磨淌,但會(huì)浪費(fèi)更多的內(nèi)存疲憋。這個(gè)值的設(shè)置與 searchwindowsize 合作會(huì)提供更多功能。
緩存的大小并不會(huì)影響獲取的內(nèi)容梁只,也就是說如果一個(gè)命令輸出超過2000個(gè)字符以后缚柳,先前緩存的字符不會(huì)丟失掉,而是放到其他地方去搪锣,當(dāng)你用 self.before (這里 self 代表 spawn 的實(shí)例)還是可以取到完整的輸出的秋忙。
searchwindowsize - 模式匹配閥值
默認(rèn)值: None
searchwindowsize 參數(shù)是與 maxread 參數(shù)一起合作使用的,它的功能比較微妙淤翔,但可以顯著減少緩存中有很多字符時(shí)的匹配時(shí)間翰绊。
默認(rèn)情況下, expect() 匹配指定的關(guān)鍵字都是這樣:每次緩存中取得一個(gè)字符時(shí)就會(huì)對(duì)整個(gè)緩存中的所有內(nèi)容匹配一次正則表達(dá)式旁壮,你可以想像如果程序的返回特別多的時(shí)候监嗜,性能會(huì)多么的低。
設(shè)置 searchwindowsize 的值表示一次性收到多少個(gè)字符之后才匹配一次表達(dá)式抡谐,比如現(xiàn)在有一條命令會(huì)出現(xiàn)大量的輸出裁奇,但匹配關(guān)鍵字是標(biāo)準(zhǔn)的 FTP 提示符 ftp> ,顯然要匹配的字符只有 5 個(gè)(包括空格)麦撵,但是默認(rèn)情況下每當(dāng) expect 獲得一個(gè)新字符就從頭匹配一次這幾個(gè)字符刽肠,如果緩存中已經(jīng)有了 1W 個(gè)字符,一次一次的從里面匹配是非常消耗資源的免胃,這個(gè)時(shí)候就可以設(shè)置 searchwindowsize=10, 這樣 expect 就只會(huì)從最新的(最后獲取的) 10 個(gè)字符中匹配關(guān)鍵字了音五,如果設(shè)置的值比較合適的話會(huì)顯著提升性能。不用擔(dān)心緩存中的字符是否會(huì)被丟棄羔沙,不管有多少輸出躺涝,只要不超時(shí)就總會(huì)得到所有字符的,這個(gè)參數(shù)的設(shè)置僅僅影響匹配的行為扼雏。
這個(gè)參數(shù)一般在 expect() 命令中設(shè)置坚嗜, pexpect 2.x 版本似乎有一個(gè) bug 夯膀,在 spawn 中設(shè)置是不生效的。
logfile - 運(yùn)行輸出控制
默認(rèn)值: None
當(dāng)給 logfile 參數(shù)指定了一個(gè)文件句柄時(shí)苍蔬,所有從標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出獲得的內(nèi)容都會(huì)寫入這個(gè)文件中(注意這個(gè)寫入是 copy 方式的)诱建,如果指定了文件句柄,那么每次向程序發(fā)送指令(process.send)都會(huì)刷新這個(gè)文件(flush)碟绑。
這里有一個(gè)很重要的技巧:如果你想看到spawn過程中的輸出俺猿,那么可以將這些輸出寫入到 sys.stdout 里去,比如:
process = pexpect.spawn("ftp sw-tftp", logfile=sys.stdout)
用這樣的方式可以看到整個(gè)程序執(zhí)行期間的輸入和輸出蜈敢,很適合調(diào)試辜荠。
還有一個(gè)例子:
process = pexpect.spawn("ftp sw-tftp")
logFileId = open("logfile.txt", 'w')
process.logfile = logFileId
注意: logfile.txt 文件里汽抚,既包含了程序運(yùn)行時(shí)的輸出抓狭,也包含了 spawn 向程序發(fā)送的內(nèi)容,有的時(shí)候你也許不希望這樣造烁,因?yàn)槟承﹥?nèi)容出現(xiàn)了2次否过,那么還有 2 個(gè)很重要的 logfile 關(guān)聯(lián)參數(shù):
logfile_read - 獲取標(biāo)準(zhǔn)輸出的內(nèi)容
默認(rèn)值: None
記錄執(zhí)行程序中返回的所有內(nèi)容,也就是去掉你發(fā)出去的命令惭蟋,而僅僅只包括命令結(jié)果的部分:
process.logfile_read = sys.stdout
上面的語句會(huì)在屏幕上打印程序執(zhí)行過程中的所有輸出苗桂,但是一般不包含你向程序發(fā)送的命令,不過大部分程序都有回顯機(jī)制告组,比如發(fā)命令的時(shí)候設(shè)備不光接收到命令字符串煤伟,還會(huì)反向在你的終端上把字符串顯示出來讓你明白哪些字符被輸入了,這種時(shí)候也是會(huì)被這個(gè)方法讀到的木缝。只有那些不會(huì)回顯的情況 logfile_read 才會(huì)拿不到便锨,比如輸入密碼的時(shí)候。
logfile_send - 獲取發(fā)送的內(nèi)容
默認(rèn)值: None
記錄向執(zhí)行程序發(fā)送的所有內(nèi)容
process.logfile_send = sys.stdout
上面的語句僅僅在屏幕上打印向程序發(fā)送的內(nèi)容我碟。
cwd - 指定命令執(zhí)行的目錄
默認(rèn)值: None 或者說 ./
cwd 用來指定命令發(fā)送的命令在哪個(gè)路徑下執(zhí)行放案,它一般是用在 send() 系列命令中,比如在 Linux 中矫俺,你想在 /etc 目錄下執(zhí)行 ls –l 命令吱殉,那么完全不需要用 sendline("cd /etc && ls -l")
這樣的方式,而是用 sendline("ls –l", cwd="/etc")
就可以了厘托。
env - 指定環(huán)境變量
默認(rèn)值: None
指定環(huán)境變量的值友雳,這個(gè)值是一個(gè)字典,如果你發(fā)送的命令要使用一些環(huán)境變量铅匹,那么可以在這里提供
ignore_sighup - 是否過濾 SIGHUP 信號(hào)
默認(rèn)值: True
這個(gè)參數(shù)是 pexpect 3.1 開始引入的押赊,在 3.1 之前(比如 pexpect 2.3),spawn 的子程序會(huì)過濾 SIGHUP 信號(hào)伊群,也就是用 Ctrl+C 是不能終止子程序的考杉,3.1的默認(rèn)值也繼承了這個(gè)行為策精,但是如果設(shè)置 ignore_sighup = False 就可以改變這個(gè)行為。
delaybeforesend - 字符發(fā)送延時(shí)
默認(rèn)值: 0.05
這是一個(gè)隱藏參數(shù)用來設(shè)置發(fā)送字符串之前的延時(shí)崇棠。增加這個(gè)參數(shù)的最大理由是因?yàn)楹芏嗳伺鲆娺@樣一個(gè)問題:
在 FTP 程序中登錄時(shí)如果用腳本輸入密碼時(shí)會(huì)直接顯示出來咽袜。這是基于一個(gè)一般人不可思議的事實(shí):當(dāng) FTP 登錄時(shí),實(shí)際上服務(wù)器會(huì)先打印要求你輸入密碼的提示符枕稀,然后再發(fā)一個(gè)信號(hào)把回顯功能取消询刹,當(dāng)人使用鍵盤輸入的時(shí)候因?yàn)檫@個(gè)動(dòng)作延時(shí)比較高所以不可能看到回顯的密碼,但腳本會(huì)在發(fā)現(xiàn)輸入密碼的提示符時(shí)立即發(fā)送萎坷,于是密碼就會(huì)在關(guān)閉回顯之前出現(xiàn)了凹联。 Pexpect 為了解決這個(gè)問題在每次發(fā)送字符前默認(rèn)等待 50 毫秒,如果你認(rèn)為不必要的話就可以自己設(shè)置為 0 來取消這個(gè)行為哆档。
expect() - 關(guān)鍵字匹配
當(dāng) spawn() 啟動(dòng)了一個(gè)程序并返回程序控制句柄后蔽挠,就可以用 expect() 方法來等待指定的關(guān)鍵字了。它最后會(huì)返回 0 表示匹配到了所需的關(guān)鍵字瓜浸,如果后面的匹配關(guān)鍵字是一個(gè)列表的話澳淑,就會(huì)返回一個(gè)數(shù)字表示匹配到了列表中第幾個(gè)關(guān)鍵字,從 0 開始計(jì)算插佛。
expect() 利用正則表達(dá)式來匹配所需的關(guān)鍵字杠巡。(正則表達(dá)式使用范圍非常廣,幾乎所有語言都對(duì)它提供支持雇寇,如果不知道如何使用的話氢拥,可以參考我的另一份文檔《正則表達(dá)式參考》)。
它的使用方式:
# pattern_list 正則表達(dá)式列表锨侯,表示要匹配這些內(nèi)容
# timeout 不設(shè)置或者設(shè)置為-1的話嫩海,超時(shí)時(shí)間就采用self.timeout的值,默認(rèn)是30秒识腿。也可以自己設(shè)置出革。
# searchwindowsize 功能和 spawn 上的一樣,但是渡讼!請(qǐng)注意這個(gè)但是骂束!下面會(huì)實(shí)際說明
process.expect(pattern_list, timeout=-1, searchwindowsize=None)
在這里的 searchwindowsize 是在 expect() 方法中真正生效的,默認(rèn)情況下是 None成箫,也就是每從子進(jìn)程中獲取一個(gè)字符就做一次完整匹配展箱,如果子進(jìn)程的輸出很多的話……性能會(huì)非常低。如果設(shè)置為其他的值蹬昌,表示從子進(jìn)程中讀取到多少個(gè)字符才做一次匹配混驰,這樣會(huì)顯著減少匹配的次數(shù),增加性能。
經(jīng)過測(cè)試栖榨,對(duì)于一個(gè)有 48100000 個(gè)字符的子進(jìn)程昆汹,將 searchwindowsize 設(shè)置為 2000 時(shí),完全處理完成需要 73.2730 秒婴栽;同樣的子進(jìn)程將這個(gè)參數(shù)設(shè)置為 None 則需要 1949.6259 秒满粗,Oh, my Lady GAGA…… 完全是指數(shù)上升啊。
-
最簡(jiǎn)單的匹配方式
process.expect('[Nn]ame')
上面的代碼表示:匹配 process 這個(gè)句柄(代表 spawn 方法的例子中我們啟動(dòng)的 ftp 連接)中的 name 關(guān)鍵字愚争,其中 n 不分大小寫焦匈。
上面的關(guān)鍵字一旦匹配翩伪,就會(huì)返回0表示匹配成功着倾,但是如果一直匹配不到呢寺旺?默認(rèn)是會(huì)一直等下去,但是如果設(shè)置了 timeout 的話就會(huì)超時(shí)鞍陨。
-
匹配一系列輸出
實(shí)際上步淹, expect() 可以匹配一系列輸出,通過檢查匹配到的輸出湾戳,我們可以做不同的事情贤旷。比如之前 spawn 的 ftp 連接广料,如果我們輸入用戶名之后有不同的情況砾脑,就可以通過監(jiān)控這些不同情況來做不同的動(dòng)作,比如:
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 方法中的是一個(gè)列表韧衣,列表中的每個(gè)元素都是一個(gè)關(guān)鍵字的正則表達(dá)式,也就是說我們期待這 3 種情況之一购桑,而 expect 返回一個(gè)順序值來代表我匹配到了哪一個(gè)元素(也就是發(fā)生了哪種情況了)畅铭,這個(gè)順序值是從 0 開始計(jì)算的。
當(dāng)expect之后勃蜘,下面的 if 語句就開始處理這 3 種情況了:
- 權(quán)限不足硕噩,這可能是 ftp 服務(wù)器出現(xiàn)問題,或者沒有這個(gè)帳號(hào)缭贡,或者其他什么情況炉擅,反正只要發(fā)現(xiàn)這種情況的話,我們就給用戶提示一下阳惹,然后殺掉這個(gè)進(jìn)程
- 登陸成功谍失,但還要用戶指定終端模式才能真正使用,所以我們?cè)诖a中指定了 vty100 這種模式莹汤,然后看是不是能真正使用了
- 還是登陸成功了快鱼,而且還可以直接輸入命令操作 ftp 服務(wù)器了,于是我們提示用戶,然后把操作權(quán)限交給用戶
另外有一種特殊情況抹竹,如果同時(shí)有2個(gè)被匹配到线罕,那么怎么辦?簡(jiǎn)單來說就是這樣:
- 原始流中窃判,第一個(gè)被關(guān)鍵字匹配到的內(nèi)容會(huì)被使用
- 匹配關(guān)鍵字列表中闻坚,最左邊的會(huì)被使用
給個(gè)例子:
# 如果流里面的內(nèi)容是 "hello world" index = process.expect(["hi", "hello", "hello world"])
返回的值是 1,也就是 'hello' 被匹配到了兢孝,哪怕真正最好的匹配是 "hello world" 但因?yàn)榉旁诤竺嫠匀匀粺o效窿凤。
使用技巧
如果要檢查或者匹配 expect.EOF 和 expect.TIMEOUT 這兩種情形,那么必須將它們放進(jìn)匹配列表里面去跨蟹,這樣可以通過檢查返回的數(shù)字來處理它們雳殊。如果沒放進(jìn)列表的話,就會(huì)發(fā)生 EOF 或者 TIMEOUT 錯(cuò)誤窗轩,程序就會(huì)中途停止了
-
匹配規(guī)則中有些特殊語法夯秃,比如下面的規(guī)則中前 2 個(gè)匹配都是大小寫無關(guān)的,關(guān)鍵就是這個(gè) (?i) 匹配規(guī)則痢艺,它相當(dāng)于 re.IGNORE 或者 re.I 這個(gè)關(guān)鍵字仓洼,因?yàn)楫吘共皇钦嬲恼齽t表達(dá)式引擎,所以 pexpect 使用這樣特殊語法:
child.expect(['(?i)etc', '(?i)readme', pexpect.EOF, pexpect.TIMEOUT])
expect_exact() - 精確匹配
它的使用和 expect() 是一樣的堤舒,唯一不同的就是它的匹配列表中不再使用正則表達(dá)式色建。
從性能上來說 expect_exact() 要更好一些,因?yàn)榧词鼓銢]有使用正則表達(dá)式而只是簡(jiǎn)單的用了幾個(gè)字符 expect() 也會(huì)先將它們轉(zhuǎn)換成正則表達(dá)式模式然后再搜索舌缤,但 expect_exact() 不會(huì)箕戳,而且也不會(huì)把一些特殊符號(hào)轉(zhuǎn)換掉。
expect_list() - 預(yù)轉(zhuǎn)換匹配
使用方式和 expect() 一樣国撵,唯一不同的就是它里面接受的正則表達(dá)式列表只會(huì)轉(zhuǎn)換一次陵吸。
expect() 稍微有點(diǎn)笨,每調(diào)用一次它都會(huì)將內(nèi)部的正則表達(dá)式轉(zhuǎn)換一次(當(dāng)然也有其他辦法避免)介牙,如果你是在以后循環(huán)中調(diào)用 expect() 的話壮虫,多余的轉(zhuǎn)換動(dòng)作就會(huì)降低性能,在這種情況下建議用 expect_list() 來代替环础。
使用方法:
# timeout 為 -1 的話使用 self.timeout 的值
# searchwindowsize 為 -1 的話囚似,也使用系統(tǒng)默認(rèn)的值
process.expect_list(pattern_list, timeout=-1, searchwindowsize=-1)
expect_loop()
用于從標(biāo)準(zhǔn)輸入中獲取內(nèi)容,loop這個(gè)詞代表它會(huì)進(jìn)入一個(gè)循環(huán)喳整,必須要從標(biāo)準(zhǔn)輸入中獲取到關(guān)鍵字才會(huì)往下繼續(xù)執(zhí)行谆构。
使用方法:
expect_loop(self, searcher, timeout=-1, searchwindowsize=-1)
send() - 發(fā)送關(guān)鍵字
send() 作為3個(gè)關(guān)鍵操作之一,用來向程序發(fā)送指定的字符串框都,它的使用沒什么特殊的地方搬素,比如:
process.expect("ftp>")
process.send("by\n")
這個(gè)方法會(huì)返回發(fā)送字符的數(shù)量呵晨。
sendline() - 發(fā)送帶回車符的字符串
sendline() 和 send() 唯一的區(qū)別就是在發(fā)送的字符串后面加上了回車換行符,這也使它們用在了不同的地方:
- 只需要發(fā)送字符就可以的話用send()
- 如果發(fā)送字符后還要回車的話熬尺,就用 sendline()
它也會(huì)返回發(fā)送的字符數(shù)量
sendcontrol() - 發(fā)送控制信號(hào)
sendcontrol() 向子程序發(fā)送控制字符摸屠,比如 <kbd>ctrl+C</kbd> 或者 <kbd>ctrl+D</kbd> 之類的,比如你要向子程序發(fā)送 <kbd>ctrl+G</kbd>粱哼,那么就這樣寫:
process.sendcontrol('g')
sendeof() - 發(fā)送 EOF 信號(hào)
向子程序發(fā)送 End Of File 信號(hào)季二。
sendintr() - 發(fā)送終止信號(hào)
向子程序發(fā)送 SIGINT 信號(hào),相當(dāng)于 Linux 中的 kill 2 揭措,它會(huì)直接終止掉子進(jìn)程胯舷。
interact() - 將控制權(quán)交給用戶
interact() 表示將控制權(quán)限交給用戶(或者說標(biāo)準(zhǔn)輸入)。一般情況下 pexpect 會(huì)接管所有的輸入和輸出绊含,但有的時(shí)候還是希望用戶介入桑嘶,或者僅僅是為了完成一部分工作的時(shí)候, interact() 就很有用了躬充。
比如:
- 登陸 ftp 服務(wù)器的時(shí)候逃顶,在輸入用戶密碼階段希望用戶手工輸入密碼,然后腳本完成剩余工作時(shí)(將用戶密碼寫在腳本中可不安全)
- 只希望完成登陸工作充甚,比如要 ssh 連接到一臺(tái)遠(yuǎn)方的服務(wù)器以政,但中間要經(jīng)過好幾跳,用手工輸入實(shí)在太麻煩伴找,所以就用腳本先跳到目的服務(wù)器上盈蛮,然后再把控制權(quán)限還給用戶做操作。
使用方法:
# escape_character 就是當(dāng)用戶輸出這里指定的字符以后表示自己的操作完成了疆瑰,將控制權(quán)重新交給 pexpect
process.interact(escape_character='\x1d', input_filter=None, output_filter= None)
詳細(xì)來說眉反,這個(gè)方法將控制權(quán)交給用戶(或者說用戶操作的鍵盤),然后簡(jiǎn)單的將標(biāo)準(zhǔn)輸出穆役、標(biāo)準(zhǔn)錯(cuò)誤輸出和標(biāo)準(zhǔn)輸入綁定到系統(tǒng)上來。
通過設(shè)置 escape_character 的值梳凛,可以定義返回碼耿币,默認(rèn)是 <kbd>ctrl+]</kbd> 或者說 <kbd>^]</kbd>,當(dāng)輸入了返回碼以后韧拒,腳本會(huì)將控制權(quán)從用戶那里重新拿回來淹接,然后繼續(xù)向下執(zhí)行。
close() - 停止應(yīng)用程序
如果想中途關(guān)閉子程序叛溢,那么可以用 close 來完成塑悼,調(diào)用這個(gè)方法后會(huì)返回這個(gè)程序的返回值。
如果設(shè)置 force=True 會(huì)強(qiáng)行關(guān)閉這個(gè)程序楷掉,大概的過程就是先發(fā)送 SIGHUP 和 SIGINT 信號(hào)厢蒜,如果都無效的話就發(fā) SIGKILL 信號(hào),反正不管怎么樣都會(huì)保證這個(gè)程序被關(guān)閉掉。
多次調(diào)用這個(gè)方法是允許的斑鸦,但是不保證每次都能返回正確的返回值愕贡。盡量不要這么做,如果想保證程序被關(guān)閉的話只要設(shè)置force的值就可以了巷屿。
下面是實(shí)例:
process.close(force=True)
terminate() - 停止應(yīng)用程序
可以看作是上面 close() 的別名固以,因?yàn)椴还苁枪δ苓€是使用方法都是一樣的。
Kill() - 發(fā)送 SIGKILL 信號(hào)
向子程序發(fā)送 SIGKILL 的信號(hào)嘱巾。
flush()
什么都不干憨琳,只是為了與文件方法兼容而已。
isalive() - 檢查子程序運(yùn)行狀態(tài)
檢查被調(diào)用的子程序是否正在運(yùn)行旬昭,這個(gè)方法是運(yùn)行在非阻斷模式下面的栽渴。
如果獲得的返回是 True 表示子程序正在運(yùn)行;返回 False 則表示程序運(yùn)行終止稳懒。
isatty() - 檢查是否運(yùn)行在 TTY 設(shè)備上
返回 True 表示打開和連接到了一個(gè) tty 類型的設(shè)備闲擦,或者返回 False 表示未連接。
next() - 返回下一行內(nèi)容
和操作文件一樣场梆,這個(gè)方法也是返回緩存中下一行的內(nèi)容墅冷。
read() - 返回剩下的所有內(nèi)容
獲取子程序返回的所有內(nèi)容,一般情況下我們可以用 expect 來期待某些內(nèi)容或油,然后通過 process.before 這樣的方式來獲取寞忿,但這種方式有一個(gè)前提:那就是必須先 expect 某些字符,然后才能用 process.before 來獲取緩存中剩下的內(nèi)容顶岸。
read() 的使用很不同腔彰,它期待一個(gè) EOF 信號(hào),然后將直到這個(gè)信號(hào)之前的所有輸出全部返回辖佣,就像讀一個(gè)文件那樣霹抛。
一般情況下,交互式程序只有關(guān)閉的時(shí)候才會(huì)返回 EOF 卷谈,比如用 by 命令關(guān)閉 ftp 服務(wù)器杯拐,或者用 exit 命令關(guān)閉一個(gè) ssh 連接。
這個(gè)方法使用范圍比較狹窄世蔗,因?yàn)橥耆梢杂?expect.EOF 方式來代替端逼。當(dāng)然如果是本機(jī)命令,每執(zhí)行完一次之后都會(huì)返回 EOF 污淋,這種情況下倒是很有用:
process = pexpect.spawn('ls –l')
output = process.read()
print output
看起來這么做有點(diǎn)無聊顶滩?但我想一定有什么理由支持這個(gè)方法。
可以用指定 read(size=-1) 的方式來設(shè)置返回的字符數(shù)寸爆,如果沒有設(shè)置或者設(shè)置為負(fù)數(shù)則返回所有內(nèi)容礁鲁,正數(shù)則返回指定數(shù)量的內(nèi)容盐欺,返回的內(nèi)容是字符串形式。
readline() - 返回一行輸出
返回一行輸出救氯,返回的內(nèi)容包括最后的\r\n字符找田。
也可以設(shè)置 readline(size=-1) 來指定返回的字符數(shù),默認(rèn)是負(fù)數(shù)表示返回所有的着憨。
readlines() - 返回列表模式的所有輸出
返回一個(gè)列表墩衙,列表中的每個(gè)元素都是一行(包括\r\n字符)。
setecho() - 子程序響應(yīng)模式
設(shè)置子程序運(yùn)行時(shí)的響應(yīng)方式甲抖,一般情況下向子程序發(fā)送字符的時(shí)候漆改,這些字符都會(huì)在標(biāo)準(zhǔn)輸出上顯示出來,這樣你可以看到你發(fā)送出去的內(nèi)容准谚,但是有的時(shí)候挫剑,我們不需要顯示,那么就可以用這個(gè)方法來設(shè)置了柱衔。
注意樊破,必須在發(fā)送字符之前設(shè)置,設(shè)置之后在之后的代碼中都一直有效唆铐。比如:
process = pexpect.spawn('cat')
# 默認(rèn)情況下哲戚,下面的1234這個(gè)字符串會(huì)顯示2次,一次是pexpect返回的艾岂,一次是cat命令返回的
process.sendline("1234")
# 現(xiàn)在我們關(guān)閉pexpect()的echo功能
process.setecho(False)
# 下面的字符只會(huì)顯示一次了顺少,這是由cat返回的
process.sendline("abcd")
# 現(xiàn)在重新開啟echo功能,就可以再次看到我們發(fā)送的字符了
process.setecho(True)
setwinsize() - 控制臺(tái)窗口大小
如果子程序是一個(gè)控制臺(tái)(TTY)王浴,比如 SSH 連接脆炎、 Telnet 連接這種通過網(wǎng)絡(luò)登陸到系統(tǒng)并發(fā)送命令的都算控制臺(tái),那么可以用這個(gè)方法來設(shè)置這個(gè)控制太的大忻ダ薄(或者說長寬)秒裕。
它的調(diào)用方式是 process.setwinsize(r, c)
默認(rèn)值是 setwinsize(24, 80)
,其中 24 是高度筛婉,單位是行簇爆; 80 是寬度,單位是字符爽撒。
為什么要用它?想像下面的場(chǎng)景:
有的時(shí)候你通過pexpect登陸到某個(gè)ssh控制臺(tái)之后响蓉,又用 interact() 來將控制權(quán)交給用戶硕勿,然后用戶到控制臺(tái)里面寫自己的命令,如果命令比較長枫甲,就會(huì)發(fā)現(xiàn)當(dāng)命令到屏幕邊緣之后不會(huì)自動(dòng)換行源武,而是又返回到這一行的最前面重新覆蓋前面的字符扼褪;這不會(huì)影響命令的實(shí)際效果,但是很惱人粱栖。
這種情況用 setwinsize() 就可以解決话浇,找到自己終端支持的長度,重新設(shè)置一下闹究,比如 setwinsize(25, 96 )幔崖,如果設(shè)置的正確的話就可以解決了。
wait() - 執(zhí)行等待
直到被調(diào)用的子程序執(zhí)行完畢之前渣淤,程序都停止(或者說等待)執(zhí)行赏寇。它不會(huì)從被調(diào)用的子程序中讀取任何內(nèi)容。
waitnoecho()
它使用的地方比較特殊价认,唯一匹配的地方就是:當(dāng)子程序的 echo 功能被設(shè)置為 Fals 時(shí)嗅定。
看起來很奇怪?其實(shí)這個(gè)功能是基于一個(gè)很讓人難以置信但的確是真實(shí)的情況:
在命令行模式下用踩,很多要求輸入密碼的地方渠退,比如 FTP/SSH 等,密碼實(shí)際上都會(huì)在你輸入之后又重新返回并打印出來的脐彩,但是為什么我們看不到我們自己輸入的密碼呢碎乃?這就是因?yàn)槊艽a在要打印出來之前被程序?qū)?echo 功能設(shè)置為 False 了。
現(xiàn)在知道為什么有這么一個(gè)方法了吧丁屎?比如要進(jìn)行一個(gè) ssh 連接時(shí)荠锭,如何檢查是否要輸入密碼?用關(guān)鍵字 password 是一個(gè)方法晨川,但還有一個(gè)方法就是這樣:
# 啟動(dòng)ssh連接
process = pexpect.spawn("ssh user@example.com")
# 等待echo被設(shè)置為False证九,這就意味著本地不會(huì)有回顯
process.waitnoecho()
process.sendline('mypassword')
可以設(shè)置超時(shí)時(shí)間,默認(rèn)是:waitnoecho(timeout=-1)
共虑,表示和系統(tǒng)設(shè)置的超時(shí)時(shí)間相同愧怜,也可以設(shè)置為 None 表示永遠(yuǎn)等待,直到回顯被設(shè)置為 False 妈拌,當(dāng)然還可以設(shè)置其他的數(shù)字來表示超時(shí)時(shí)間拥坛。
write() - 發(fā)送字符串
類似于send()命令,只不過不會(huì)返回發(fā)送的字符數(shù)尘分。
writelines() - 發(fā)送包含字符串的列表
類似于 write() 命令猜惋,只不過接受的是一個(gè)字符串列表, writelines() 會(huì)向子程序一條一條的發(fā)送列表中的元素培愁,但是不會(huì)自動(dòng)在每個(gè)元素的最后加上回車換行符著摔。
與 write() 相似的是,這個(gè)方法也不會(huì)返回發(fā)送的字符數(shù)量定续。
特殊變量
pexpect.EOF - 匹配終止信號(hào)
EOF 變量使用范圍很廣泛谍咆,比如檢查 ssh/ftp/telnet 連接是否終止啊禾锤,文件是否已經(jīng)到達(dá)末尾啊。 pexpect 大部分腳本的最后都會(huì)檢查 EOF 變量來判斷是不是正常終止和退出摹察,比如下面的代碼:
process.expect("ftp>")
process.sendline("by")
process.expect(pexpect.EOF)
print "ftp connect terminated."
pexpect.TIMEOUT - 匹配超時(shí)信號(hào)
TIMEOUT 變量用來匹配超時(shí)的情況恩掷,默認(rèn)情況下 expect 的超時(shí)時(shí)間是 60 秒,如果超過 60 秒還沒有發(fā)現(xiàn)期待的關(guān)鍵字供嚎,就會(huì)觸發(fā)這個(gè)行為黄娘,比如:
# 匹配pexpect.TIMEOUT的動(dòng)作,只有超時(shí)事件發(fā)生的時(shí)候才會(huì)有效
index = process.expect(['ftp>', pexpect.TIMEOUT],)
if index == 1:
process.interactive() ; # 將控制權(quán)交給用戶
elif index == 2:
print "Time is out."
process.kill(0) ; # 殺掉進(jìn)程
# 那么怎么改變超時(shí)時(shí)間呢查坪?其實(shí)可以修改spawn對(duì)象里的timeout參數(shù):
# 下面的例子僅僅加了一行寸宏,這樣就改變了超時(shí)的時(shí)間了
process.timeout = 300 ; # 注意這一行
index = process.expect(['ftp>', pexpect.TIMEOUT],)
if index == 1:
process.interactive() ; # 將控制權(quán)交給用戶
elif index == 2:
print "Time is out."
process.kill(0) ; # 殺掉進(jìn)程
process.before/after/match - 獲取程序運(yùn)行輸出
當(dāng) expect() 過程匹配到關(guān)鍵字(或者說正則表達(dá)式)之后,系統(tǒng)會(huì)自動(dòng)給3個(gè)變量賦值偿曙,分別是 before, after 和 match
- process.before - 保存了到匹配到關(guān)鍵字為止氮凝,緩存里面已有的所有數(shù)據(jù)。也就是說如果緩存里緩存了 100 個(gè)字符的時(shí)候終于匹配到了關(guān)鍵字望忆,那么 before 就是除了匹配到的關(guān)鍵字之外的所有字符
- process.after - 保存匹配到的關(guān)鍵字罩阵,比如你在 expect 里面使用了正則表達(dá)式,那么表達(dá)式匹配到的所有字符都在 after 里面
- process.match - 保存的是匹配到的正則表達(dá)式的實(shí)例启摄,和上面的 after 相比一個(gè)是匹配到的字符串稿壁,一個(gè)是匹配到的正則表達(dá)式實(shí)例
如果 expect() 過程中發(fā)生錯(cuò)誤,那么 before 保存到目前位置緩存里的所有數(shù)據(jù)歉备, after 和 match 都是 None
self.exitstatus | self.signalstatus
上面的2個(gè)值用來保存spawn子程序的退出狀態(tài)傅是,但是注意:只有使用了 process.close() 命令之后這 2 個(gè)參數(shù)才會(huì)被設(shè)置。
其他說明
CR/LF約定
眾所周知的是:世界上有很多種回車換行約定蕾羊,它們給我們?cè)斐闪撕芏嗦闊┬剩热纾?/p>
- windows 中用 \r\n 表示回車換行
- Linux like 系統(tǒng)中用 \r 表示回車換行
- Mac 系統(tǒng)中用 \n 表示回車換行
這種種回車換行約定對(duì)代碼移植造成了很大的困難,幾乎所有全平臺(tái)支持的程序語言都有它們自己的解決方案龟再,而 pexpect 的解決方案就是:
不管哪個(gè)平臺(tái)书闸,回車換行都替換成 \r\n
所以,如果我們要在expect中匹配回車換行符號(hào)的話利凑,就必須這么做:
process.expect('\r\n')
# 想匹配一行里的最后一個(gè)單詞:
process.expect('\w+\r\n')
# 下面的匹配方式是錯(cuò)誤的(在其他腳本語言中是正確的浆劲,比如TCL語言的Expect實(shí)現(xiàn)中,這也是很容易搞混淆的地方):
process.expect('\r')
$ * + 約定
正則表達(dá)式中哀澈, $ 符號(hào)表示從一行中的最后開始匹配——但是在 pexpect 中是無效的牌借。如果要匹配一行的最后,那么必須有一行數(shù)據(jù)存在割按,也就是有回車換行符走哺,但是 pexpect 的處理不是按行來進(jìn)行的,它一次僅僅讀一個(gè)并且處理一個(gè)字符哲虾,而且不會(huì)處理【未來】的數(shù)據(jù)丙躏。
所以不管什么時(shí)候,都不要在 expect() 中用 $ 符號(hào)來匹配束凑。
正因?yàn)閜expect一次僅僅處理一個(gè)字符晒旅,所以加號(hào) (+) 、星號(hào) (*) 的功能也無效了汪诉,比如:
# 無論何時(shí)废恋,都只會(huì)返回一個(gè)字符
process.expect(".+")
# 無論何時(shí),都只會(huì)返回空字符
process.expect(".*")
程序調(diào)試
如果要調(diào)試pexpect扒寄,那么可以使用下面的方式:
str(processHandle)
# 通過 pexpect.spawn() 可以創(chuàng)建一個(gè)進(jìn)程鱼鼓,并通過操作這個(gè)進(jìn)程的句柄來控制程序。
# 但是如果將這個(gè)句柄用 str() 函數(shù)重載一下呢该编?它會(huì)顯示這個(gè)控制句柄的一系列內(nèi)部信息迄本,比如:
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
技巧和陷阱
循環(huán)匹配
Python 的 pexpect 模塊與 TCL 的 expect 相比有些功能明顯支持不足,其中就包括循環(huán)匹配课竣。 TCL 的 expect 模塊可以給出一系列匹配關(guān)鍵字嘉赎,然后通過 continue 語句的設(shè)置保證同一個(gè) expect 可以在關(guān)鍵字列表中重新循環(huán)。
比如一個(gè) expect 有 3 個(gè)關(guān)鍵字于樟,其中匹配到第二個(gè)關(guān)鍵字的時(shí)候會(huì)碰見 continue 語句公条,那么下一次匹配就重復(fù)這個(gè) expect 過程,這是一個(gè)很有用的功能迂曲,比如超時(shí)時(shí)間設(shè)置為 10 秒靶橱,然后重復(fù) 3 次才會(huì)真正超時(shí)的情況。
可惜的是 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."
# 終止循環(huán)
break
獲取 before 中內(nèi)容的戰(zhàn)略與清空 buffer
絕大多數(shù)情況下我們都會(huì)利用before變量來獲取命令執(zhí)行的結(jié)果鬓长。但是谒拴,你真的知道怎么用好 before 么?
before 中到底什么時(shí)候保存你所需的內(nèi)容涉波?這個(gè)細(xì)節(jié)必須非常清楚英上,我們以一個(gè)調(diào)用一個(gè)命令為例子:
這里我們預(yù)計(jì)是在 linux 系統(tǒng)中,下面的 handle 是一個(gè) spawn 后的句柄啤覆,而 prompt 則是 bash 的提示符苍日。我們預(yù)計(jì)做這樣的步驟:
- 匹配提示符,以此判斷系統(tǒng)已經(jīng)準(zhǔn)備好接受命令
- 發(fā)送命令
- 獲取命令執(zhí)行后的結(jié)果
handle.expect(prompt)
handle.sendline("ls –l")
handle.expect(prompt)
output = handle.before
一共 4 個(gè)語句窗声,就可以獲取 ls –l 命令的結(jié)果了相恃,但是且慢,是否發(fā)現(xiàn)有什么不合理的地方笨觅?
第一句和第二句分別是匹配系統(tǒng)提示符和發(fā)送命令拦耐,這都是比較正常的耕腾。
但是為什么第三句是再次匹配系統(tǒng)提示符?在一般的想像下杀糯,發(fā)送命令之后扫俺,設(shè)備就會(huì)執(zhí)行并返回結(jié)果了,那么完全就可以用 handle.before 語句來獲取到這些內(nèi)容了才對(duì)肮毯病狼纬?
實(shí)際上,從 before 這個(gè)單詞就可以大概明白骂际,它并不是實(shí)時(shí)生效的疗琉,它里面的內(nèi)容,實(shí)際上是上一次 expect 匹配之后歉铝,除掉匹配到的關(guān)鍵字本身盈简,系統(tǒng)緩存中剩余下來的全部?jī)?nèi)容。也就是說犯戏,如果第三句就是 output = handle.before的話送火,那么它里面的內(nèi)容就是第一句的那個(gè) expect 中去掉 prompt 內(nèi)容后緩存中剩下來的內(nèi)容。顯然先匪,這里面不會(huì)包括后面 ls –l
命令的內(nèi)容了种吸。
那么想獲取 ls –l
的內(nèi)容,唯一的辦法是再增加一個(gè) expect 關(guān)鍵字匹配呀非。這是非常關(guān)鍵的一點(diǎn)坚俗。
另外, pexpect 中的 buffer 是一個(gè)關(guān)鍵岸裙,但又不能被直接操作的變量猖败,它保存的是運(yùn)行過程中每一個(gè) expect 之后的所有內(nèi)容,隨時(shí)被更新降允。而 before/after 都是直接源于它的恩闻,而 expect 的關(guān)鍵字匹配本身也是在 buffer 中做匹配的。
正因?yàn)樗闹匾跃缍瑢?duì)這個(gè)變量中的內(nèi)容需要特別的警惕幢尚。比如我們將登陸設(shè)備,發(fā)送命令翅楼,退出設(shè)備這3個(gè)步驟寫進(jìn)3個(gè)函數(shù)的時(shí)候尉剩,最好保證每個(gè)步驟都不會(huì)影響下一個(gè)步驟,在每個(gè)步驟開始的時(shí)候毅臊,最好做這樣的操作:
handle.buffer = ""
代碼實(shí)例
FTP服務(wù)器的登陸
下面的代碼比較簡(jiǎn)單理茎,就是登陸到一個(gè) FTP 服務(wù)器,并自動(dòng)輸入密碼,等進(jìn)入服務(wù)器以后皂林,先輸入幾個(gè)預(yù)定義的命令朗鸠,然后將控制權(quán)交還給用戶,用戶操作完成后按 <kbd>ctrl+]</kbd> 表示自己操作完成了式撼,腳本再自動(dòng)退出 ftp 登陸童社。
#!/usr/bin/env python
import sys
import pexpect
# FTP服務(wù)器的標(biāo)準(zhǔn)提示符
ftpPrompt = 'ftp>'
# 啟動(dòng)FTP服務(wù)器,并將運(yùn)行期間的輸出都放到標(biāo)準(zhǔn)輸出中
process = pexpect.spawn('ftp sw-tftp')
process.logfile_read = sys.stdout
# 服務(wù)器登陸過程
process.expect('[Nn]ame')
process.sendline('dev')
process.expect('[Pp]assword')
process.sendline('abcd1234')
# 先自動(dòng)輸入一些預(yù)定命令
cmdList = ("passive", 'hash')
for cmd in cmdList:
process.expect(ftpPrompt)
process.sendline(cmd)
process.expect(ftpPrompt)
# 在這里將FTP控制權(quán)交還給用戶著隆,用戶輸入完成后按 ctrl+] 再將控制權(quán)還給腳本
# ctrl+] 交還控制權(quán)給腳本是默認(rèn)值,用戶還可以設(shè)置其他的值呀癣,比如 ‘\x2a’
# 就是用戶按星號(hào)的時(shí)候交還美浦。這個(gè)值實(shí)際上是 ASCII 的16進(jìn)制碼,它們的對(duì)應(yīng)關(guān)系
# 可以自己去其他地方找一下项栏,但是注意必須是16進(jìn)制的浦辨,并且前綴是 \x
process.interact()
# 當(dāng)用戶將控制權(quán)交還給腳本后,再由腳本退出ftp服務(wù)器
# 注意下面這個(gè)空的sendline()命令沼沈,它很重要流酬。用戶將控制權(quán)交還給腳本的時(shí)候,
# 腳本緩存里面是沒任何內(nèi)容的列另,所以也不可能匹配芽腾,這里發(fā)送一個(gè)回車符會(huì)從服務(wù)器取得
# 一些內(nèi)容,這樣就可以匹配了页衙。
# 最后的EOF是確認(rèn)FTP連接完成的方法摊滔。
process.sendline()
process.expect(ftpPrompt)
process.sendline('by')
process.expect(pexpect.EOF)
上面的腳本實(shí)際上缺少很多錯(cuò)誤處理,比如登陸以后用戶名或者密碼錯(cuò)誤店乐,或者無法連接服務(wù)器之類的艰躺,但是核心動(dòng)作已經(jīng)完整了。