2017/03/21

Pythonのsubprocessで外部プロセスの標準入出力と戻り値を扱う

Pythonのsubprocessモジュール内の機能を用いると、外部プロセスを実行して標準入出力を扱ったり終了ステータス(プロセスの戻り値)を取得したりできる。

同モジュールはバージョン2.4で追加されたものだが、これよりも新しいバージョンのPythonが既に十分普及しており、使う上でバージョンを気にする必要は基本的にはないと考えられる。ただし、バージョン3系で新しく追加された機能も一部存在し、古いバージョンでは使えない。

元の記事は2008年4月と2010年4月に書かれたが、内容を大幅に見直してサンプルコードも含めて大部分を書き直している。

  1. 共通した引数指定
    1. 最初の引数
    2. 入出力関係の引数
  2. 高レベル処理
  3. 低レベル処理
  4. 使用例
    1. 高レベル処理の使用例
    2. 低レベル処理の使用例
    3. ファイルを入出力に用いる例
    4. subprocessを用いた自作ツールを含んだ記事

共通した引数指定

subprocessモジュール内の機能(関数やクラス)では様々な引数を受け取って動作に反映させるようになっているが、以下の高レベル処理と低レベル処理とで引数の指定形式の多くは共通している。

最初の引数

関数やオブジェクト作成時に最初に渡す引数は実行する外部プロセスのコマンド行となり

  • リストやタプルの要素として並べる形(例:['uname', '-a'])
  • シェルに渡す形と同じ形式のコマンド行文字列(例:'uname -a')

の2種類の形式が使用できるが、後者はキーワード引数shellTrueにする必要がある上、セキュリティの観点からは好ましくない(“;” や “$()” などのシェルの表現を用いて任意のコマンドが実行可能となる危険性がある)。使用の際にはshlex.quote()を用いるのがよいとされるが、Python 3.3以上が必要。

実行ファイル名は環境変数PATHを用いて探索され、os.environ['PATH']への代入によってスクリプト内で変更することもできる。もちろん、実行ファイルの場所を絶対パスを指定して実行することもできる。

入出力関係の引数

キーワード引数stdin,stdout,stderrはそれぞれ外部プロセスからみた標準入力,標準出力,標準エラー出力が指定でき、値と動作は以下となる。

動作
subprocess.PIPE子プロセスに対してパイプを作ってプロセス間で入出力を行う
subprocess.STDOUTstderrの値として指定することで標準エラー出力を標準出力と同じ出力先にする
Noneリダイレクトなしでそのまま入出力が行われる(既定値)

値にファイルオブジェクトやファイル記述子を指定することでファイルを入出力対象とすることができる(シェルのリダイレクションと同様)。

Python 3.3以上では、出力を捨てたり空の入力を与えたりするのに用いられる特殊な値subprocess.DEVNULLも指定できる。

高レベル処理

細かい制御が行えない代わりに、関数を呼び出すだけで外部プロセスが実行できるのでお手軽なものとなっている。目的によってはこれだけで事足りることも多い。

関数処理
call()外部プロセスを実行して終了ステータスを返すが特別な動作はない
check_call()終了ステータスが0以外の場合に例外subprocess.CalledProcessErrorを発生
check_output()子プロセスの出力内容が戻り値になる
run()戻り値のメンバから終了ステータスや出力が得られるなど高機能だがPython 3.5以上が必要

子プロセスの出力はPython 3ではバイト型で得られるため、文字列として扱う場合は文字列型にする必要がある。

低レベル処理

  • subprocess.Popenクラスのオブジェクトを作成すると外部プロセスが実行される
  • このオブジェクトのstdin,stdout,stderrメンバを用いることでファイルオブジェクトに近い扱い方で入出力が行える
  • メンバ関数wait()を呼ぶことで終了を待機し、プロセスの終了時に終了ステータスの値が得られる

使用例

高レベル処理の使用例

[任意]ファイル名:subprocesstest-highlevel.py
#! /usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

import subprocess


# subprocessでは実行ファイルの探索には
# 環境変数PATH(os.environ['PATH'])が使用される
#
# call()は外部プロセスを実行して戻り値を得る
# 最初の引数が外部プロセスのコマンド行となり
# リストやタプルの場合は直接実行
# 文字列指定かつ "shell = True" 指定の場合はシェル経由で実行
# (シェルの仕組み上、意図しないコマンドが実行可能になる恐れがあるため非推奨)
print ('''>>> ret = subprocess.call ('uname -o', shell = True)''')
ret = subprocess.call ('uname -o', shell = True)
print ('ret={0}'.format (ret))
print ('-' * 80)

print ('''>>> ret = subprocess.call (('uname', '-p'))''')
ret = subprocess.call (('uname', '-p'))
print ('ret={0}'.format (ret))
print ('-' * 80)

# check_call()はプロセス終了時の戻り値が0以外だと例外を発生させる
try:
  ret = subprocess.check_call (('uname', '-4'))
  print ('ret={0}'.format (ret))
  print ('-' * 80)
except subprocess.CalledProcessError as e:
  print ('subprocess.CalledProcessError: {0}'.format (e))
  print ('-' * 80)

# check_output()は戻り値がプロセスからの出力になる
# 標準エラー出力も同時に含める場合は "stderr = subprocess.STDOUT" を指定
print ('''>>> output = subprocess.check_output (('uname', '-p'))''')
output = subprocess.check_output (('uname', '-p'))
print (output)

# Python 3.5以上では高機能なrun()が使える
try:
  print ('''>>> p = subprocess.run (('uname', '-p'), stdout = subprocess.PIPE, stderr = subprocess.PIPE)''')
  p = subprocess.run (('uname', '-p'), stdout = subprocess.PIPE, stderr = subprocess.PIPE)
  print ('p.args={0} p.returncode={1} p.stdout="{2}" p.stderr="{3}"'.format (p.args, p.returncode, p.stdout, p.stderr))
except AttributeError:
  print ('subprocess.run() is not supported')
実行例(Python 3)
>>> ret = subprocess.call ('uname -o', shell = True)
GNU/Linux
ret=0
--------------------------------------------------------------------------------
>>> ret = subprocess.call (('uname', '-p'))
x86_64
ret=0
--------------------------------------------------------------------------------
uname: 無効なオプション -- '4'
Try 'uname --help' for more information.
subprocess.CalledProcessError: Command '('uname', '-4')' returned non-zero exit status 1
--------------------------------------------------------------------------------
>>> output = subprocess.check_output (('uname', '-p'))
b'x86_64\n'
>>> p = subprocess.run (('uname', '-p'), stdout = subprocess.PIPE, stderr = subprocess.PIPE)
p.args=('uname', '-p') p.returncode=0 p.stdout="b'x86_64\n'" p.stderr="b''"

低レベル処理の使用例

[任意]ファイル名:subprocesstest-lowlevel.py
#! /usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

import subprocess


# "seq 5" を実行して出力をそのまま表示
print ('''>>> p = subprocess.Popen (('seq', '5'), stdout = subprocess.PIPE)''')
p = subprocess.Popen (('seq', '5'), stdout = subprocess.PIPE)
print ('''>>> for l in p.stdout:''')
print ('''>>>   print (l.strip ())''')
for l in p.stdout:
  print (l.strip ())
ret = p.wait ()
print ('ret={0}'.format (ret))

print ('-' * 80)

# パイプを用いて "seq 10 | grep 1" と同等の処理を行う
print ('''>>> p1 = subprocess.Popen (('seq', '10'), stdout = subprocess.PIPE)''')
print ('''>>> p2 = subprocess.Popen (('grep', '1'), stdin = p1.stdout, stdout = subprocess.PIPE)''')
print ('''>>> p1.stdout.close ()''')
p1 = subprocess.Popen (('seq', '10'), stdout = subprocess.PIPE)
p2 = subprocess.Popen (('grep', '1'), stdin = p1.stdout, stdout = subprocess.PIPE)
p1.stdout.close ()
print ('''>>> for l in p2.stdout:''')
print ('''>>>   print (l.strip ())''')
for l in p2.stdout:
  print (l.strip ())
ret = p1.wait ()
print ('ret(p1)={0}'.format (ret))
ret = p2.wait ()
print ('ret(p2)={0}'.format (ret))

print ('-' * 80)

# "echo abc123 | base64" と同等の処理を行う
# 直接外部プロセスの標準入力に書き込む
p = subprocess.Popen (('base64',), stdin = subprocess.PIPE, stdout = subprocess.PIPE)
p.stdin.write ('abc123\n'.encode ('ascii'))
p.stdin.close ()
for l in p.stdout:
  print (l.strip ())
ret = p.wait ()
print ('ret={0}'.format (ret))
実行例(Python 3)
>>> p = subprocess.Popen (('seq', '5'), stdout = subprocess.PIPE)
>>> for l in p.stdout:
>>>   print (l.strip ())
b'1'
b'2'
b'3'
b'4'
b'5'
ret=0
--------------------------------------------------------------------------------
>>> p1 = subprocess.Popen (('seq', '10'), stdout = subprocess.PIPE)
>>> p2 = subprocess.Popen (('grep', '1'), stdin = p1.stdout, stdout = subprocess.PIPE)
>>> p1.stdout.close ()
>>> for l in p2.stdout:
>>>   print (l.strip ())
b'1'
b'10'
ret(p1)=0
ret(p2)=0
--------------------------------------------------------------------------------
b'YWJjMTIzCg=='
ret=0

ファイルを入出力に用いる例

[任意]ファイル名:subprocesstest-redirection.py
#! /usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

import subprocess
import sys
import os


# "seq -f "https://www.example.com/images/%03g.jpg" 12" の結果をファイルに出力
urllist_file = 'subprocesstest-urllist.txt'
try:
  with open (urllist_file, 'w') as f_out:
    subprocess.check_call (('seq', '-f', 'https://www.example.com/images/%03g.jpg', '12'), stdout = f_out)
except IOError as e:
  sys.exit ('Could not write to file "{0}": {1}'.format (urllist_file, e))
except subprocess.CalledProcessError as e:
  print ('seq failed: {0}'.format (e))

# 出力されたファイルを入力として "egrep -o '\w+.jpg'" を実行
try:
  with open (urllist_file, 'r') as f_in:
    subprocess.check_call (('egrep', '-o', '\w+.jpg'), stdin = f_in)
except IOError as e:
  sys.exit ('Could not read from file "{0}": {1}'.format (urllist_file, e))
except subprocess.CalledProcessError as e:
  print ('egrep failed: {0}'.format (e))

# ファイルを消す
os.unlink (urllist_file)
実行例
001.jpg
002.jpg
...
011.jpg
012.jpg

subprocessを用いた自作ツールを含んだ記事

使用したバージョン:
  • Python 2.7.12, 3.5.2