Python の subprocess.run で外部コマンドの出力を取得する

subprocess.run() で外部コマンドを実行するだけなら簡単だが、実際にはコマンドの出力結果を Python 側で受け取って処理したい場面のほうが多い。ディスク使用量を取得して集計する、Git のログを解析する、外部ツールの出力をパースしてレポートを生成するなど、出力のキャプチャは subprocess を使う上で欠かせない機能だ。

capture_output=True で出力を取得する

Python 3.7 以降では、capture_output=True を指定するだけで標準出力と標準エラー出力の両方をキャプチャできる。

import subprocess

result = subprocess.run(["echo", "Hello"], capture_output=True, text=True)
print(result.stdout)   # Hello\n
print(result.stderr)   # (空文字列)

text=True を併用しているのがポイントだ。これを指定しないと stdoutstderr はバイト列(bytes)として返される。text=True を付ければ文字列(str)として扱えるため、後続の処理が格段に楽になる。

text=True あり

stdout/stderr が str 型で返る。文字列処理にそのまま使える

text=True なし

stdout/stderr が bytes 型で返る。デコードが必要になる

PIPE を使った方法

capture_output=True は内部的に stdout=subprocess.PIPE, stderr=subprocess.PIPE を設定しているだけだ。Python 3.6 以前や、stdout だけをキャプチャしたい場合は PIPE を直接指定する。

import subprocess

# stdout のみキャプチャ(stderr はそのまま端末に出る)
result = subprocess.run(
    ["ls", "-la"],
    stdout=subprocess.PIPE,
    text=True
)
print(result.stdout)
import subprocess

# stdout と stderr を両方キャプチャ
result = subprocess.run(
    ["ls", "/nonexistent"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
print("stdout:", result.stdout)
print("stderr:", result.stderr)

capture_output=Truestdout=PIPE を同時に指定するとエラーになるため、どちらか一方だけを使う。基本的には capture_output=True で十分だが、片方だけキャプチャしたいときは PIPE を直接指定するとよい。

出力を加工する実践例

キャプチャした出力を Python で加工する典型的なパターンをいくつか見てみよう。

import subprocess

# ディスク使用量を取得して表示
result = subprocess.run(
    ["df", "-h", "/"],
    capture_output=True, text=True, check=True
)

lines = result.stdout.strip().split("\n")
for line in lines:
    print(line)

コマンドの出力は末尾に改行を含むことが多いため、strip() で余分な空白を除去してから split("\n") で行ごとに分割するのが定番のパターンだ。

もう少し実用的な例として、Git のコミットログから情報を抽出してみる。

import subprocess

result = subprocess.run(
    ["git", "log", "--oneline", "-5"],
    capture_output=True, text=True, check=True
)

commits = result.stdout.strip().split("\n")
for i, commit in enumerate(commits, 1):
    hash_val, message = commit.split(" ", 1)
    print(f"{i}. [{hash_val}] {message}")

外部コマンドの出力をそのまま表示するだけでなく、Python のデータ構造に変換して再利用できるのが subprocess の強みだ。

stderr を stdout にまとめる

コマンドによっては、通常の出力と警告メッセージが stdout と stderr に分かれて出力される。これらをまとめて 1 つのストリームとして扱いたい場合は stderr=subprocess.STDOUT を使う。

import subprocess

result = subprocess.run(
    ["python3", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    capture_output=False,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True
)

print(result.stdout)
# out
# err

stderr=subprocess.STDOUT は stderr の内容を stdout に合流させるため、result.stdout に両方の出力が時系列順で入る。result.stderrNone になる点に注意が必要だ。

なお、capture_output=Truestderr=subprocess.STDOUT は併用できない。capture_output=True は内部で stderr=PIPE を設定するため、競合が発生する。

ValueError: stderr and capture_output may not both be set というエラーになる。

encoding パラメータ

text=True の代わりに encoding パラメータで文字コードを明示的に指定することもできる。日本語環境では Shift_JIS で出力するコマンドに遭遇することがあり、そうした場合に役立つ。

import subprocess

# Windows の dir コマンドなど、Shift_JIS で出力するケース
result = subprocess.run(
    ["some_command"],
    capture_output=True,
    encoding="shift_jis"
)

encoding を指定すると text=True は不要になる。逆に text=True を指定した場合はシステムのデフォルトエンコーディングが使われる。文字化けが起きたときは、まず encoding の明示指定を試してみるのがよい。

エラー時の出力を取得する

check=Truecapture_output=True を組み合わせると、コマンドが失敗した場合でも CalledProcessError の属性から出力を取得できる。

import subprocess

try:
    result = subprocess.run(
        ["python3", "-c", "raise ValueError('test error')"],
        capture_output=True, text=True, check=True
    )
except subprocess.CalledProcessError as e:
    print(f"終了コード: {e.returncode}")
    print(f"stderr: {e.stderr}")

例外オブジェクトの e.stdoute.stderr にキャプチャされた出力が格納されているため、エラーの原因を解析するのに使える。ログに記録したり、ユーザーにわかりやすいエラーメッセージを組み立てたりする際に重宝する。

出力のキャプチャは subprocess を実用レベルで使いこなすための必須知識だ。capture_output=Truetext=True の組み合わせを基本形として覚えておけば、大半のユースケースに対応できるだろう。