subprocessでパイプラインの実装

前回に続いてsubprocessの話です。予告していた通り、PythonでOSコマンドをパイプラインで繋いで実行する方法を紹介します。

まず前提ですが、subprocess.run にパイプラインを含むOSコマンドを渡してもそのままでは動きません。例えば実行中のプロセスから jupyter の文字を含む次のようなコマンドを考えます。

$ ps aux | grep jupyter
yutaro             762   0.0  0.8  4315736  67452 s000  S    11:55PM   0:03.71 {Pythonのパス} {pyenvのパス}/versions/anaconda3-2019.10/bin/jupyter-notebook
yutaro             910   0.0  0.0  4278648    712 s000  S+   12:04AM   0:00.00 grep jupyter

このコマンドをそのまま subprocess に渡しても動かないわけです。

import subprocess


cp = subprocess.run(
    ["ps", "aux", "|", "grep", "jupyter"],
    capture_output=True,
    text=True
)
# リターンコードが0ではない
print(cp.returncode)
# 1

# 標準出力は空っぽ
print(cp.stdout)
# 
# 標準エラー出力にはエラーが出ている
print(cp.stderr)
"""
ps: illegal argument: |
usage: ps [-AaCcEefhjlMmrSTvwXx] [-O fmt | -o fmt] [-G gid[,gid...]]
          [-u]
          [-p pid[,pid...]] [-t tty[,tty...]] [-U user[,user...]]
       ps [-L]
"""

実は、パイプラインを含むコマンドを簡単に動かす方法はあります。それがshell引数にTrueを渡すことです。これは渡されたコマンドをシェルによって実行するオプションです。この場合、コマンドは空白で区切った配列ではなく一つの文字列で渡します。

cp = subprocess.run(
    "ps aux | grep jupyter",
    capture_output=True,
    text=True,
    shell=True
)

# リターンコードは0
print(cp.returncode)
# 0
# 標準出力に結果が入る
print(cp.stdout)
# 結果略。
# 標準エラー出力は空
print(cp.stderr)
# 

ただし、ドキュメントに「注釈 shell=True を使う前に セキュリティで考慮すべき点 を読んでください。」という注釈がついてるように、これはセキュリティ面で問題がある方法のようです。
参考: セキュリティで考慮すべき点
シェルインジェクションを避けるのはアプリ側の責任だって書いてありますね。この点気をつけて使いましょう。

さて、色々検証してみたのですが、 shell=True を使わなくてもパイプラインを実装する方法はあるようです。それは単純に標準入力を使う方法で、1個目のコマンドの標準出力を2個目のコマンドの標準入力に渡してあげます。

とりあえず、パイプラインではなく単一のコマンドで標準入力を使ってみましょう。macabコマンドに、いつもの「すもももももももものうち」を渡してみます。

runメソッドに標準入力を渡すには、 input という引数を使います。これで注意しないといけないのは、inputには”バイト列”でデータを渡す必要があることです。str型だとエラーになるので、encode() してから渡します。ただ、text=True も指定するときは逆にstrで渡さないといけないようですね。

text = "すもももももももものうち"  # 入力するテキスト
text_byte = text.encode()  # byte型にエンコード

cp = subprocess.run(
    "mecab",
    capture_output=True,
    input=text_byte  # 通常はbyte型で標準入力を渡す
)

# byte型でデータが返ってきているので、decode()して表示
print(cp.stdout.decode())
"""
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
"""

# text=True を指定するときは str型で標準入力を渡す
cp = subprocess.run(
    "mecab",
    capture_output=True,
    text=True,
    input=text  # text=True を指定するときは str型で標準入力を渡す
)
# str型で格納されているのでそのままprintできる
print(cp.stdout)
"""
結果は同じなので略
"""

さて、標準入力の渡し方がわかったら、あとは先行するコマンドの標準出力を次のコマンドの標準入力に渡すだけです。

最初の ps aux | grep jupyter でやってみましょう。

cp1 = subprocess.run(
    ["ps", "aux"],
    capture_output=True,
    text=True,
)

cp2 = subprocess.run(
    ["grep", "jupyter"],
    capture_output=True,
    text=True,
    input=cp1.stdout  # 一つ目のコマンドの標準出力を渡す
)
print(cp2.stdout)
"""
yutaro             762   0.0  0.8  4315736  67720 s000  S    11:55PM   0:05.04 {Pythonのパス} {pyenvのパス} /versions/anaconda3-2019.10/bin/jupyter-notebook
"""

この記事の先頭のコマンドの結果と微妙に異なりますね。 grep jupyter のプロセスが出てきません。これは、ps aux だけ先行して動かし、その結果をもとにgrepしているので、厳密にはシェルでパイプラインしたのとは異なるからそうなっているのでしょう。

ただ、通常の用途であればほぼ同じ結果が得られると思います。
どうしても差分が気になるのであれば shell=Trueの方の方法を使うことも検討が必要でしょうね。

サンプルとして選んだコマンドがイマイチだったので、厳密にいうと再現できてないサンプルを提示してしまったのですが、このようにして、PythonでOSコマンドのパイプラインが再現できます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です