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()