Python 3で外部コマンドを呼ぶにはsubprocess
モジュールで提供されているrun()
を使うことが推奨されるのですが、毎回使い方を調べてしまっているので自分用にまとめます。順を追っていかないと引数の意味を理解しづらいところがあるので、冗長ですが簡単な例から書いていきます。なお、run()
が導入されたのはPython 3.5なので、それより前のバージョンでは使えません。CygwinのPython 3.8.3で動作確認しています。
基本
呼び出される側のスクリプトとして、以下のようなhello.py
を用意します。これは標準出力に「こんにちは」、標準エラー出力に「こんばんは」と出力するだけのトイプログラムです。
import sys print("こんにちは", file=sys.stdout) print("こんばんは", file=sys.stderr)
さっそくhello.py
をsubprocess.run()`を使って呼び出してみます。もっともシンプルな呼び出し方は
res = subprocess.run(['python3', 'hello.py'])
ですが、これだとresの中には
CompletedProcess(args=['python3', 'hello.py'], returncode=0)
という情報しか入っていません。これでは肝心の標準出力結果、標準エラー出力結果は得られません。
標準出力結果、標準エラー出力結果を得るためには、以下のようにcapture_output=True
をつけます(Python 3.7以降限定;Python 3.6以前では代わりにstdout=subprocess.PIPE
と stderr=subprocess.PIPE
の2つを指定)。
res = subprocess.run(['python3', 'hello.py'], capture_output=True) print("return code: {}".format(res.returncode)) print("captured stdout: {}".format(res.stdout.decode())) print("captured stderr: {}".format(res.stderr.decode()))
出力結果は以下です。res.stdout
で標準出力結果を、res.stderr
で標準エラー出力結果を得られていることがわかります。
return code: 0 captured stdout: こんにちは captured stderr: こんばんは
ここで注意なのは、res.stdout
とres.stderr
は、ともに文字列ではなくバイトデータであることです。なのでdecode()を呼んで文字列に変換してやる必要があります。
これは面倒なので、以下のようにtext=True
をつけると、文字列が返ってくるようになり便利です(Python 3.7以降;Python 3.3 ~ 3.6では代わりにuniversal_newlines=True
をつける)。
res = subprocess.run(['python3', 'hello.py'], capture_output=True, text=True) print("return code: {}".format(res.returncode)) print("captured stdout: {}".format(res.stdout)) # こんにちは print("captured stderr: {}".format(res.stderr)) # こんばんは
明に文字列のエンコード方法を指定したいときは、text=True
の代わりにencoding='utf8'
などとつけても良いようです。
標準出力と標準エラー出力とをまとめてキャプチャ
標準出力と標準エラー出力とをまとめてキャプチャするには、stdout=subprocess.PIPE, stderr=subprocess.STDOUT
と指定します(参考:subprocess — Subprocess management — Python 3.9.5 documentation の If you wish to capture and combine both streams into one, use stdout=PIPE and stderr=STDOUT instead of capture_output. )
res = subprocess.run(['python3', 'hello.py'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) print("captured stdout & stderr: {}".format(res.stdout))
ただ、これには大きな欠点があります。標準出力、標準エラー出力それぞれの出力の順番が保存されずマージされてしまいます。上記の例では、
captured stdout & stderr: こんばんは こんにちは
と、順序が逆に表示されてしまいました。
上記の例だけに限っていうと、python - How can I make subprocess get the stdout and stderr in order? - Stack Overflow にある通り、['python3', '-u', 'hello.py']
と-u
オプションを入れてunbufferedを強制することで期待通りの順序になりました。しかし、一般論でいうと、順序を常に保存するかんたんな方法はなさそうでした。
中身の理解はできていませんが参考になりそうなリンクを張っておきます。
- python subprocess won't interleave stderr and stdout as what terminal does - Stack Overflow
- python - subprocess.Popen handling stdout and stderr as they come - Stack Overflow
- python - Run command and get its stdout, stderr separately in near real time like in a terminal - Stack Overflow
例外処理
呼び出したコマンドが成功したかどうかは、run()
の戻り値のreturncode
を確認します。0なら成功、非0なら失敗です。
例として、実行すると異常終了するraise_error.py
を以下のように作成します。
import sys sys.exit(1)
これの戻り値を確認する例が以下です。
res = subprocess.run(['python3', 'raise_error.py']) print("return code: {}".format(res.returncode)) # 1が表示される
run()
にcheck=True
を追加することで、呼び出したコマンドが失敗したときに例外を返すようにすることができます。
$ python3 subprocess_test.py return code: 1 ------------------------------------------------------------ Traceback (most recent call last): File "subprocess_test.py", line 46, in <module> res = subprocess.run(['python3', 'raise_error.py'], check=True) File "/usr/lib/python3.8/subprocess.py", line 512, in run raise CalledProcessError(retcode, process.args, subprocess.CalledProcessError: Command '['python3', 'raise_error.py']' returned non-zero exit status 1.
check=True
をつけるべきかどうかは趣味と場合によりそうです。
コマンドに引数を与える
呼び出される側のスクリプトとして、1からNまでの整数の和を返すsum_1_to_n.py
を用意します。n
は標準入力で受け取る仕様です。
n = int(input()) print(n * (n + 1) // 2)
コンソールで
$ echo 10 | python3 sum_1_to_n.py
とするのと同じノリでsubprocess.run()を使う場合、コマンドをリストで表現する方法ではパイプ(|
)を使えないので、以下のようにshell = True
を使ってコマンドを文字列として表現する必要があります。
res = subprocess.run('echo 10 | python3 sum_1_to_n.py', shell=True, capture_output=True, text=True) print("captured stdout: {}".format(res.stdout)) # 55と表示される
しかし、一般にshell=True
と指定すると脆弱性が発生するので推奨されません。個人のスクリプトでは便利なのでよくやってしまいますが…
上記の例だと、input=10
というのを使うと、標準入力に10を指定したのと同じになってパイプを回避できます。
res = subprocess.run(['python3', 'sum_1_to_n.py'], input='10', text=True, capture_output=True) print("captured stdout: {}".format(res.stdout))
パイプを回避するより一般的な例が、
この処理Pythonでどう書く? - エムスリーテックブログ の「パイプを使う」に記載されています。subprocess.run()
より低レベルな関数subprocess.Popen()
を使うのですが、私は理解できていません。