Pexpect 模块使用说明

背景介绍

Expect 程序主要用于人机对话的模拟,就是那种系统提问,人来回答 yes/no ,或者账号登录输入用户名和密码等等的情况。因为这种情况特别多而且繁琐,所以很多语言都有各种自己的实现。最初的第一个 Expect 是由 TCL 语言实现的,所以后来的 Expect 都大致参考了最初的用法和流程,整体来说大致的流程包括:

  1. 运行程序
  2. 程序要求人的判断和输入
  3. Expect 通过关键字匹配
  4. 根据关键字向程序发送符合的字符串

TCL 语言实现的 Expect 功能非常强大,我曾经用它实现了防火墙设备的完整测试平台。也因为它使用方便、范围广,几乎所有脚本语言都实现了各种各样的类似与Expect的功能,它们叫法虽然不同,但原理都相差不大

pexpect 是 Python 语言的类 Expect 实现。从我的角度来看,它在功能上与 TCL 语言的实现还是有一些差距,比如没有buffer_full 事件、比如没有 expect before/after 事件等,但用来做一般的应用还是足够了。

基本使用流程

pexpect 的使用说来说去,就是围绕3个关键命令做操作:

  1. 首先用 spawn 来执行一个程序
  2. 然后用 expect 来等待指定的关键字,这个关键字是被执行的程序打印到标准输出上面的
  3. 最后当发现这个关键字以后,根据关键字用 send 方法来发送字符串给这个程序

第一步只需要做一次,但在程序中会不停的循环第二、三步来一步一步的完成整个工作。掌握这个概念之后 pexpect 的使用就很容易了。当然 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"')
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 关联参数:

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")就可以了。

child = pexpect.spawn("terragrunt plan",
                      cwd='/Users/jenrey/myProject')  # cwd指定命令执行的目录

 

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 开始计算的。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Python)