Knowledge Base

お知らせや身辺のことを綴っています。
目次
subprocessのPopenを使ってみよう

subprocessのPopenを使ってみよう

Pythonのsubprocessは便利なモジュールです。subprocess.run()のように簡単に使えるものから、Popenのようなインターフェースを用いて、パイプを使って柔軟にコマンドを組み合わせて結果を取得したり、処理をその処理系に委任することもできます。そうした当モジュールですが、本記事では特にPopenについて、3つほどのありそうな使用例を例を交えて紹介してみようと思います。

検証環境

uname -srvmpio
# Linux 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 5 21:02:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
python3 --version
# Python 3.10.12

subprocess.run()の使い道は限られる

subprocess.run() は優秀で、簡単に子プロセスを立ち上げ、その実行結果を取得できます。たとえば、以下のコマンドは、dd にブロックデバイスの/dev/zero から10バイトを1回分コピーして来い!という命令です。実際、バイト数を数えることができる関数である、len() で確かめると、確かに10バイトのデータがコピーされていることが分かります。

>>> import subprocess
>>> process = subprocess.run(['dd', 'if=/dev/zero', 'bs=10', 'count=1'], capture_output=True)
>>> process
CompletedProcess(args=['dd', 'if=/dev/zero', 'bs=10', 'count=1'], returncode=0, stdout=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', stderr=b'1+0 records in\n1+0 records out\n10 bytes copied, 6.6194e-05 s, 151 kB/s\n')
>>> len(process.stdout)
10

subprocessでもパイプを使いたい!

しかし、subprocess.run()で対応できない場合もあります。たとえば、2つ以上のコマンドをパイプを使って組み合わせて、何か有意な結果を得たいという場合です。

パイプ

パイプは、2つの異なるプロセスがデータをやり取りするときに用いるデータの通り道であり、プロセス間通信(IPC)の一つです。パイプは、FIFO(First in First Out)と呼ばれるデータ構造に基づいていて、一種のキューです。Linuxを含め各種UNIX系のOSにおいては、無名パイプ(Anonymous Pipe)と名前付きパイプ(Named Pipe)というものがあり、前者は、シェルの | 、後者は、mkfifo コマンドによって作成できます。無名パイプについて、たとえば、wcdd が出力した1KBのデータをパイプを通じて送信したいときは、以下のようにします。

dd if=/dev/urandom bs=1k count=1 | wc
# 1+0 records in
# 1+0 records out
# 1024 bytes (1.0 kB, 1.0 KiB) copied, 6.6427e-05 s, 15.4 MB/s
#       4      19    1024

前述のとおり、ddzstd をつなぐとき、シェルでは、| という記号で表現されるパイプを使います。一方、Pythonの subprocess において、パイプは subprocess.PIPE によって表現されます。

そこで、Popenインターフェースの出番です。次の節では、本節ではPopenについてもう少し深く掘り下げます。

Popenを使う

Popenは、公式ドキュメントによれば、subprocess.run()といった簡易関数によってカバーされないような、あまり一般的でないケースを開発者が扱えるように、多くの柔軟性を提供するオブジェクトです。

本節ではまず最初に、ざっくりとその使い方に触れます。そして、前節の課題であった、2つ以上のプロセスをパイプでつなぎ、ddの出力をPython側でキャプチャする実演を行います。そして最後に、Pythonから直接プログラムにデータを送信する例についても触れていきます。

プロセスが一つのみ

それでは、プロセスが1つだけの場合における標準出力、および標準エラー出力のキャプチャ方法を見ていきましょう。簡単そうに見えますが、Popenを利用する場合は少し手順が増えます。

まずは、Popenクラスを、引数とstdout=PIPE, stderr=PIPEをともに指定してインスタンス化します。stdout, stderrを指定した理由は、プロセスの標準出力と標準エラー出力を捕捉し、Python内で利用できるようにするためです。次に、communicate()を呼び出し、プロセスを実行します。

>>> from subprocess import Popen, PIPE
>>> proc = Popen(['dd', 'if=/dev/zero', 'bs=10', 'count=1'], stdout=PIPE, stderr=PIPE)
>>> outs, errs = proc.communicate()

communicate() は、標準入力から、proc の標準出力(と標準エラー出力)を、End-of-fileに達するまで読み出したあと、proc が終了し、returncode属性を付けるのを確認するまで待ってから実行結果のタプルを返します。

これらは、それぞれouts, errsのようにして取り出せます。

>>> outs
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> errs
b'1+0 records in\n1+0 records out\n10 bytes copied, 1.7363e-05 s, 576 kB/s\n'

なお、Popenコンストラクタで、stdout、あるいは stderrを指定しない場合、これらのデフォルト値はNoneになります。Noneの場合、プロセスの出力は現在のシェルに出力されます。

>>> ƒproc = Popen(['dd', 'if=/dev/zero', 'bs=10', 'count=1'])
>>> 1+0 records in
1+0 records out
10 bytes copied, 8.1815e-05 s, 122 kB/s
   # Pythonにターミナル画面を荒らされました
>>>

標準エラー出力が不要な場合は/dev/nullに相当するDEVNULLを指定すると良いでしょう。

>>> from subprocess import Popen, PIPE, DEVNULL
>>> ƒƒproc = Popen(['dd', 'if=/dev/zero', 'bs=10', 'count=1'], stdout=PIPE, stderr=DEVNULL)
>>> ƒƒouts, errs = proc.communicate()
>>> ƒƒouts
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> ƒƒerrs
>>>

2つ以上のプロセス

それでは、先ほどのシェルコマンドdd if=/dev/urandom bs=1k count=1 | wcに相当する操作をPythonで行う方法を見ていきます。最終的には、このコマンドの標準出力と標準エラー出力を取得し、Python内で利用できることを目標にします。

まずは、2つのプロセスをPopenを使って生成します。1つ目のプロセスoneの標準出力は、そのままパイプに渡します。標準エラー出力は不要なため、DEVNULLを指定しています。

>>> from subprocess import Popen, PIPE, DEVNULL
>>> one = Popen(['dd', 'if=/dev/zero', 'bs=1', 'count=1'], stdout=PIPE, stderr=DEVNULL)

ここがミソなのですが、2つめのプロセスの標準入力は、一つ目のプロセスである one の標準出力に設定しておくようにします。あとは全てPIPEにつなぎます。

>>> two = Popen(['wc'], stdin=one.stdout, stdout=PIPE, stderr=PIPE)

最後に、twoに生えているcommunicate()を呼び出すことで、プロセス間の通信を開始します。このようにして、先ほどと同じように実行結果を確認できます。

>>> outs, errs = two.communicate(timeout=15)
>>> outs.decode()
'      0       0       1\n'
>>> errs.decode()
''

Pythonから直接プロセスに接続

もちろん、Python側で保持していたデータを別のプログラムにパイプを通じて流すこともできます。今回は、5行にわたって’Hello’とつぶやく文字列を、wcコマンドに渡して、その改行数を結果として捕捉する以下のプログラムを想定します。

本プログラムでは、1番目のプロセスの標準入力をパイプに設定しています。そして、communicate()の引数にはinput=を指定し、直接プロセスに文字列(.encode()しているのでバイト列です)を送信していることに注目しましょう。

>>> from subprocess import Popen, PIPE
>>> txt = "".join(["Hello\n" for _ in range(5)])
>>> wc = Popen(['wc', '-l'], stdin=PIPE, stdout=PIPE)
>>> outs, errs = wc.communicate(input=txt.encode(), timeout=15)
>>> outs.decode()
'5\n'

まとめ

本記事では、Pythonを含む2つ以上のアプリケーションでデータをやり取りさせたい場合に便利なPopenについて紹介しました。ユースケースはごく一部でしたが、役に立てば幸いです。

前の記事

ABC329 を Python で解く (AからD問題まで)

次の記事

技術者にとって代えがたき至高の検索ツールとしての DuckDuckGo: !Bangs のすすめ

コメント

0

コメントはありません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

関連投稿

プログラミング
「Pythonで対話的UIを実装したいときはreadlineモジュールを使おう」のサムネイル画像

Pythonで対話的UIを実装したいときはreadlineモジュールを使おう

Pythonのreadlineモジュールを使って、インラインで修正ができる対話的UIを実装します。 続きを読む

プログラミング
「ABC329 を Python で解く (AからD問題まで)」のサムネイル画像

ABC329 を Python で解く (AからD問題まで)

AtCoderで解いた問題の振り返りを行うための自分用のメモです。 続きを読む

プログラミング
「PHPでカリー化を使ってみたよ」のサムネイル画像

PHPでカリー化を使ってみたよ

PHPでカリー化を使って、2つの値を記号でつないでくれる関数を作ってみました。 続きを読む

プログラミング
「AtCoder Beginners Contest 303 を PHP で解く」のサムネイル画像

AtCoder Beginners Contest 303 を PHP で解く

AtCoderで解いた問題の振り返りを行うための自分用のメモです。 続きを読む