T-CREATOR

Python subprocess チート:標準入出力・パイプ・非同期実行の最短レシピ

Python subprocess チート:標準入出力・パイプ・非同期実行の最短レシピ

Python でシェルコマンドや外部プログラムを実行したいとき、subprocess モジュールは強力な味方になります。この記事では、標準入出力の制御からパイプ処理、非同期実行まで、実務で即使える最短レシピを一挙にご紹介します。

初めて subprocess を使う方でも、この記事を読めば基本から応用まで網羅的に理解できるでしょう。

subprocess 早見表

まず、よく使う関数とオプションを早見表でまとめました。必要な処理を素早く見つけられます。

主要関数比較表

#関数名用途戻り値待機推奨度
1subprocess.run()同期実行(Python 3.5+)CompletedProcessする★★★
2subprocess.Popen()低レベル制御・非同期Popen オブジェクトしない★★☆
3subprocess.call()同期実行(旧式)終了コードする★☆☆
4subprocess.check_output()出力取得(旧式)bytesする★☆☆

標準入出力制御オプション表

#オプション名説明使用例
1stdinsubprocess.PIPE標準入力をパイプ経由で渡すコマンドに文字列を送る
2stdoutsubprocess.PIPE標準出力をキャプチャ実行結果を取得する
3stderrsubprocess.PIPE標準エラーをキャプチャエラーメッセージを取得
4stderrsubprocess.STDOUTエラーを標準出力に統合出力を一つにまとめる
5stdoutsubprocess.DEVNULL出力を破棄不要な出力を抑制

よく使うオプション表

#オプション名説明デフォルト
1shellboolシェル経由で実行False
2checkboolエラー時に例外を発生False
3textbool文字列として扱う(Python 3.7+)False
4encodingstr文字エンコーディング指定None
5timeoutfloatタイムアウト秒数None
6cwdstr作業ディレクトリNone
7envdict環境変数None

背景

Python で外部コマンドを実行する必要性

Python は強力なプログラミング言語ですが、システム管理やデータ処理では外部コマンドと連携する場面が多くあります。

たとえば、git コマンドでバージョン管理を行ったり、ffmpeg で動画変換を実行したり、シェルスクリプトを Python から呼び出したりするケースです。こうした外部プログラムとの橋渡しを担うのが subprocess モジュールなのです。

subprocess モジュールの位置づけ

subprocess は Python 2.4 で導入され、古い os.system()os.popen() を置き換える形で発展してきました。

以下の図は、Python から外部プログラムを実行する際の基本フローを示しています。

mermaidflowchart LR
    python["Python プログラム"] -->|コマンド実行| subprocess["subprocess<br/>モジュール"]
    subprocess -->|プロセス起動| shell["シェル/外部<br/>プログラム"]
    shell -->|標準出力/エラー| subprocess
    subprocess -->|結果返却| python

このように、Python は subprocess を通じて外部プログラムを起動し、その実行結果や出力を受け取ります。

図で理解できる要点:

  • Python と外部プログラムの間に subprocess が介在する
  • 双方向でデータをやり取りできる
  • 標準入出力を通じて柔軟に連携可能

Python 3.5 以降の変化

Python 3.5 で subprocess.run() が追加され、より直感的で安全な API が提供されるようになりました。それ以前は call()check_output() を使い分ける必要がありましたが、現在は run() 一本で多くのケースに対応できます。

Python 3.7 では text パラメータが導入され、文字列処理がさらに簡単になりました。このように、subprocess は時代とともに進化を続けているのです。

課題

初心者が直面する 3 つの壁

subprocess を使い始めると、多くの開発者が以下の課題に直面します。

1. 標準入出力の制御が分かりにくい

外部コマンドの実行結果をどう取得するのか、エラー出力をどう扱うのか、初見では戸惑うことが多いです。

stdout=subprocess.PIPEstderr=subprocess.STDOUT といったオプションの意味を理解するまでに時間がかかります。

2. パイプ処理の複雑さ

Unix/Linux では grepawk を組み合わせてパイプ処理を行いますが、Python でこれを再現しようとすると、複数の Popen オブジェクトを連結する必要があり、コードが複雑になりがちです。

3. 同期実行と非同期実行の使い分け

短時間で終わるコマンドなら同期実行で問題ありませんが、時間のかかる処理や複数コマンドの並列実行では非同期処理が必要になります。

しかし、Popen を使った非同期実行は初心者には難易度が高いのです。

以下の図は、同期実行と非同期実行の違いを示しています。

mermaidstateDiagram-v2
    state "同期実行" as sync_mode {
        [*] --> CommandStart: run() 呼び出し
        CommandStart --> Waiting: プロセス起動
        Waiting --> ResultReturned: 完了待ち
        ResultReturned --> [*]: 結果取得
    }

    state "非同期実行" as async_mode {
        [*] --> ProcStart: Popen() 呼び出し
        ProcStart --> Continue: すぐ制御戻る
        Continue --> OtherWork: 他の処理実行
        OtherWork --> CheckResult: poll()/wait()
        CheckResult --> [*]: 結果取得
    }

図で理解できる要点:

  • 同期実行は完了まで待機してから結果を返す
  • 非同期実行はすぐに制御が戻り、他の処理を並行できる
  • 用途に応じて使い分けることが重要

セキュリティリスク

shell=True を安易に使うと、シェルインジェクション攻撃のリスクが生じます。

ユーザー入力をそのままコマンドに渡すと、悪意のあるコマンドが実行される可能性があるため、注意が必要です。

解決策

基本方針:run() を第一選択に

Python 3.5 以降では、ほとんどのケースで subprocess.run() を使うことをおすすめします。

シンプルで安全、かつ必要な機能が揃っているからです。非同期処理が必要な場合のみ Popen() を検討しましょう。

標準入出力制御の基本パターン

標準入出力を制御するには、stdinstdoutstderr パラメータを使います。以下が代表的なパターンです。

パターン 1: 標準出力を取得する

これは最も基本的な使い方です。コマンドの実行結果を Python で受け取ります。

pythonimport subprocess
python# ls コマンドの結果を取得
result = subprocess.run(
    ['ls', '-la'],
    stdout=subprocess.PIPE,  # 標準出力をキャプチャ
    text=True  # 文字列として扱う
)
python# 結果を表示
print(result.stdout)
print(f"終了コード: {result.returncode}")

text=True を指定すると、出力が文字列(str 型)で返ってきます。指定しない場合はバイト列(bytes 型)になるため、注意が必要です。

パターン 2: 標準エラーも同時に取得する

エラー出力を別々に取得したい場合は、stderr=subprocess.PIPE を追加します。

pythonresult = subprocess.run(
    ['python', 'script.py'],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,  # エラー出力も取得
    text=True
)
python# 出力とエラーを分けて表示
print("標準出力:", result.stdout)
print("標準エラー:", result.stderr)

これにより、正常な出力とエラーメッセージを明確に区別できます。

パターン 3: エラーを標準出力に統合する

エラーと標準出力を一つにまとめたい場合は、stderr=subprocess.STDOUT を使います。

pythonresult = subprocess.run(
    ['python', 'script.py'],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,  # エラーを標準出力に統合
    text=True
)
python# すべての出力が stdout に含まれる
print("全出力:", result.stdout)

ログファイルに全出力をまとめて記録する際に便利です。

パイプ処理の実現方法

Unix/Linux の command1 | command2 のようなパイプ処理を Python で実現するには、2 つの方法があります。

方法 1: shell=True を使う(簡単だが注意が必要)

シェルのパイプ機能をそのまま使う方法です。

python# 'ls -la | grep .py' と同じ
result = subprocess.run(
    'ls -la | grep .py',
    shell=True,  # シェルを経由
    stdout=subprocess.PIPE,
    text=True
)
pythonprint(result.stdout)

この方法は簡単ですが、shell=True はセキュリティリスクがあるため、ユーザー入力を含む場合は避けるべきです。

方法 2: Popen でパイプを連結する(推奨)

より安全な方法は、Popen を使って複数のプロセスを明示的に連結することです。

python# 最初のコマンド(ls -la)
proc1 = subprocess.Popen(
    ['ls', '-la'],
    stdout=subprocess.PIPE
)
python# 2番目のコマンド(grep .py)
proc2 = subprocess.Popen(
    ['grep', '.py'],
    stdin=proc1.stdout,  # proc1の出力を入力に
    stdout=subprocess.PIPE,
    text=True
)
python# proc1 の stdout を閉じる(重要)
proc1.stdout.close()
python# 最終結果を取得
output, _ = proc2.communicate()
print(output)

proc1.stdout.close() は SIGPIPE を正しく処理するために必要です。これを忘れると、プロセスが正常に終了しないことがあります。

以下の図は、パイプ処理のデータフローを示しています。

mermaidflowchart LR
    proc1["プロセス1<br/>(ls -la)"] -->|stdout| pipe1["PIPE"]
    pipe1 -->|stdin| proc2["プロセス2<br/>(grep .py)"]
    proc2 -->|stdout| result["最終結果"]
    python["Python"] -->|起動| proc1
    python -->|起動| proc2
    result -->|取得| python

図で理解できる要点:

  • プロセス 1 の標準出力がプロセス 2 の標準入力になる
  • Python が両プロセスを管理し、最終結果を取得する
  • パイプを介して安全にデータを受け渡しできる

非同期実行のパターン

時間のかかるコマンドを実行する際、Python プログラムをブロックせずに並行して作業したい場合は、Popen を使います。

基本的な非同期実行

python# コマンドを起動(すぐに制御が戻る)
proc = subprocess.Popen(
    ['sleep', '10'],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE
)
python# この間に他の処理ができる
print("コマンド実行中...")
python# 完了を待つ
stdout, stderr = proc.communicate()
print(f"終了コード: {proc.returncode}")

communicate() は、プロセスの完了を待ち、標準出力とエラー出力を返します。

タイムアウト付き実行

長時間実行されるコマンドには、タイムアウトを設定できます。

pythontry:
    result = subprocess.run(
        ['python', 'long_script.py'],
        timeout=5,  # 5秒でタイムアウト
        stdout=subprocess.PIPE,
        text=True
    )
except subprocess.TimeoutExpired:
    print("Error: タイムアウトしました")

これにより、無限ループなど予期しない状況でプログラムが固まるのを防げます。

進行状況の確認

実行中のプロセスの状態を確認するには、poll() メソッドを使います。

pythonproc = subprocess.Popen(['sleep', '5'])
pythonimport time
while proc.poll() is None:
    print("まだ実行中...")
    time.sleep(1)
pythonprint(f"完了しました。終了コード: {proc.returncode}")

poll() は、プロセスが終了していれば終了コードを、実行中なら None を返します。

具体例

ここでは、実務でよく使われる具体的なユースケースを紹介します。

例 1: Git コマンドの実行と出力取得

バージョン管理システムとの連携は、開発現場でよくある要件です。

pythonimport subprocess
python# 現在のブランチ名を取得
result = subprocess.run(
    ['git', 'branch', '--show-current'],
    stdout=subprocess.PIPE,
    text=True,
    check=True  # エラー時に例外発生
)
pythonbranch_name = result.stdout.strip()
print(f"現在のブランチ: {branch_name}")

check=True を指定すると、コマンドが失敗した場合(終了コードが 0 以外)に subprocess.CalledProcessError 例外が発生します。

エラーハンドリング付きの例

pythontry:
    result = subprocess.run(
        ['git', 'status', '--short'],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        check=True
    )
    print("変更ファイル:")
    print(result.stdout)
except subprocess.CalledProcessError as e:
    print(f"Error: Git コマンドが失敗しました")
    print(f"終了コード: {e.returncode}")
    print(f"エラー内容: {e.stderr}")

このように、エラー情報を詳細に取得して適切に処理できます。

例 2: ログファイルの grep 処理

大きなログファイルから特定のパターンを抽出する場合、grep コマンドを活用すると効率的です。

python# エラーログだけを抽出
result = subprocess.run(
    ['grep', 'ERROR', '/var/log/app.log'],
    stdout=subprocess.PIPE,
    text=True
)
python# 結果を1行ずつ処理
for line in result.stdout.splitlines():
    # タイムスタンプを抽出するなど
    print(f"エラー発見: {line[:50]}...")

grep が何も見つからない場合、終了コードは 1 になりますが、これはエラーではないため、check=True は使わない方が良いでしょう。

例 3: 複数コマンドの並列実行

複数の独立したコマンドを同時に実行することで、処理時間を大幅に短縮できます。

python# 複数のスクリプトを同時実行
processes = []

# スクリプトを起動
for script in ['script1.py', 'script2.py', 'script3.py']:
    proc = subprocess.Popen(
        ['python', script],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    processes.append((script, proc))
python# すべての完了を待つ
results = []
for script, proc in processes:
    stdout, stderr = proc.communicate()
    results.append({
        'script': script,
        'returncode': proc.returncode,
        'stdout': stdout,
        'stderr': stderr
    })
python# 結果を確認
for res in results:
    print(f"{res['script']}: 終了コード {res['returncode']}")
    if res['returncode'] != 0:
        print(f"  エラー: {res['stderr']}")

このパターンを使えば、シリアル実行では 30 秒かかる処理が、並列実行で 10 秒に短縮されることもあります。

例 4: 標準入力にデータを渡す

コマンドに対して標準入力経由でデータを送る方法です。

python# Python スクリプトに JSON データを渡す
import json

data = {'name': 'Alice', 'age': 30}
json_str = json.dumps(data)
pythonresult = subprocess.run(
    ['python', 'process_json.py'],
    input=json_str,  # 標準入力として渡す
    stdout=subprocess.PIPE,
    text=True
)
pythonprint(result.stdout)

input パラメータを使うことで、簡単にデータを送り込めます。

例 5: 環境変数を設定して実行

特定の環境変数を設定してコマンドを実行したい場合は、env パラメータを使います。

pythonimport os

# 現在の環境変数をコピー
env = os.environ.copy()
python# 追加の環境変数を設定
env['DEBUG'] = '1'
env['API_KEY'] = 'secret-key-12345'
python# 環境変数付きで実行
result = subprocess.run(
    ['python', 'api_client.py'],
    env=env,
    stdout=subprocess.PIPE,
    text=True
)

os.environ.copy() で現在の環境変数をコピーしてから、必要な変数を追加するのがポイントです。

例 6: 作業ディレクトリを変更して実行

特定のディレクトリでコマンドを実行したい場合は、cwd パラメータを使います。

pythonresult = subprocess.run(
    ['ls', '-la'],
    cwd='/tmp/work',  # このディレクトリで実行
    stdout=subprocess.PIPE,
    text=True
)
pythonprint(result.stdout)

Python スクリプト自体のカレントディレクトリは変わらないため、安全に別ディレクトリでコマンドを実行できます。

以下の図は、よくある subprocess の使用パターンを示しています。

mermaidflowchart TD
    start["コマンド実行<br/>開始"] --> sync_q{"同期/非同期<br/>どちら?"}

    sync_q -->|同期| run_func["run() 使用"]
    sync_q -->|非同期| popen_func["Popen() 使用"]

    run_func --> output_q{"出力取得<br/>必要?"}
    output_q -->|はい| pipe_out["stdout=PIPE"]
    output_q -->|いいえ| no_capture["stdout 指定なし"]

    pipe_out --> error_q{"エラー処理<br/>必要?"}
    error_q -->|はい| check_true["check=True"]
    error_q -->|いいえ| check_false["check=False"]

    popen_func --> async_wait["communicate() で<br/>完了待ち"]
    async_wait --> poll_check["poll() で<br/>状態確認"]

    check_true --> done["実行完了"]
    check_false --> done
    no_capture --> done
    poll_check --> done

図で理解できる要点:

  • 同期/非同期の選択が最初の分岐点
  • 出力取得の有無でオプションが変わる
  • エラー処理の要否で check パラメータを使い分ける

まとめ

この記事では、Python の subprocess モジュールを使った外部コマンド実行の実践テクニックを解説しました。

重要なポイント:

  1. 基本は run() を使う: Python 3.5 以降では、ほとんどのケースで subprocess.run() が最適です。シンプルで安全、かつ必要十分な機能を備えています。

  2. 標準入出力の制御: stdout=subprocess.PIPE で出力を取得し、text=True で文字列として扱うのが基本パターンです。エラー出力は stderr=subprocess.PIPE または stderr=subprocess.STDOUT で制御できます。

  3. パイプ処理: 複数コマンドの連携は、Popen で明示的にパイプを連結する方法が安全です。shell=True は簡単ですが、セキュリティリスクに注意が必要です。

  4. 非同期実行: 時間のかかる処理や並列実行には Popen を使い、communicate()poll() で制御します。タイムアウトも設定できます。

  5. エラー処理: check=True で自動的に例外を発生させるか、終了コードを確認して手動で処理するかを使い分けましょう。

  6. セキュリティ: ユーザー入力を含むコマンド実行では、リスト形式でコマンドを渡し、shell=True は避けるのが鉄則です。

subprocess は外部プログラムとの強力な橋渡し役です。この記事で紹介したパターンを使えば、シェルスクリプトに頼らず、Python だけで柔軟なシステム連携が実現できるでしょう。

最初は複雑に感じるかもしれませんが、基本パターンを押さえれば、応用は無限大です。ぜひ実際のプロジェクトで試してみてください。

関連リンク