对Python Pexpect 模块的使用说明详解
背景介绍
Expect程序主要用于人机对话的模拟,就是那种系统提问,人来回答yes/no,或者账号登录输入用户名和密码等等的情况。因为这种情况特别多而且繁琐,所以很多语言都有各种自己的实现。最初的第一个Expect是由TCL语言实现的,所以后来的Expect都大致参考了最初的用法和流程,整体来说大致的流程包括:
运行程序
程序要求人的判断和输入
Expect通过关键字匹配
根据关键字向程序发送符合的字符串
TCL语言实现的Expect功能非常强大,我曾经用它实现了防火墙设备的完整测试平台。也因为它使用方便、范围广,几乎所有脚本语言都实现了各种各样的类似与Expect的功能,它们叫法虽然不同,但原理都相差不大
pexpect是Python语言的类Expect实现。从我的角度来看,它在功能上与TCL语言的实现还是有一些差距,比如没有buffer_full事件、比如没有expectbefore/after事件等,但用来做一般的应用还是足够了。
基本使用流程
pexpect的使用说来说去,就是围绕3个关键命令做操作:
首先用spawn来执行一个程序
然后用expect来等待指定的关键字,这个关键字是被执行的程序打印到标准输出上面的
最后当发现这个关键字以后,根据关键字用send方法来发送字符串给这个程序
第一步只需要做一次,但在程序中会不停的循环第二、三步来一步一步的完成整个工作。掌握这个概念之后pexpect的使用就很容易了。当然pexpect不会只有这3个方法,实际上还有很多外围的其他方法,我们一个一个来说明
API
spawn()-执行程序
spawn()方法用来执行一个程序,它返回这个程序的操作句柄,以后可以通过操作这个句柄来对这个程序进行操作,比如:
process=pexpect.spawn('ftpsw-tftp')
上面spawn()中的字符串就是要执行的程序,这里我们打开一个到sw-tftp服务器的ftp连接。spawn()中的第一个元素就是要执行的命令,除此之外还可以指定一些其他参数,比如:pexpect.spawn('ftpsw-tftp',timeout=60)就指定了超时时间,这些具体的会在后面讲解。
process就是spawn()的程序操作句柄了,之后对这个程序的所有操作都是基于这个句柄的,所以它可以说是最重要的部分。尽量给它起个简短点的名字,不然后面的程序要多打不少字的。-
注意:spawn(),或者说pexpect并不会转译任何特殊字符比如|*字符在Linux的shell中有特殊含义,但是在pexpect中不会转译它们,如果在linux系统中想使用这些符号的正确含义就必须加上shell来运行,这是很容易犯的一个错误。
正确的方式:
process=pexpect.spawn('/bin/bash–c"ls–l|grepLOG>log_list.txt"') process.expect(pexpect.EOF)
spawn()还有一种调用方式就是第一个参数是主程序,而下一个参数是主程序的参数,理解起来很麻烦?看看实际代码吧:
cmd="ls–l|grepLOG>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()命令中设置,pexpect2.x版本似乎有一个bug,在spawn中设置是不生效的。
logfile-运行输出控制
默认值:None
当给logfile参数指定了一个文件句柄时,所有从标准输入和标准输出获得的内容都会写入这个文件中(注意这个写入是copy方式的),如果指定了文件句柄,那么每次向程序发送指令(process.send)都会刷新这个文件(flush)。
这里有一个很重要的技巧:如果你想看到spawn过程中的输出,那么可以将这些输出写入到sys.stdout里去,比如:
process=pexpect.spawn("ftpsw-tftp",logfile=sys.stdout)
用这样的方式可以看到整个程序执行期间的输入和输出,很适合调试。
还有一个例子:
process=pexpect.spawn("ftpsw-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
上面的语句仅仅在屏幕上打印向程序发送的内容。
cwd-指定命令执行的目录
默认值:None或者说./
cwd用来指定命令发送的命令在哪个路径下执行,它一般是用在send()系列命令中,比如在Linux中,你想在/etc目录下执行ls–l命令,那么完全不需要用sendline("cd/etc&&ls-l")这样的方式,而是用sendline("ls–l",cwd="/etc")就可以了。
env-指定环境变量
默认值:None
指定环境变量的值,这个值是一个字典,如果你发送的命令要使用一些环境变量,那么可以在这里提供
ignore_sighup-是否过滤SIGHUP信号
默认值:True
这个参数是pexpect3.1开始引入的,在3.1之前(比如pexpect2.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,myLadyGAGA……完全是指数上升啊。
最简单的匹配方式
process.expect('[Nn]ame')
上面的代码表示:匹配process这个句柄(代表spawn方法的例子中我们启动的ftp连接)中的name关键字,其中n不分大小写。
上面的关键字一旦匹配,就会返回0表示匹配成功,但是如果一直匹配不到呢?默认是会一直等下去,但是如果设置了timeout的话就会超时。
匹配一系列输出
实际上,expect()可以匹配一系列输出,通过检查匹配到的输出,我们可以做不同的事情。比如之前spawn的ftp连接,如果我们输入用户名之后有不同的情况,就可以通过监控这些不同情况来做不同的动作,比如:
index=process.expect([ 'PermissionDenied', 'Terminaltype', 'ftp>', ]) ifindex==0: print"Permissiondeniedathost,can'tlogin." process.kill(0) elifindex==1: print"Loginok,setupterminaltype…" process.sendline('vty100') process.expect("ftp>") elifindex==2: print"LoginOk,pleasesendyourcommand" process.interact()
上面的代码中,expect方法中的是一个列表,列表中的每个元素都是一个关键字的正则表达式,也就是说我们期待这3种情况之一,而expect返回一个顺序值来代表我匹配到了哪一个元素(也就是发生了哪种情况了),这个顺序值是从0开始计算的。
当expect之后,下面的if语句就开始处理这3种情况了:
权限不足,这可能是ftp服务器出现问题,或者没有这个帐号,或者其他什么情况,反正只要发现这种情况的话,我们就给用户提示一下,然后杀掉这个进程
登陆成功,但还要用户指定终端模式才能真正使用,所以我们在代码中指定了vty100这种模式,然后看是不是能真正使用了
还是登陆成功了,而且还可以直接输入命令操作ftp服务器了,于是我们提示用户,然后把操作权限交给用户
另外有一种特殊情况,如果同时有2个被匹配到,那么怎么办?简单来说就是这样:
原始流中,第一个被关键字匹配到的内容会被使用
匹配关键字列表中,最左边的会被使用
给个例子:
#如果流里面的内容是"helloworld" index=process.expect(["hi","hello","helloworld"])
返回的值是1,也就是'hello'被匹配到了,哪怕真正最好的匹配是"helloworld"但因为放在后面所以仍然无效。
使用技巧
如果要检查或者匹配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信号
向子程序发送EndOfFile信号。
sendintr()-发送终止信号
向子程序发送SIGINT信号,相当于Linux中的kill2,它会直接终止掉子进程。
interact()-将控制权交给用户
interact()表示将控制权限交给用户(或者说标准输入)。一般情况下pexpect会接管所有的输入和输出,但有的时候还是希望用户介入,或者仅仅是为了完成一部分工作的时候,interact()就很有用了。
比如:
登陆ftp服务器的时候,在输入用户密码阶段希望用户手工输入密码,然后脚本完成剩余工作时(将用户密码写在脚本中可不安全)
只希望完成登陆工作,比如要ssh连接到一台远方的服务器,但中间要经过好几跳,用手工输入实在太麻烦,所以就用脚本先跳到目的服务器上,然后再把控制权限还给用户做操作。
使用方法:
#escape_character就是当用户输出这里指定的字符以后表示自己的操作完成了,将控制权重新交给pexpect process.interact(escape_character='\x1d',input_filter=None,output_filter=None)
详细来说,这个方法将控制权交给用户(或者说用户操作的键盘),然后简单的将标准输出、标准错误输出和标准输入绑定到系统上来。
通过设置escape_character的值,可以定义返回码,默认是ctrl+]或者说^],当输入了返回码以后,脚本会将控制权从用户那里重新拿回来,然后继续向下执行。
close()-停止应用程序
如果想中途关闭子程序,那么可以用close来完成,调用这个方法后会返回这个程序的返回值。
如果设置force=True会强行关闭这个程序,大概的过程就是先发送SIGHUP和SIGINT信号,如果都无效的话就发SIGKILL信号,反正不管怎么样都会保证这个程序被关闭掉。
多次调用这个方法是允许的,但是不保证每次都能返回正确的返回值。尽量不要这么做,如果想保证程序被关闭的话只要设置force的值就可以了。
下面是实例:
process.close(force=True)
terminate()-停止应用程序
可以看作是上面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() printoutput
看起来这么做有点无聊?但我想一定有什么理由支持这个方法。
可以用指定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功能被设置为Fals时。
看起来很奇怪?其实这个功能是基于一个很让人难以置信但的确是真实的情况:
在命令行模式下,很多要求输入密码的地方,比如FTP/SSH等,密码实际上都会在你输入之后又重新返回并打印出来的,但是为什么我们看不到我们自己输入的密码呢?这就是因为密码在要打印出来之前被程序将echo功能设置为False了。
现在知道为什么有这么一个方法了吧?比如要进行一个ssh连接时,如何检查是否要输入密码?用关键字password是一个方法,但还有一个方法就是这样:
#启动ssh连接 process=pexpect.spawn("sshuser@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"ftpconnectterminated."
pexpect.TIMEOUT-匹配超时信号
TIMEOUT变量用来匹配超时的情况,默认情况下expect的超时时间是60秒,如果超过60秒还没有发现期待的关键字,就会触发这个行为,比如:
#匹配pexpect.TIMEOUT的动作,只有超时事件发生的时候才会有效 index=process.expect(['ftp>',pexpect.TIMEOUT],) ifindex==1: process.interactive();#将控制权交给用户 elifindex==2: print"Timeisout." process.kill(0);#杀掉进程 #那么怎么改变超时时间呢?其实可以修改spawn对象里的timeout参数: #下面的例子仅仅加了一行,这样就改变了超时的时间了 process.timeout=300;#注意这一行 index=process.expect(['ftp>',pexpect.TIMEOUT],) ifindex==1: process.interactive();#将控制权交给用户 elifindex==2: print"Timeisout." 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表示回车换行
Linuxlike系统中用\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("ftpsw-tftp") printstr(process) #version:2.3($Revision:399$) command:/usr/bin/ftp args:['/usr/bin/ftp','sw-tftp'] searcher:searcher_re: 0:EOF buffer(last100chars): before(last100chars):was14494bytesin1transfers. 221-ThankyouforusingtheFTPserviceonsw-tftp. 221Goodbye. after: match: match_index:0 exitstatus:0 flag_eof:True pid:50733 child_fd:3 closed:False timeout:30 delimiter: 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语句来完成,比如:
whileTrue: index=process.expect([ pexpect.TIMEOUT, pexpect.EOF, ]} ifindex==0: print"timeisout" #重新从开始匹配 continue elifindex==1: print"Terminate." #终止循环 break
获取before中内容的战略与清空buffer
绝大多数情况下我们都会利用before变量来获取命令执行的结果。但是,你真的知道怎么用好before么?
before中到底什么时候保存你所需的内容?这个细节必须非常清楚,我们以一个调用一个命令为例子:
这里我们预计是在linux系统中,下面的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/envpython importsys importpexpect #FTP服务器的标准提示符 ftpPrompt='ftp>' #启动FTP服务器,并将运行期间的输出都放到标准输出中 process=pexpect.spawn('ftpsw-tftp') process.logfile_read=sys.stdout #服务器登陆过程 process.expect('[Nn]ame') process.sendline('dev') process.expect('[Pp]assword') process.sendline('abcd1234') #先自动输入一些预定命令 cmdList=("passive",'hash') forcmdincmdList: process.expect(ftpPrompt) process.sendline(cmd) process.expect(ftpPrompt) #在这里将FTP控制权交还给用户,用户输入完成后按ctrl+]再将控制权还给脚本 #ctrl+]交还控制权给脚本是默认值,用户还可以设置其他的值,比如‘\x2a' #就是用户按星号的时候交还。这个值实际上是ASCII的16进制码,它们的对应关系 #可以自己去其他地方找一下,但是注意必须是16进制的,并且前缀是\x process.interact() #当用户将控制权交还给脚本后,再由脚本退出ftp服务器 #注意下面这个空的sendline()命令,它很重要。用户将控制权交还给脚本的时候, #脚本缓存里面是没任何内容的,所以也不可能匹配,这里发送一个回车符会从服务器取得 #一些内容,这样就可以匹配了。 #最后的EOF是确认FTP连接完成的方法。 process.sendline() process.expect(ftpPrompt) process.sendline('by') process.expect(pexpect.EOF)
上面的脚本实际上缺少很多错误处理,比如登陆以后用户名或者密码错误,或者无法连接服务器之类的,但是核心动作已经完整了。
以上这篇对PythonPexpect模块的使用说明详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。