pythonサードパーティライブラリシリーズの二十二--subprocessの使い方


一、なぜsubprocess
Python 2.4から、Pythonはsubprocessモジュールを導入してサブプロセスを管理し、古いモジュールの方法、例えばos.system、os.spawn*、os.popen*、popen2.*、commands.*,外部のコマンドをサブプロセスとして呼び出すだけでなく、サブプロセスのinput/output/errorパイプに接続して、関連する戻り情報を取得できます.
二、subprocess及びよく使われるパッケージ関数
pythonを実行するときは、プロセスを作成して実行します.Linuxプロセスのように、1つのプロセスは1つのサブプロセスをforkし、このサブプロセスexecの別のプログラムを譲ることができます.Pythonでは、標準ライブラリのsubprocessパッケージを使用してサブプロセスをforkし、外部のプログラムを実行します.
subprocessパッケージには、サブプロセスを作成する関数がいくつか定義されています.これらの関数は、それぞれ異なる方法でサブプロセスを作成するので、必要に応じて使用を選択できます.さらにsubprocessは、プロセス間でテキスト通信を使用するために標準ストリームとパイプを管理するツールも提供しています.
(1)subprocess.call()
親プロセスは子プロセスの完了を待つ
終了情報を返す(returncode、Linux exit codeに相当)
(2)subprocess.check_call()
親プロセスは子プロセスの完了を待つ
0を返します
終了情報をチェックし、returncodeが0でない場合はエラーsubprocessを挙げる.CalledProcessError、このオブジェクトにはreturncode属性が含まれており、try...except...でチェックできます.
(3)subprocess.check_output()
親プロセスは子プロセスの完了を待つ
サブプロセスから標準に出力された出力結果を返します.
終了情報をチェックし、returncodeが0でない場合はエラーsubprocessを挙げる.CalledProcessError、このオブジェクトにはreturncode属性とoutput属性が含まれています.output属性は標準出力の出力結果で、try...except...でチェックできます.
この3つの関数の使用方法は類似しており、以下ではsubprocess.call()の例:
>>> import subprocess
>>> retcode = subprocess.call(["ls", "-l"])
# shell   ls -a      
>>> print retcode
0
は、プログラム名(ls)と、持つパラメータ(-l)とをともに1つのテーブルに配置してsubprocessに渡す.call()
shellのデフォルトはFalseであり、Linuxでshell=Falseの場合、Popenはosを呼び出す.execvp()argsで指定したプログラムを実行します.shell=Trueの場合、argsが文字列である場合、Popenは直接システムのShellを呼び出してargsが指定したプログラムを実行し、argsがシーケンスである場合、argsの最初の項目はプログラムコマンド文字列を定義し、他の項目はシステムShellを呼び出す際の追加パラメータである.
上記の例は以下のように書くこともできます.
>>> retcode = subprocess.call("ls -l",shell=True)
Windowsでは、shellの値に関係なく、PopenはCreateProcess()を呼び出してargsで指定された外部プログラムを実行します.argsがシーケンスである場合はlist 2 cmdline()で文字列に変換しますが、MS Windowsの下のすべてのプログラムがlist 2 cmdlineでコマンドライン文字列に変換できるわけではありません.
(4)subprocess.Popen
class Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
実際,上のいくつかの関数はPopen()に基づくカプセル化(wrapper)である.これらのパッケージの目的は、サブプロセスを容易に使用できるようにすることです.私たちのニーズをより個性化したい場合は、サブプロセスを表すために生成されたオブジェクトを使用するPopenクラスに移行します.
上のパッケージとは異なり、Popenオブジェクトが作成されると、メインプログラムはサブプロセスの完了を自動的に待つことはありません.親プロセスが待つ(すなわちブロックブロック)ように、オブジェクトのwait()メソッドを呼び出さなければなりません.例:
>>> import subprocess
>>> child = subprocess.Popen(['ping','-c','4','blog.linuxeye.com'])
>>> print 'parent process'
実行結果から、親プロセスは、子プロセスを開いた後、childの完了を待つのではなく、printを直接実行していることがわかります.
待機状況の比較:
>>> import subprocess
>>> child = subprocess.Popen('ping -c4 blog.linuxeye.com',shell=True)
>>> child.wait()
>>> print 'parent process'
実行結果から、親プロセスは、サブプロセスが開始された後、childの完了を待ってからprintを実行する.
また、上記の例のchildオブジェクトなど、親プロセスでサブプロセスを他の操作することもできます.
child.poll()           #        
child.kill()           #      
child.send_signal()    #         
child.terminate()      #      
    PID   child.pid

(5)サブプロセスのテキストフロー制御サブプロセスの標準入力、標準出力および標準エラーは、それぞれ次の属性で表される.
child.stdin
child.stdout
child.stderr
は、Popen()がサブプロセスを確立するときに、標準入力、標準出力、および標準エラーを変更する、subprocessを利用することができる.PIPEは、複数のサブプロセスの入出力を接続してパイプ(pipe)を構成し、以下の2つの例である.
>>> import subprocess
>>> child1 = subprocess.Popen(["ls","-l"], stdout=subprocess.PIPE)
>>> print child1.stdout.read(),
#  child1.communicate()
>>> import subprocess
>>> child1 = subprocess.Popen(["cat","/etc/passwd"], stdout=subprocess.PIPE)
>>> child2 = subprocess.Popen(["grep","0:0"],stdin=child1.stdout, stdout=subprocess.PIPE)
>>> out = child2.communicate()
subprocess.PIPEは実際にテキストストリームにキャッシュ領域を提供する.child 1のstdoutはテキストをキャッシュ領域に出力し、child 2のstdinはこのPIPEからテキストを読み出す.child 2の出力テキストも、communicate()メソッドがPIPEからPIPEのテキストを読み出すまでPIPEに格納される.
注:communicate()は、親プロセスが子プロセスが完了するまでブロックされるPopenオブジェクトのメソッドです.
三、タイムアウト処理を加える
def system_exec(args, timeout=2, shell=False, interval=0.01):
    '''
    @args: command array
    '''
    retval = 0 
    output = "success"

    end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout)

    try:
        # output = subprocess.check_output(args, shell=shell)
        #                 ,         ;
        sub = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell, bufsize=4096)
        
        #subprocess.poll()  :          ,     ,      ,  subprocess.returncode   
        while sub.poll() is None:
            time.sleep(interval)
            if end_time <= datetime.datetime.now():
                sub.terminate()
                return (-1, "Command Timeout")
            else:
                pass

        ## read all lines from stdout
        with sub.stdout as fd:
            output = fd.read()     # read() or readlines()
        retval = sub.returncode
        output = output.strip()    ## strip the last 
fd.close() except Exception as e: retval = -1 output = str(e) return retval, output