python之pexpect模块

Linux运维 专栏收录该内容
17 篇文章 1 订阅

一:pexpect简介

以下演示python版本:python3.7.4
pexpect官方文档: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 个方法,实际上还有很多外围的其他方法
# 在本机执行命令,并输出命令执行结果
import pexpect
child = pexpect.spawn('ls -l')
child.expect(pexpect.EOF)
result = child.before.decode()
print(result)

三:API使用

3.1 spawn类

spawn() 方法用来执行一个程序,它返回这个程序的操作句柄,以后可以通过操作这个句柄来对这个程序进行操作。spawn类的__init__方法如下:

class spawn(SpawnBase):
    '''This is the main class interface for Pexpect. Use this class to start
    and control child applications. '''

    # This is purely informational now - changing it has no effect
    use_native_pty_fork = use_native_pty_fork

    def __init__(self, command, args=[], timeout=30, maxread=2000,
                 searchwindowsize=None, logfile=None, cwd=None, env=None,
                 ignore_sighup=False, echo=True, preexec_fn=None,
                 encoding=None, codec_errors='strict', dimensions=None,
                 use_poll=False):

上面第二节演示的就是spawn()第一个参数command的使用,变量child就是 spawn() 的程序操作句柄了,之后对这个程序的所有操作都是基于这个句柄的,所以它可以说是最重要的部分。
command参数也可以配合args参数使用:

child = pexpect.spawn('ls', args = ['-l', '/'])
child.expect(pexpect.EOF)
result = child.before.decode()
print(result)

注意:command参数不支持直接使用管道,通配符,标志输入,输出,错误重定向,如要使用就必须配合args参数

child = pexpect.spawn('/bin/bash', ['-c', 'cat test|grep green'])
child.expect(pexpect.EOF)
result = child.before.decode()
print(result)

timeout参数:设置超时时间,单位为秒
maxread参数:从TTY读取信息最大缓冲区
logfile=None:指定日志文件,可指定为sys.stdout
cwd=None:指定命令运行的目录,默认值 None 或者说 ./
env=None:命令运行时的环境变量
encoding=None:命令运行时的编码
codec_errors=‘strict’:编码转换时的选项

child = pexpect.spawn('ls -l', logfile=sys.stdout, cwd = '/home')
child.expect(pexpect.EOF)

3.2 expect()方法

当 spawn() 启动了一个程序并返回程序控制句柄后,就可以用 expect() 方法来等待指定的关键字了。它最后会返回 0 表示匹配到了所需的关键字,如果后面的匹配关键字是一个列表的话,就会返回一个数字表示匹配到了列表中第几个关键字,从 0 开始计算。
expect() 利用正则表达式来匹配所需的关键字。使用方式如下:

# pattern_list   正则表达式列表,表示要匹配这些内容
# timeout        不设置或者设置为-1的话,超时时间就采用self.timeout的值,默认是30秒。也可以自己设置。
# searchwindowsize  功能和 spawn 上的一样,但是!请注意这个但是!下面会实际说明
child.expect(pattern_list, timeout=-1, searchwindowsize=None)

patter_list:可以为字符串,正则表达式,EOF,TIMEOUT,或者以上类型的列表,用以匹配子命令返回的结果。从子命令返回结果中进行匹配,若只提供字符串等非列表,匹配成功返回0;若提供列表,则返回匹配成功的列表序号;匹配失败,抛出异常
searchwindowsize:是在 expect() 方法中真正生效的,默认情况下是 None,也就是每从子进程中获取一个字符就做一次完整匹配,如果子进程的输出很多的话……性能会非常低。如果设置为其他的值,表示从子进程中读取到多少个字符才做一次匹配,这样会显著减少匹配的次数,增加性能。

child = pexpect.spawn('ls -l ./')
child.expect('run')				# 匹配run字符 

before/after/match:获取程序运行输出
当 expect() 过程匹配到关键字(或者说正则表达式)之后,系统会自动给3个变量赋值,分别是 before, after 和 match

before - 保存了到匹配到关键字为止,缓存里面已有的所有数据。也就是说如果缓存里缓存了 100 个字符的时候终于匹配到了关键字,那么 before 就是除了匹配到的关键字之外的所有字符
after - 保存匹配到的关键字,比如你在 expect 里面使用了正则表达式,那么表达式匹配到的所有字符都在 after 里面
match - 保存的是匹配到的正则表达式的实例,和上面的 after 相比一个是匹配到的字符串,一个是匹配到的正则表达式实例
如果 expect() 过程中发生错误,那么 before 保存到目前位置缓存里的所有数据, after 和 match 都是 None

child = pexpect.spawn('ls -l ./')
child.expect('run')

print(child.before)
print(child.match)
print(child.after)

python之pexpect模块
expect 方法中也可以传入一个列表,列表中的每个元素都是一个关键字的正则表达式

child = pexpect.spawn('ls -l ./')
index = child.expect(['test', 'run'])	# 匹配到列表中任意一个元素即停止匹配
print(index)							# 返回列表中匹配到的字符索引

如果没有匹配到任何字符则抛出异常:

child = pexpect.spawn('ls -l ./')
child.expect('who')

异常信息如下:

pexpect.exceptions.EOF: End Of File (EOF). Exception style platform.
<pexpect.pty_spawn.spawn object at 0x7f101f7b4550>
command: /bin/ls
args: ['/bin/ls', '-l', './']
buffer (last 100 chars): ''
before (last 100 chars): 'rw-r--r-- 1 root root  3731 Mar 20 04:21 run.py\r\n-rw-r--r-- 1 root root   178 Mar 20 22:36 test.py\r\n'
after: <class 'pexpect.exceptions.EOF'>
match: None
match_index: None
exitstatus: 0
flag_eof: True
pid: 7785
child_fd: 5
closed: False
timeout: 30
delimiter: <class 'pexpect.exceptions.EOF'>
logfile: None
logfile_read: None
logfile_send: None
maxread: 2000
ignorecase: False
searchwindowsize: None
delaybeforesend: 0.05
delayafterclose: 0.1
delayafterterminate: 0.1
searcher: searcher_re:
    0: re.compile('who')

可以匹配异常,让异常不在终端显示,从而程序不退出运行:

child = pexpect.spawn('ls -l ./')
child.expect(pexpect.EOF)			# 如果将此行代码打印的话会输出0
child = pexpect.spawn('ls')
child.expect(['run', pexpect.EOF])	# 如果返回1说明匹配到了异常

匹配时自动应用re.DOTALL正则选项。(.+ 匹配所有字符,.* 返回空字符),匹配行尾使用 ‘\r\n’,无法用$匹配行尾

3.3 sendline()方法

sendline() - 发送带回车符的字符串
sendline() 和 send() 唯一的区别就是在发送的字符串后面加上了回车换行符,这也使它们用在了不同的地方:

  1. 只需要发送字符就可以的话用send()
  2. 如果发送字符后还要回车的话,就用 sendline()

它也会返回发送的字符数量

child = pexpect.spawn('nslookup')
child.expect('>')
child.sendline('www.baidu.com')
child.expect('>')
print(child.before)
child.sendline('exit')

3.4 其他发送信息的方法

send() :发送关键字
send() 作为3个关键操作之一,用来向程序发送指定的字符串,末尾不带回车换行符

process.expect("ftp>")
process.send("by\n")
# 这个方法会返回发送字符的数量

sendcontrol():发送控制信号
sendcontrol() 向子程序发送控制字符,比如 ctrl+C 或者 ctrl+D 之类的,比如你要向子程序发送 ctrl+G,那么就这样写:

child.sendcontrol('g')

sendeof() :发送 EOF 信号
向子程序发送 End Of File 信号,一般用于确认上一次发送内容缓冲结束

sendintr():发送终止信号
向子程序发送 SIGINT 信号,相当于 Linux 中的 kill 2 ,它会直接终止掉子进程

write():发送字符串
类似于send()命令,只不过不会返回发送的字符数。

writelines():发送包含字符串的列表
类似于 write() 命令,只不过接受的是一个字符串列表, writelines() 会向子程序一条一条的发送列表中的元素,但是不会自动在每个元素的最后加上回车换行符。
与 write() 相似的是,这个方法也不会返回发送的字符数量。

3.5 其他获取结果的方法

expect_exact():精确匹配
它的使用和 expect() 是一样的,唯一不同的就是它的匹配列表中不再使用正则表达式。
从性能上来说 expect_exact() 要更好一些,因为即使你没有使用正则表达式而只是简单的用了几个字符 expect() 也会先将它们转换成正则表达式模式然后再搜索,但 expect_exact() 不会,而且也不会把一些特殊符号转换掉。

expect_list():预转换匹配
使用方式和 expect() 一样,唯一不同的就是匹配列表只用已编译正则表达式和EOF, TIMEOUT;提高匹配速度;expect()方法是通过它工作的。
expect() 稍微有点笨,每调用一次它都会将内部的正则表达式转换一次(当然也有其他办法避免),如果你是在以后循环中调用 expect() 的话,多余的转换动作就会降低性能,在这种情况下建议用 expect_list() 来代替。

# timeout 为 -1 的话使用 self.timeout 的值
# searchwindowsize 为 -1 的话,也使用系统默认的值
child.expect_list(pattern_list, timeout=-1, searchwindowsize=-1)

expect_loop()
用于从标准输入中获取内容,loop这个词代表它会进入一个循环,必须要从标准输入中获取到关键字才会往下继续执行。

expect_loop(self, searcher, timeout=-1, searchwindowsize=-1)

read():返回剩下的所有内容
获取子程序返回的所有内容,一般情况下我们可以用 expect 来期待某些内容,然后通过 before 这样的方式来获取,但这种方式有一个前提:那就是必须先 expect 某些字符,然后才能用 before 来获取缓存中剩下的内容。

read() 的使用很不同,它期待一个 EOF 信号,然后将直到这个信号之前的所有输出全部返回,就像读一个文件那样。
一般情况下,交互式程序只有关闭的时候才会返回 EOF ,比如用 by 命令关闭 ftp 服务器,或者用 exit 命令关闭一个 ssh 连接。
这个方法使用范围比较狭窄,因为完全可以用 expect.EOF 方式来代替。当然如果是本机命令,每执行完一次之后都会返回 EOF ,这种情况下倒是很有用:

child = pexpect.spawn('ls –l')
output = child.read()
print output

可以用指定 read(size=-1) 的方式来设置返回的字符数,如果没有设置或者设置为负数则返回所有内容,正数则返回指定数量的内容,返回的内容是字符串形式。

readline():返回一行输出
返回一行输出,返回的内容包括最后的\r\n字符。
也可以设置 readline(size=-1) 来指定返回的字符数,默认是负数表示返回所有的。

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

四:应用案例

1:远程登录主机并执行命令

child = pexpect.spawn('ssh ginvip@172.17.2.117')
child.logfile = sys.stdout

child.expect('password')
child.sendline('ginvip')
child.expect('ginvip')
child.sendline('ls /')
child.expect('ginvip')
child.sendline('exit')

如果要将日志写入文件:

child = pexpect.spawn('ssh ginvip@172.17.2.117')
fout = file('log.txt', 'w')
child.logfile = fout
child.expect('password')
child.sendline('ginvip')
child.expect('ginvip')
child.sendline('ls /')
child.expect('ginvip')
child.sendline('exit')

2:FTP文件管理

import pexpect, sys, os, re, time
def login_ftp():
    ftp = pexpect.spawn('ftp', cwd = cwd)
    if ftp.expect(prmpt) != 0:
        sys.exit()
    ftp.sendline('open {ip} {port}'.format(ip = ip, port = port))
    if ftp.expect('Name') != 0:
        sys.exit()
    ftp.sendline(username)
    if ftp.expect('Password:') != 0:
        sys.exit()
    ftp.sendline(password)
    if ftp.expect('230') != 0 or ftp.expect(prmpt) != 0:
        sys.exit()
    return ftp

def get_server_files(ftp):
    ftp.sendline('ls')
    if ftp.expect('226') != 0:
        sys.exit()
    file_list = ftp.before
    file_list = file_list.split('\n')
    remtch = re.compile('\s+')
    file_list = [remtch.subn(' ', item.strip('\r'))[0] for item in file_list if 'group' in item]
    file_dict = dict()
    for item in file_list:
        datas = item.split(' ')
        file_dict[datas[-1]] = {'mon': week.index(datas[-4]) + 1, 'day': int(datas[-3]), 'time': datas[-2]}
    return file_dict

def get_local_files():
    local_files = os.listdir(cwd)
    local_files_dict = dict()
    for file in local_files:
        t = time.ctime(os.stat(os.path.join(cwd, file)).st_mtime)
        datas = t.split(' ')
        local_files_dict[file] = {'mon': week.index(datas[-4]) + 1, 'day': int(datas[-3]), 'time': datas[-2][:5]}
    return local_files_dict

def sync_files(ftp, local_files, remote_files):
    add_file = []
    for file in local_files.keys():
        if file not in remote_files:
            add_file.append(file)
        if file in remote_files:
            if local_files[file]['mon'] > remote_files[file]['mon']:
                add_file.append(file)
    if add_file:
        for f in add_file:
            ftp.sendline('put' + f)
            if ftp.expect(['226', pexpect.EOF]) == 0:
                print('upload success: ', f)
            else:
                sys.exit()
    # 以下代码会删除FTP服务器上的文件,测试请慎重
    # delete_files = set(remote_files.keys()) - set(local_files.keys())
    # if delete_files:
    #     for f in delete_files:
    #         ftp.sendline('delete' + f)
    #         if ftp.expect(['250', pexpect.EOF]) == 0:
    #             print('delete success: ', f)
    #         else:
    #             print('Permision denied')
    #             sys.exit()

def exit_ftp(ftp):
    if ftp:
        ftp.sendcontrol('d')
        print(ftp.read().decode())

if __name__ == '__main__':
    week = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
    cwd = '/root/backup'
    prmpt = ['ftp>', pexpect.EOF]
    ip = '172.17.2.76'
    port = '2121'
    username = 'xxxxx'
    password = 'xxxxxx'

    ftp = login_ftp()
    remote_files = get_server_files(ftp)
    print(remote_files)
    print('=' * 50)
    local_files = get_local_files()
    print(local_files)
    exit_ftp(ftp)

五:SSH专用类

pxssh类:为建立SSH连接定制了一些功能
第一次连接时可以自动接受远程认证,可以自动应用公钥登录而不用输入密码
构造方法参数除无CMD参数外其余与spawn类相同

# 远程登录主机执行命令
from pexpect.pxssh import pxssh
hostname = '172.17.2.117'
user = 'ginvip'
password = 'ginvip'
s = pxssh()
s.login(hostname, user, password)
s.sendline('ip addr')
s.prompt()				# 匹配命令提示符
print(s.before)			# 查看命令执行结果
s.logout()

实例:逐个登录指定的多台远程主机,监控远程主机并依据相关信息要求用户处理

from pexpect.pxssh import pxssh
import pexpect

def login_host(host):
    ssh = pxssh()
    if ssh.login(host[0], host[1], host[2]):
        return ssh

def get_cpu_num(ssh):
    ssh.sendline('cat /proc/cpuinfo')
    res = ssh.expect(['cpu cores.*\r\n', pexpect.EOF])
    if res == 0:
        data = ssh.after.split('\r\n')[0]
        data = data[data.index(':') + 1:]
        cpu_cores = int(data)
        ssh.prompt()
        return cpu_cores

def get_cpu_load(ssh):
    ssh.sendline('uptime')
    if ssh.prompt():
        data = ssh.before
        data = data.strip('\r\n')
        data = data[data.rfind(':')+1:]
        data = data.split(',')
        return (float(data[0]), float(data[1]), float(data[2]))

def get_cpu_stat(ssh):
    ssh.sendline('vmstat')
    ssh.prompt()
    print(ssh.before)

def user_deal(host, file_name):
    s = login_host(host)
    if not s:
        print('Login failed: ', host[0])
        return
    try:
        cpu_cores = get_cpu_num(s)
        if not cpu_cores:
            print('Do not get cpu cores: ', host[0])
            return
        cpu_load = get_cpu_load(s)
        if cpu_load[2] >= cpu_cores:
            get_cpu_stat(s)
            print('system is not healthy. Do you want to deal? (yes/no)')
            yn = input()
            if yn == 'yes':
                with open(file_name, 'ab+') as f:
                    s.logfile = f
                    s.interact()
                    s.prompt()
                    s.logfile = None
        else:
            print('system is healthy: ', host[0])
    except Exception as e:
        print('failed: ', host[0])
    finally:
        s.logout()

if __name__ == '__main__':
    hosts = [('172.17.2.117', 'root', 'root')]
    file_name = 'log.txt'
    for host in hosts:
        user_deal(host, file_name)
  • 12
    点赞
  • 6
    评论
  • 39
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值