見出し画像

Python基礎15:サブプロセス管理(subprocess)

1.概要

1-1.subprocessとは

 Pythonのsubprocessモジュールは、新しいプロセスを生成、プロセスの入出力管理、プロセスの終了コードを取得できます。つまりsubprocessモジュールを使用すると、Pythonスクリプト内から他のプログラムやコマンドを起動することができます。例えばテキストファイルを開いたり、ディレクトリ内のファイル一覧を取得したり、ウェブサイトからデータをダウンロードしたりといった操作が可能です。

 なお本記事での実装はWindows PCで作成したため、引数はOSに応じて最適に調整可能です。

1-2.os.system()との違い

 os.systemもPythonで他のプログラムを実行するための方法の一つですが、subprocessと比べて機能が限定的です。公式Docsにも「subprocess モジュールは新しいプロセスを実行して結果を取得するためのより強力な機能を提供しています。この関数の代わりに subprocess モジュールを利用することが推奨されています。」とあります。

[API]
os.system(command)

 os.systemは指定したコマンドを実行して終了ステータスを返すだけで、コマンドの出力を取得する機能はありません。また常にシェル経由でコマンド実行するためセキュリティリスク(シェルインジェクション)があります。
 一方、subprocessはコマンドの出力を取得しさらに入力をコマンドに渡す機能もあります。シェルもshell=Falseを指定することで設定できます。
 よってコマンドの出力を取得したり、シェルを介さずにコマンドを実行したりする場合にはsubprocessが便利です。

[IN]
import subprocess

result = subprocess.run('echo Hello World!', capture_output=True, text=True, shell=True)
print(result.stdout)

import os
result_os = os.system('echo Hello World!')
print(result_os)
[OUT]
Hello World!

0

 os.systemは'Hello World!'をコンソールに直接出力するため、Jupyterで取得した結果は0となります。subprocessでは'Hello World!'はコマンドの出力として捕捉され、Pythonプログラム内で利用できます。

2.事前知識

2-1.基礎用語

 事前に基本的な用語を紹介します。

  1. OS (Operating System, オペレーティングシステム): OSはコンピュータの基本的なソフトウェアで、ハードウェアとソフトウェアの間のインターフェースとして動作します。具体的には、キーボードやマウスからの入力を受け取り、スクリーンに何を表示するかを制御したり、ファイルをディスクに保存したり、ネットワーク通信を管理したりします。Windows、macOS、Linuxなどが主な例です。

  2. Unix系OS: Unixは1970年代にAT&Tのベル研究所で開発されたOSです。その設計哲学と機能は多くの後続のOSに影響を与え、これらを合わせて"Unix系OS"と呼びます。LinuxやmacOSはUnix系OSの例で、Unixの設計原則に基づいています。Unix系OSの特徴的な要素には、シェルスクリプト、階層型ファイルシステム、多ユーザー対応などがあります。

  3. コマンドライン:ユーザーがコンピュータに対して直接命令を入力して操作するテキストベースのインターフェースです。WindowsではコマンドプロンプトやPowerShell、LinuxやMacではターミナルが該当します。

  4. サブプロセス:あるプロセスから作成された別のプロセスのこと。サブプロセスは新しい独立したプロセスとして動作し、元のプロセス(親プロセス)とは独立して停止、再開、実行できます。

  5. シェル:ユーザーのコマンドを読み取り、それを実行するプログラム。シェルはユーザーのキーボード入力をOSのコマンドとして解釈し、対応するアクションを行います。

  6. パイプライン:一連のプロセスの間でデータを直接転送する一連のデータストリームのこと。コマンドラインでパイプ(|)を使用すると、一つのプロセスの出力を別のプロセスの入力に直接つなげることができます。

  7. API:API(Application Programming Interface)とはアプリケーションプログラミングインターフェースの略でソフトウェアが他のソフトウェアとやり取りするための規則や手順の集合を指します。APIには多くの種類がありますが、その中でも一般的には「高水準API」および「低水準API」という二つのカテゴリがあります。

    • 高水準API:いわゆる短いコードでシンプルに使えるもの。

    • 低水準API:いわゆる長いコードを書く必要があるけど、細かいことまで実装できるもの

  8. Jupyter Notebook: Jupyter NotebookはPythonや他の多くのプログラミング言語でコードを記述、実行できる対話型のオープンソースのウェブアプリケーションです。ノートブック形式のドキュメントを作成でき、これにはコード、テキスト、画像、グラフ等を含めることができます。

【コラム:コマンドライン(CLI)とJupyter Notebookでの操作】
 基本的にはどちらでも使用可能ですが環境によっては動作が異なる点は注意が必要です。

  • 作業ディレクトリ:CLIは「実行dir=作業dir」であり、Jupyterは「ファイル保存場所=作業dir」です。

  • Jupyterでは実行した出力もノートブック上に表示されます

2-2.コマンド一覧

 subprocessモジュールでは基本的にはOSのシェルで実行できるコマンド全てを実行できます。なお、それぞれのOSによって使用可能なコマンドは異なります
 以下に、WindowsとUnix系OS(Linux、macOSなど)でよく使用される基本的なコマンドを示します。なお、subprocessを使ってこれらのコマンドを実行する際、一部のコマンドは引数の形式が異なることに注意が必要です。

 2-2-1.Windowsのコマンド

 注意点としてsubprocessはデフォルトでは実行可能ファイルを操作します。しかしWindowsの多くのコマンドはコマンドプロンプト(cmd.exe)の一部として実装されているため"shell=True"を指定しないと”FileNotFoundError”がでるコマンドがあります。

  • dir:ディレクトリの内容を表示

  • cd:現在のディレクトリを変更

  • type:ファイルの内容を表示

  • copy:ファイルをコピー

  • del:ファイルを削除

  • move:ファイルを移動

  • echo:文字列を表示

  • set:環境変数を設定

  • find:テキストファイルの内容を検索

  • start:新しいプロセスを開始

  • ipconfig: ネットワーク設定の詳細を表示

  • ping: ネットワーク経由で他のシステムとの接続をテスト

  • notepad: テキストエディタのNotepadを開く

 2-2-2.Unix系OSのコマンド

 UNIX系OSでは「ls」や「echo」などのコマンドはそれ自体が独立した実行可能ファイルとしてシステムに存在します。よってWindowsのように"shell=True"を指定しなくても実行可能です。
 ただしUNIX系OSでもシェルの内蔵コマンドや特定のシェル機能(パイプライン、リダイレクションなど)を使用する場合は、shell=Trueを使用する必要があります。

  • ls:ディレクトリの内容を表示

  • cd:現在のディレクトリを変更

  • cat:ファイルの内容を表示

  • cp:ファイルをコピー

  • rm:ファイルを削除

  • mv:ファイルを移動

  • echo:文字列を表示

  • export:環境変数を設定

  • grep:テキストファイルの内容を検索

  • &:新しいプロセスを開始

3.基本操作

 subprocessでは主にrun()メソッドを使用しますこれは新しいプロセスを開始し、そのプロセスが終了するのを待ち、その結果をCompletedProcessインスタンスとして返します。
 Python 3.5 より前のバージョンではサブプロセスに対して以下の 3 つの関数からなる高水準 API が用意されていましたが、今は run()で対応できます。ただし、既存のコードで使用されているため理解しておくと便利です。

  • subprocess.call

  • subprocess.check_call

  • subprocess.check_output

3-1.シンプルな実装:subprocess.run

 文字列を出力する"echo"コマンドを使用して"Hello World"を表示させます。run()に渡すコマンドラインは文字列だけでなく、コマンドをリストで渡すこともできます。引数としては下記があります。

  • args(必須):実行するためのコマンド

  • capture_output:標準出力と標準エラー出力を捕捉するためのオプション

    • 結果はCompletedProcessオブジェクトのstdoutstderr属性で取得

    • コマンドの終了コード/エラーはCompletedProcessオブジェクトのreturncode属性で取得

  • check:Trueでコマンドがエラーを返した場合にすぐに例外を出力

  • text:Trueで出力をバイト列ではなく文字列として取得

  • shell:Pythonは指定したコマンドをシェル経由で実行する

    • Windowsのシェル(コマンドプロンプト)の組み込みコマンドを使用することができる

    • shell=Trueだと「OSコマンド・インジェクション」による危険性がある(Shellを操作できるようになるため外部から実行されると危険)

【コマンドを文字列で渡す】
 
文字列全体がシェルに渡され、シェルがその文字列を解釈してコマンドとその引数を認識します。文字列内にスペースが含まれている場合、スペースで区切られた各部分が別々の引数として認識されます

[IN]
import subprocess

result = subprocess.run('echo Hello World!', capture_output=True, text=True, shell=True)
print(result)
print(result.stdout)
[OUT]
CompletedProcess(args='echo Hello World!', returncode=0, stdout='Hello World!\n', stderr='')
Hello World!

【コマンドをリストで渡す】
 
リストの各要素が個別の引数としてコマンドに渡されます。引数の中にスペースが含まれている場合でも、その引数は1つのまとまりとして扱われスペースによって分割されません

[IN]
result = subprocess.run(['echo', 'Hello World!'], capture_output=True, text=True, shell=True)
print(result)
print(result.stdout)
[OUT]
CompletedProcess(args=['echo', 'Hello World!'], returncode=0, stdout='"Hello World!"\n', stderr='')
"Hello World!"

【コラム:Windowsではshell=Trueが必要】
 2章で説明した通りWindowsのechoコマンドは実行可能ファイルではなくコマンドプロンプト(CMD)の組み込みとなっているため、そのまま指定すると"FileNotFoundError"になります。例えばWIndowsでも”ipconfig”を実行すれば"shell=False"でもエラーなく実行できます。

[IN]
import subprocess

# ipconfigの実行
result = subprocess.run(['ipconfig'], capture_output=True, text=True)
print(result.stdout)

[OUT]
ネットワーク情報が出力されます

3-2.OS別で実装:os.name

 前述の通りOSでコマンドが異なるため、別のOSでも実行させる場合はosモジュールの"os.name()"とif文を組み合わせます。WIndowsの場合、os.name()は"nt"となるため、その下にWindows用のコマンドを記載します。

 参考としてWindowsの場合に、「コマンドプロンプト (cmd) を起動し (/c オプション)、dir コマンドを実行」するコマンドを記載しました。"dir"コマンド、"ls"コマンドはディレクトリ内のファイル一覧を取得します。

[IN]
import subprocess
import os

# 現在のOSを確認し、対応するコマンドを選択します。
if os.name == 'nt':  # Windowsの場合
    command = ['cmd', '/c', 'dir']
else:  # Unix系OSの場合
    command = ['ls']

# コマンドを実行します。
result = subprocess.run(command, capture_output=True, text=True)

# 結果を表示します。
print(result.stdout)
[OUT]
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は 1C11-XXXX です

 c:\Users\KIYO\Desktop\note\01_Python基礎\note_python基礎15_subprocess のディレクトリ

2023/07/02  14:05    <DIR>          .
2023/07/02  14:05    <DIR>          ..
2023/07/02  14:05            11,582 note_subprocess.ipynb
2023/07/02  09:28            66,627 note見出し_Python基礎.jpg
2023/07/02  10:12               268 s1.py
2023/07/02  10:40               266 s2.py
2023/07/02  11:01               446 s3.py
               5 個のファイル              79,189 バイト
               2 個のディレクトリ  406,233,956,352 バイトの空き領域

  参考までに作業ディレクトリは下記の通りです。

3-3.エラー処理:check=True

 コマンドがエラーを返した場合にすぐに例外を送出させたい場合は、check=Trueオプションを使用します。下記では存在しないディレクトリに対してlsコマンドを実行しており、このコマンドはエラーを返すためsubprocess.CalledProcessErrorが出力され、それがexcept節で捕捉されます。例外オブジェクトにはreturncodestdoutstderr属性があり、それぞれ終了コード、標準出力、標準エラー出力を取得できます。

[IN]
import subprocess

try:
    result = subprocess.run(['ls', '/nonexistent_directory'], capture_output=True, text=True, check=True, shell=True)
except subprocess.CalledProcessError as e:
    print(f'e.returncode: {e.returncode}')
    print(f'e.stdout: {e.stdout}')
    print(f'e.stderr: {e.stderr}')
[OUT]
e.returncode: 1
e.stdout: 
e.stderr: 'ls' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

4.応用操作

4-1.ワイルドカード

 ワイルドカード(*)を使用すると特定のパターンに一致する複数のファイルを一度に操作することができます。これはシェルの機能でPythonのsubprocessではshell=Trueを指定することで利用可能です。以下に現在のディレクトリ内のすべてのPythonファイルを一覧表示する例を示します。

[IN]
import subprocess

result = subprocess.run('dir *.py', capture_output=True, text=True, shell=True)
print(result.stdout)
[OUT]
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は 1C11-XXXX です

 c:\Users\KIYO\Desktop\note\01_Python基礎\note_python基礎15_subprocess のディレクトリ

2023/07/02  10:12               268 s1.py
2023/07/02  10:40               266 s2.py
2023/07/02  11:01               446 s3.py
               3 個のファイル                 980 バイト
               0 個のディレクトリ  406,231,502,848 バイトの空き領域

4-2.パイプライン

 シェルには一つのコマンドの出力を別のコマンドの入力として使用するためのパイプライン(|)という機能があります。これもshell=Trueを指定することで利用可能です。以下に"s1.py"ファイルから特定の文字列("World")を検索する例を示します。

[s1.py]
import argparse

parser = argparse.ArgumentParser() #parserを作成
parser.add_argument("arg1", help="arg1 description") #parserに引数を追加

args = parser.parse_args() #parserを解析
print(args)
print(type(args))
print(f'Hello World:{args.arg1}')
[IN]
import subprocess

# 'some text'が含まれる行を検索します。
command = 'type s1.py | findstr "World"' #s1.pyというファイル->findstr "World"でWorldという文字列を検索
result = subprocess.run(command, capture_output=True, text=True, shell=True)
print(result.stdout)
[OUT]
print(f'Hello World:{args.arg1}')

5.Pythonスクリプトを実行

 Jupyter上からPythonスクリプトを実行します。os.getcwd()でも可能ですが、作業ディレクトリをsubprocessで確認しました。

【Windows】

[IN]
import subprocess

result = subprocess.run('cd', capture_output=True, text=True, shell=True)
print(result.stdout)

[OUT]
c:\Users\KIYO\Desktop\note\01_Python基礎\note_python基礎15_subprocess

Unix系OS(LinuxやMacOS)

[IN]
import subprocess

result = subprocess.run('pwd', capture_output=True, text=True, shell=True)
print(result.stdout)

5-1.シンプルなスクリプト実行

 次に引数を受け取るモジュールを実行します。以下のs3.pyスクリプトは、複数の引数をパースし、それらの引数を使って'{arg1} {arg2}'と表示します。

[s1.py]
print("Hello World")
[IN]
import subprocess

result = subprocess.run(['python', 's1.py'], capture_output=True, text=True)
print(result.stdout)
[OUT]
Hello World

5-2.引数をコマンドリストに追加

 引数を受け取るモジュールを実行します。

  • s2.py:コマンドライン引数をパースし、その引数を使って'Hello World:{arg}'と表示します。

  • s3.py:複数の引数をパースし、それらの引数を使って'{arg1} {arg2}'と表示します。

【Case1:引数が1つ】

[s2.py]
import argparse

parser = argparse.ArgumentParser() #parserを作成
parser.add_argument("arg1", help="arg1 description") #parserに引数を追加

args = parser.parse_args() #parserを解析
print(args)
print(type(args))
print(f'Hello World:{args.arg1}')
[IN]
result = subprocess.run(['python', 's2.py', 'KIYO'], capture_output=True, text=True)
print(result.stdout)
[OUT]
Namespace(arg1='KIYO')
<class 'argparse.Namespace'>
Hello World:KIYO

 s2.pyの引数は必須引数のため指定しないとエラーが出ます。エラーの有無は”capture_output=True”として"stderr"や"returncode"で確認できます。

[IN]
result = subprocess.run(['python', 's2.py'], capture_output=True, text=True)
print(result.stdout)
print(result.stderr)
print(result.returncode)
[OUT]
usage: s2.py [-h] arg1
s2.py: error: the following arguments are required: arg1

2

【Case2:引数が複数】
 今回のスクリプトはオプション引数で指定しているため、コマンドもオプション引数で記載しました。

[s3.py]
import argparse

parser = argparse.ArgumentParser() #parserを作成
#parserに引数を追加
parser.add_argument("-a1", "--arg1", default='Hello', help="arg1 help") 
parser.add_argument("--arg2", default='World')
parser.add_argument("-v", "--verbose", action='store_true') #引数があるときはTrue, ないときはFalse

args = parser.parse_args() #parserを解析
print(args)
print(type(args))
print(f'{args.arg1} {args.arg2}')
[IN]
import subprocess

result = subprocess.run(['python', 's3.py', '--arg1=Good Morning', '--arg2=KIYO'], capture_output=True, text=True)
print(result.stdout)
[OUT]
Namespace(arg1='Good Morning', arg2='KIYO', verbose=False)
<class 'argparse.Namespace'>
Good Morning KIYO

 argparseで実装したモジュールは"-h"や"--help"コマンドを実行することで引数の確認ができます。

[IN]
result = subprocess.run(['python', 's3.py', '-h'], capture_output=True, text=True)
print(result.stdout)
[OUT]
usage: s3.py [-h] [-a1 ARG1] [--arg2 ARG2] [-v]

optional arguments:
  -h, --help            show this help message and exit
  -a1 ARG1, --arg1 ARG1
                        arg1 help
  --arg2 ARG2
  -v, --verbose

5-3.パッケージ内のスクリプト実行

 スクリプトをフォルダ内に入れてパッケージとして管理している場合もあるため、最後にパッケージ内のスクリプトを実行します。
 以下のmods/s4.pyスクリプトは引数をパースしてその引数を使って'Hello World:{arg1}'と表示します。コマンドは"<dir名>/<ファイル名>.py"で指定するだけで他に違いはありません。

[mods/s4.py]
import argparse

parser = argparse.ArgumentParser() #parserを作成
parser.add_argument("-a1", "--arg1", help="arg1 help") #parserに引数を追加

args = parser.parse_args() #parserを解析
print(args)
print(type(args))
print(f'Hello World:{args.arg1}')
[IN]
import subprocess

result = subprocess.run(['python', 'mods/s4.py', '--arg1=KIYO'], capture_output=True, text=True)
print(result.stdout)
[OUT]
Namespace(arg1='KIYO')
<class 'argparse.Namespace'>
Hello World:KIYO

6.API

6ー1.subprocess.run

[API]
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, 
               capture_output=False, shell=False, cwd=None, timeout=None, 
               check=False, encoding=None, errors=None, text=None, env=None, 
               universal_newlines=None, **other_popen_kwargs)

6ー2.Popen オブジェクト

 新しいプロセスを開始し、そのプロセスオブジェクトを直接操作できます。これによりプロセスの入出力ストリームをリアルタイムで読み書きすることが可能になります。

6ー3.subprocess.call※古い高水準 API

 subprocess.run()と同様に新しいプロセスを作成しますが、結果のオブジェクトを返す代わりに直接終了ステータスを返します。

[API]
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, 
                shell=False, cwd=None, timeout=None, **other_popen_kwargs)

6ー4.subprocess.check_call※古い高水準 API

 この関数は新しいプロセスを起動しそのプロセスが終了するのを待ちます。プロセスが成功(つまり終了ステータスが0)で終了した場合は0を返し、それ以外の場合はCalledProcessError例外を送出します。そのため、プロセスが成功したことだけを確認したい場合に便利です。

[API]
subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None,
                      shell=False, cwd=None, timeout=None, **other_popen_kwargs)

6ー5.subprocess.check_output※古い高水準 API

 コマンドの出力を直接文字列として返します。コマンドがエラーコードで終了した場合、この関数は例外を発生させます。

[API]
subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, 
                        cwd=None, encoding=None, errors=None, 
                        universal_newlines=None, timeout=None, 
                        text=None, **other_popen_kwargs)


参考記事

あとがき

 とりあえず先出。


この記事が気に入ったらサポートをしてみませんか?