Pythonからシェルコマンドを実行!subprocessでサブプロセスを実行する方法まとめ
こんにちは、平野です。
PythonからAWS CLIなどのシェルコマンドを使いたい時には標準モジュールである
subprocess
モジュールを使いますが、
結構ややこしい感じがしてて上手く使えていませんでした。
シェルコマンドを非同期に動かして色々とごにょごにょしたかったので、 自分の用途の範囲ですがまとめてみました。
なお、subprocess
はシェルコマンド実行のモジュールとして紹介されることも多いですが、
名前の示す通り、Pythonを実行しているプロセスからサブプロセスを生成するためのものです。
この辺の勘違いも、上手く理解できていなかった原因かなぁと思います。
動作環境
- macOS Mojave 10.14.6
$ python -V Python 3.7.5
Python2系については全く調べていませんので悪しからず。。
同期処理
同期処理でコマンドを実行する際、
つまりサブプロセスに仕事させて終わるまで待つ場合はsubprocess.run
を使います。
コマンドを実行して出力結果を得る
出発点となるスクリプトは以下のようになります。
# dateコマンドを実行して文字列として結果を得る import subprocess from subprocess import PIPE proc = subprocess.run("date", shell=True, stdout=PIPE, stderr=PIPE, text=True) date = proc.stdout print('STDOUT: {}'.format(date)) # STDOUT: 2019年 12月19日 木曜日 11時46分14秒 JST
proc
にサブプロセスの結果が格納されているので、そこからstdout
を取得できます。
標準エラー出力についても同様です。
run
メソッドに与える引数について少し紹介します。
shell
コマンド文字列(第一引数)をシェルで展開します。
これを指定しない場合は、第一引数はリストである必要があります。1
つまり、文字列の解析をモジュールがやってくれないので、
ユーザが自分で要素ごとに分けたリストを用意してあげる必要があります。
shell=True
とすることで、文字列からの解析をモジュールに任せることができます。
普通にコマンドラインから実行する感じで使うためにはこの設定が必要です。
ただし、これを有効にするとシェルインジェクション攻撃などの危険性もありますので注意が必要です。
今回は単純さのために特に記述していませんが、
実際に使う場合はshlex.quote()
などを使用するようにしてください。
subprocess --- サブプロセス管理 - セキュリティで考慮すべき点
stdout
, stderr
サブプロセスの標準出力、標準エラー出力をつなげる先のファイルディスクリプタを指定します。
通常はファイルディスクリプタを設定するのですが、特別にsubprocess.PIPE
というものが用意されています。
これを設定すると、例にあるように、proc.stdout
などとしてPythonスクリプト内で出力を取得できます。
基本的にはPIPE
を設定することが多いかと思いますが、
Pythonプロセスと同様に標準出力に出力したい場合はsys.stdout
を設定します。
標準エラー出力も同様に設定できます。
text
入出力をテキストとしてデコード、エンコードしてくれます。 特別バイナリを扱うという場合でなければ設定しておいた方がラクそうです。
Python内の変数を渡す
Pythonスクリプト内の変数を、stdinとしてサブプロセスに渡すことができます。
# 変数をstdinとしてサブプロセスに渡す import subprocess from subprocess import PIPE array = [i for i in range(100)] input_text = '\n'.join(array) proc = subprocess.run("cat -n", shell=True, input=input_text, stdout=PIPE, stderr=PIPE, text=True) print(proc.stdout) # 1 1 # 2 2 # 3 3 # ・・・
input
stringを渡すと、それがサブプロセスのstdinに渡されます。
内部的にはstdin=subprocess.PIPE
などが使われているようですが、
使うだけなら気にする必要はありません。
ファイルの入出力
stdin
, stdout
にはファイルディスクリプタを指定できるので
Pythonのプロセスでファイルの中身をメモリに載せることなくサブプロセスでファイルの入出力ができます。
# ファイルの中身に行数をつけて別ファイルに保存する import subprocess from subprocess import PIPE with open('input.txt') as input_file: with open('output.txt', 'w') as output_file: proc = subprocess.run("cat -n", shell=True, stdin=input_file, stdout=output_file, text=True)
実は、shell=True
の状態ではコマンドの中身にはほぼ何でも書けるので、
次のような書き方でも同じことができます。
# ファイルの中身に行数をつけて別ファイルに保存する import subprocess from subprocess import PIPE proc = subprocess.run("cat input.txt | cat -n > output.txt", shell=True, text=True)
サブプロセスのシェルでファイルを開いて、 パイプで処理を繋いだり、リダイレクトでファイル書き出しなんかも問題なく行うことができます。
非同期に使う
subprocess.run
は同期処理なので、サブプロセスが終了するまでPythonは次の処理に進みません。
せっかく別プロセスが立ち上がっているのですから非同期に処理を行いたいところです。
非同期で処理を行うためにはsubprocess.Popen
を使います。
基本的な使い方としては
Popen
でサブプロセスの処理をスタートさせcommunicate
でサブプロセスの終了を待ち、出力結果を使う
となり、具体的は以下のようになります。
import subprocess from subprocess import PIPE # サブプロセスをスタート proc = subprocess.Popen("sleep 60; ls hoge.py aaa", shell=True, stdout=PIPE, stderr=PIPE, text=True) # サブプロセスの完了を待つ # ここではsleepしている60秒待たされる result = proc.communicate() (stdout, stderr) = (result[0], result[1]) print('STDOUT: {}'.format(stdout) print('STDERR: {}'.format(stderr) # STDOUT: hoge.py # STDERR: ls: aaaa: No such file or directory
Popen
に渡せる引数は、run
とほぼ同様ですが、
大きな違いとしてinput
を指定することはできません。
実はcommunicate
にはinput
を渡すことができるのですが、
それでは同期処理になってしまいますのでうまくいきません。
つまり、Pythonの変数に格納されている文字列を非同期処理の入力に渡すにはひと工夫必要となります。
名前付きパイプを使って非同期処理に変数を渡す
具体的な解決策としては、名前付きパイプを使えばうまくいきました。 普通のファイルに一度書き出しをしても構いませんが、 書き出す量が多い時には書き出しが終わるまでの時間が無駄になりますし、 やっぱりスマートじゃないです。
ということでまずは適当に名前付きパイプを用意します。
# named_pipeという名前の名前付きパイプを作成 $ mkfifo named_pipe
import time import subprocess from subprocess import PIPE s = 'aiueo' proc = subprocess.Popen("cat {} | cat -n; sleep 60".format('named_pipe'), shell=True, stdout=PIPE, stderr=PIPE, text=True) with open('named_pipe', 'w') as fifo: fifo.write(s) # サブプロセスが処理終わるまでの間に、並列で別の重い処理 time.sleep(30) # サブプロセスの完了を待つ result = proc.communicate() (stdout, stderr) = (result[0], result[1]) print('STDOUT: {}'.format(stdout) # STDOUT: 1 aiueo
ポイントとしては、fifoに書き込みを行う前にサブプロセスを作成する点です。 これを逆にしてしまうと、誰も読み出ししていない名前付きパイプに対する書き込み処理が終了しないので、 プロセスが止まってしまいます。
run
とPopen
の関係
同期処理の方法(run
)と非同期処理の方法(Popen
)を紹介しましたが、
実はこれらは非常にシンプルな関係にあります。
run
は、内部的にPopen
を呼んで、すぐにcommunicate
している。
これに気づくと両者はとてもスマートに理解できます。
例えば、run
ではinput
で標準入力を渡すことができましたが、Popen
ではそれができませんでした。
しかしcommunicate
にはinput
が指定できます。
つまり、run
は中でPopen
してすぐにcommunicate(input=string)
を実行しているとわかります。
まとめ
以上、subprocess
モジュールの使い方をまとめてみました。
色々メソッドがありつつも、
run
とPopen
だけ使えば良い2というシンプルなまとめを得ることができました。
この辺を知っていればたいていの処理には対応できるかと思います。
私のお気に入りとしては、input
を使うことで、
Python内の文字列変数をシェルコマンドで変換した文字列に変えられるところです。
シェルコマンドが用意されていて、それを使えばすぐできるのに、
Pythonの中で自分で実装を書くのは面倒という場合なんかに使えるかと思います。
ただし、サブプロセスを起動する処理はそこまで軽いわけではないので、
1行ずつ変換処理を行うようなスクリプトを書くと
びっくりするほど実行は遅くなります。
以上、誰かの参考になれば幸いです!