はじめに
Pythonでお手軽に外部プロセスと通信したい。標準入出力を使用して通信をしてみる。
通信相手には次のような標準入力を受付け、結果を標準出力を返すプログラムを用意する。
while True:
x = input()
print(x, end='\n')
subprocess を使用する方法
subprocess.Popen
でlistener.py
を起動し、communicate
メソッドで標準入出力通信を行う。
import subprocess
def main():
p = subprocess.Popen(
["python", "listener.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE
)
input_str = "OK!"
output_str = p.communicate(input_str.encode())[0].decode()
print(output_str)
if __name__ == '__main__':
main()
これを実行すると次のようになる。
$ python main.py
Traceback (most recent call last):
File "listener.py", line 2, in <module>
x = input()
EOFError: EOF when reading a line
OK!
$
listener.py
から応答が帰ってきているが、例外が発生している。これはプログラムを次のように書き換えるとより詳細にわかる。
import subprocess
def main():
p = subprocess.Popen(
["python", "listener.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE
)
input_str = "OK!"
output_str = p.communicate(input_str.encode())[0].decode()
print(output_str)
input_str = "OK!"
output_str = p.communicate(input_str.encode())[0].decode()
print(output_str)
if __name__ == '__main__':
main()
これを実行すると次のようになる。
$ python main.py
Traceback (most recent call last):
File "listener.py", line 2, in <module>
x = input()
EOFError: EOF when reading a line
OK!
Traceback (most recent call last):
File "main.py", line 26, in <module>
main()
File "main.py", line 17, in main
output_str = p.communicate(input_str.encode())[0].decode()
File "/Users/xxxxx/opt/miniconda3/envs/py37/lib/python3.7/subprocess.py", line 939, in communicate
raise ValueError("Cannot send input after starting communication")
ValueError: Cannot send input after starting communication
$
さらに例外が発生している。これは二回目のcommunicate
ができないことを意味している。subprocessはcommunicate
を終えると、プロセス間の通信を切ってしまうようで、一回しか通信できない。EOFError
が発生しているのは、listener.py
が無限ループで次の標準入力を待っているのにプロセスを終了しようとするためである。
したがって、subprocessはあくまでもプロセスを起動し、コマンドを一回送信するときだけに使える。
pexpect を使用する方法
今期待しているのは、listener.py
のプロセスを立ち上げ、何度も通信することである。そこで、pexpect
モジュールを使用する。
pip install pexpect
pexpectを用いた通信は次のように行う。
import pexpect
def main():
p = pexpect.spawn('python listener.py')
p.sendline("OK!")
p.expect("\n", timeout=None)
print(p.before.decode(encoding='utf-8'))
p.close()
if __name__ == '__main__':
main()
これを実行すると次のようになる。
$ python main.py
OK!
$
pexpect.spawn
で子プロセスを生成、子プロセスを制御するインスタンスを得る。expect
メソッドで、期待パターンと一致する標準出力が出力されるまで待機する。パターンには正規表現やEOF、TIMEOUTが使用できる。
sendline
メソッドで標準入力へ文字列を送信する。expect
メソッドで、標準出力の文字列のカーソルを移動する。ここではlistener.py
で出力された文字列最後尾の改行コードへ移動し、before
メソッドで改行コードより前の文字列を取得している。
ここでもし、リスナーが次のようなプログラムであった場合、
print('process start')
while True:
x = input()
print(x, end='\n')
通信を行うと次のようになる。
$ python main.py
process start
$
これは一つ目の改行コードがprint('process start')
の部分にあるためである。この場合は、spawn
直後にexpect
メソッドを使用する。
import pexpect
def main():
p = pexpect.spawn('python listener.py')
p.expect(".+")
p.sendline("OK!")
p.expect("\n", timeout=None)
print(p.before.decode(encoding='utf-8'))
p.close()
if __name__ == '__main__':
main()
ただし、今度はlistener.py
実行時に何か標準出力されないとTimeoutまで待機し続けてしまうので注意。
参考
おわりに
もっと賢い方法があったら教えて下さい。