対話処理を自動化したい(pexpect

1import pexpect
2
3child = pexpect.spawn("コマンド")
4child.expect("待ち文字列")
5child.sendline("別のコマンド")
6child.expect("待ち文字列")
7child.terminate()

pexpectexpectをPythonで実行できるようにしたツールです。 リモートサーバーとRsyncやSSHなどするときに必要なパスワード入力などの 対話処理を含んだ手順を自動化できます。

Pure Pythonで書かれており、 expectやTcl、その他のC言語モジュールを必要としません。 また、文字のエスケープや改行処理もうまく扱ってくれるため 素のexpectより読み書きしやすいと思います。

コマンド送信したい(sendline

1child.sendline("コマンド")

sendlineでコマンドを送信できます。 CRLFの改行コード(\r\n)付きで送信してくれるので、 コマンドを記入するだけでよいです。

処理待ちしたい(expect

1# str型
2child.expect("文字列")
3
4# list[str]型
5child.expect(["文字列1", "文字列2"])
6# =0: 文字列1にマッチ
7# =1: 文字列2にマッチ

expectに処理が終わった時に表示される文字列を指定することで、 直前のコマンドの処理を待つことができます。 引数は、str型とlist[str]型で指定でき、正規表現も使えます。 返り値はマッチした文字列のインデックスになっています。

プロンプト待ちしたい

1PROMPT = ["\\$", "\\#", pexpect.EOF]
2child.sendline("コマンド")
3child.expect(PROMPT)

標準的なサーバー設定のプロンプト表示は、$が一般ユーザー、#が管理者ユーザーとなっています。 $もしくは#を待つことでサーバー内での作業を続けることができます。

パスワード待ちしたい

 1# 接続成功したとき
 2ok = ["password:"]
 3
 4# 接続失敗したとき
 5ng = ["Connection refused", "Timed out"]
 6connected = child.expect([*ok, *ng])
 7# =0: "password:" にマッチ
 8# =1: "Connection refused" にマッチ
 9# =2: "Timed out" にマッチ
10
11# 接続失敗
12if connected > 0:
13    child.terminate(force=True)
14    sys.exit()
15
16# 接続成功
17child.sendline("パスワード")

rsyncやSSH接続ではパスワード入力を要求されます。 多くの場合password:の文字列が表示されるため、.expectすることで接続確認ができます。 また、接続できなかったときの文字列を確認することで、例外として処理できます。

上のサンプルではokngのリストを定義し、 マッチしたときに返ってくる文字列のインデックスを利用して 簡単な成功/失敗の判断をしています。

パスワード送信したい(sendline

1import os
2from dotenv import load_dotenv
3
4# 環境変数(.env)を読み込む
5load_dotenv()
6password = os.environ.get("PASSWORD")
7
8child.sendline(password)

利用することが多いパスワード送信部分のサンプルです。 パスワードのベタ書きを避けるため、 .envに環境変数として定義しdotenvモジュールで読み込んでいます。

# プロジェクト/.env
PASSWORD=パスワード

.envファイルは、プロジェクトルートに作成し、必要な環境変数を定義しておきます。

注意

パスワード情報などが含まれている.envは Gitリポジトリに追加しないように気をつけてください。 うっかりコミットしないように.gitignoreに追記しておきます。

rsyncしたい

$ rsync -auvz SRC_REMOTE DEST_LOCAL
ユーザー名@ホスト名's password: 🔑(ログインパスワードを入力)
receiving file list ... done
同期したファイル名...

sent XX bytes  received XXXX bytes  XX.XX bytes/sec
total size is XXXXXXXXXX  speedup is XXXXXX.XX

rsyncで リモートサーバーからファイルを取得したときの表示される内容のサンプルです。 これを再現するようにpexpectで手順を並べていきます。

1import pexpect
2
3# rsyncを開始
4child = pexpect.spawn("rsync -auvz SRC_REMOTE DEST_LOCAL")
5child.expect("password:")     # パスワード認証を確認
6child.sendline("パスワード")    # パスワードを送信
7child.expect(pexpect.EOF)     # 処理が終わることを確認
8result = child.before.decode("utf-8") # 結果を取得
9print(result)

password:の文字列が表示されたあとに、パスワードを送信しています。 pexpect.EOF.expectすることで、 ファイル転送の処理の完了を待つことができます。

実行した結果はbeforeにバイナリ文字列で保存されています。 テキストデータが欲しいのでutf-8でデコードしています。

注釈

child.expect("文字列")で待っていた文字列とマッチした場合、 child.beforechild.afterにマッチした前後の情報がバイナリ文字列で保存されています。 なので、child.beforeで実行結果が確認できます。

SSH接続したい(pexpect.pxssh

$ ssh ユーザー名@ホスト名
ユーザー名@ホスト名's password: (パスワードを入力)
Last login: 日時 from IPアドレス
[ユーザー名@ホスト名 ~]$ uptime
21:41  up 6 days, 11:26, 4 users, load averages: 2.30 3.65 4.16
[ユーザー名@ホスト名 ~]$ df -h
Filesystem        Size    Used   Avail Capacity iused ifree %iused  Mounted on
...
[ユーザー名@ホスト名 ~]$ exit
logout

SSH接続して、いくつかのコマンドを実行したときに内容のサンプルです。 この手順をpexpect.pxsshモジュールを使って並べます。

 1from pexpect import pxssh
 2
 3ssh = pxssh.pxssh(encoding="utf-8")
 4ssh.login(hostname, username, password)
 5
 6ssh.sendline("uptime")
 7ssh.prompt()
 8print(ssh.before)
 9
10ssh.sendline("df -h")
11ssh.prompt()
12print(ssh.before)
13
14ssh.logout()

pexpect.pxsshモジュールで簡単にSSH接続できます。 pexpect.spawnで書いた場合と比べると、 ログイン処理(.login)とプロンプト待ち(.prompt)が簡単に書けるようになっています。

 1import pexpect
 2
 3# 待機するプロンプト表示
 4PROMPT = ["\\$", "\\#", pexpect.EOF]
 5
 6# SSH接続を開始する
 7child = pexpect.spawn("ssh ユーザー名@ホスト名")
 8child.expect("password:")
 9
10# パスワードを送信してログインする
11child.sendline("パスワード")
12child.expect(PROMPT)
13
14# コマンド(uptime)を実行
15child.sendline("uptime")
16child.expect(PROMPT)
17print(child.before.decode("utf-8"))
18
19# コマンド(df -h)を実行
20child.sendline("df -h")
21child.expect(PROMPT)
22print(child.before.decode("utf-8"))
23
24# コマンド(exit)を実行
25child.sendline("exit")
26child.expect(PROMPT)
27print(child.before.decode("utf-8"))
28
29child.terminate()

リファレンス