Pythonから外部コマンドを非同期に実行するにはどうすればよいですか?


120

Pythonスクリプトから非同期にシェルコマンドを実行する必要があります。これは、外部コマンドがオフになり、必要なことをすべて実行している間も、Pythonスクリプトを実行し続けたいことを意味します。

私はこの投稿を読みました:

Pythonで外部コマンドを呼び出す

それから私は外に出ていくつかのテストos.system()を行いました&が、コマンドの最後に使用するように提供されるジョブを実行するように見えるので、コマンドが戻るのを待つ必要はありません。これがそのようなことを達成するための適切な方法であるかどうか私は疑問に思っていますか?試してみましたcommands.call()が、外部コマンドでブロックされるため、うまくいきません。

os.system()これを使用することが望ましいか、他の方法を試すべきかどうかを教えてください。

回答:


135

subprocess.Popenはまさにあなたが望むことをします。

from subprocess import Popen
p = Popen(['watch', 'ls']) # something long running
# ... do other stuff while subprocess is running
p.terminate()

(コメントから回答を完了するために編集)

Popenインスタンスpoll()は、まだ実行されているかどうかを確認communicate()したり、stdinにデータを送信したり、終了するまで待機したりするなど、他のさまざまなことを実行できます。


4
また、poll()を使用して子プロセスが終了したかどうかを確認したり、wait()を使用して子プロセスが終了するのを待機したりすることもできます。
アダムローゼンフィールド

Adamは非常に真実ですが、in / outバッファーの処理が向上し、フラッディングによってブロックされる可能性があるため、communicate()を使用して待機する方がよい場合があります。
Ali Afshar

Adam:docsは、「警告は、子プロセスがstdoutまたはstderrパイプに十分な出力を生成し、OSパイプバッファーがより多くのデータを受け入れるのを待機するのをブロックする場合、デッドロックになります。それを回避するには、communicate()を使用します。」
Ali Afshar

14
ただし、communication()およびwait()は操作をブロックしています。OPを使用するかどうか尋ねられるように、コマンドを並列化することはできません。
cdleary 2009年

1
Cdlearyは完全に正しいです。通信してブロックを待機することを明記する必要があります。したがって、シャットダウンするのを待っているときにのみブロックを実行してください。(あなたが善行をするために本当にすべきこと)
Ali Afshar

48

多くのプロセスを並行して実行し、結果が得られたときに処理する場合は、次のようにポーリングを使用できます。

from subprocess import Popen, PIPE
import time

running_procs = [
    Popen(['/usr/bin/my_cmd', '-i %s' % path], stdout=PIPE, stderr=PIPE)
    for path in '/tmp/file0 /tmp/file1 /tmp/file2'.split()]

while running_procs:
    for proc in running_procs:
        retcode = proc.poll()
        if retcode is not None: # Process finished.
            running_procs.remove(proc)
            break
        else: # No process is done, wait a bit and check again.
            time.sleep(.1)
            continue

    # Here, `proc` has finished with return code `retcode`
    if retcode != 0:
        """Error handling."""
    handle_results(proc.stdout)

制御フローは少し小さくしたいので少し複雑ですが、好みに合わせてリファクタリングできます。:-)

これには、早期に終了するリクエストを最初に処理するという利点があります。communicate最初に実行中のプロセスを呼び出すと、それが最も長く実行されることが判明した場合、他の実行中のプロセスは、結果を処理している可能性があるときにアイドル状態になっています。


3
@Tinoビジー待機の定義方法によって異なります。ビジー待機とポーリングの違いは何ですか?を
Piotr Dobrogost 2012

1
1つのプロセスだけでなく、一連のプロセスをポーリングする方法はありますか?
Piotr Dobrogost 2012

1
注:プロセスが十分な出力を生成すると、ハングする可能性があります。PIPEを使用する場合は、同時にstdoutを使用する必要があります(サブプロセスのドキュメントに警告が(多すぎますが十分ではありません)あります)。
jfs

@PiotrDobrogost:os.waitpid直接使用して子プロセスのステータスが変更されたかどうかを確認できます。
jfs

5
['/usr/bin/my_cmd', '-i', path]代わりに使用['/usr/bin/my_cmd', '-i %s' % path]
jfs

11

これ[os.system()]がそのようなことを達成するための適切な方法であるかどうか私は疑問に思っています

いいえ os.system()、適切な方法ではありません。それが、誰もがを使うように言う理由ですsubprocess

詳細については、http://docs.python.org/library/os.html#os.systemを参照してください。

サブプロセスモジュールは、新しいプロセスを生成してその結果を取得するためのより強力な機能を提供します。この関数を使用するよりも、そのモジュールを使用する方が適切です。サブプロセスモジュールを使用します。特に、古い関数をサブプロセスモジュールセクションで置き換えるセクションを確認してください。


8

私は、プロセスからの出力を適切に処理するasyncprocモジュールで成功しました。例えば:

import os
from asynproc import Process
myProc = Process("myprogram.app")

while True:
    # check to see if process has ended
    poll = myProc.wait(os.WNOHANG)
    if poll is not None:
        break
    # print any new output
    out = myProc.read()
    if out != "":
        print out

これはgithubのどこにありますか?
Nick

これはgplライセンスなので、きっとそこにあるはずです。こちらが1つです:github.com/albertz/helpers/blob/master/asyncproc.py
Noah

python3で動作するようにいくつかの変更を加えた要点を追加しました。(主にstrをバイトに置き換えます)。gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic

1
また、ループを出た後、もう一度出力を読み取る必要があります。そうしないと、出力の一部が失われます。
Tic

7

これを行うもう1つの方法は、非ブロックreadlineでpexpectを使用することです。Pexpectはデッドロックの問題を解決し、バックグラウンドでプロセスを簡単に実行できるようにし、プロセスが事前定義された文字列を吐き出すときにコールバックを簡単に提供し、一般にプロセスとのやり取りをはるかに簡単にします。


4

「戻るのを待つ必要がない」と考えると、最も簡単な解決策の1つは次のとおりです。

subprocess.Popen( \
    [path_to_executable, arg1, arg2, ... argN],
    creationflags = subprocess.CREATE_NEW_CONSOLE,
).pid

しかし...私が読んだことから、これは「そのようなことを達成する適切な方法」ではありません。これは、subprocess.CREATE_NEW_CONSOLEフラグによって作成されるセキュリティリスクのためです。

ここで発生する主なことは、を使用subprocess.CREATE_NEW_CONSOLEして新しいコンソールを作成し、.pid(プロセスIDを返して、必要に応じて後でプログラムをチェックできるようにする)プログラムがジョブを完了するのを待たないようにすることです。


3

Pythonのs3270スクリプトソフトウェアを使用して3270端末に接続しようとすると、同じ問題が発生します。ここで私はここで見つけたプロセスのサブクラスで問題を解決しています:

http://code.activestate.com/recipes/440554/

そして、これはファイルから取られたサンプルです:

def recv_some(p, t=.1, e=1, tr=5, stderr=0):
    if tr < 1:
        tr = 1
    x = time.time()+t
    y = []
    r = ''
    pr = p.recv
    if stderr:
        pr = p.recv_err
    while time.time() < x or r:
        r = pr()
        if r is None:
            if e:
                raise Exception(message)
            else:
                break
        elif r:
            y.append(r)
        else:
            time.sleep(max((x-time.time())/tr, 0))
    return ''.join(y)

def send_all(p, data):
    while len(data):
        sent = p.send(data)
        if sent is None:
            raise Exception(message)
        data = buffer(data, sent)

if __name__ == '__main__':
    if sys.platform == 'win32':
        shell, commands, tail = ('cmd', ('dir /w', 'echo HELLO WORLD'), '\r\n')
    else:
        shell, commands, tail = ('sh', ('ls', 'echo HELLO WORLD'), '\n')

    a = Popen(shell, stdin=PIPE, stdout=PIPE)
    print recv_some(a),
    for cmd in commands:
        send_all(a, cmd + tail)
        print recv_some(a),
    send_all(a, 'exit' + tail)
    print recv_some(a, e=0)
    a.wait()

3

受け入れられた回答は非常に古いものです。

私はここでより良い現代的な答えを見つけました:

https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/

そしていくつかの変更を加えました:

  1. Windowsで機能させる
  2. 複数のコマンドで機能させる
import sys
import asyncio

if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())


async def _read_stream(stream, cb):
    while True:
        line = await stream.readline()
        if line:
            cb(line)
        else:
            break


async def _stream_subprocess(cmd, stdout_cb, stderr_cb):
    try:
        process = await asyncio.create_subprocess_exec(
            *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )

        await asyncio.wait(
            [
                _read_stream(process.stdout, stdout_cb),
                _read_stream(process.stderr, stderr_cb),
            ]
        )
        rc = await process.wait()
        return process.pid, rc
    except OSError as e:
        # the program will hang if we let any exception propagate
        return e


def execute(*aws):
    """ run the given coroutines in an asyncio loop
    returns a list containing the values returned from each coroutine.
    """
    loop = asyncio.get_event_loop()
    rc = loop.run_until_complete(asyncio.gather(*aws))
    loop.close()
    return rc


def printer(label):
    def pr(*args, **kw):
        print(label, *args, **kw)

    return pr


def name_it(start=0, template="s{}"):
    """a simple generator for task names
    """
    while True:
        yield template.format(start)
        start += 1


def runners(cmds):
    """
    cmds is a list of commands to excecute as subprocesses
    each item is a list appropriate for use by subprocess.call
    """
    next_name = name_it().__next__
    for cmd in cmds:
        name = next_name()
        out = printer(f"{name}.stdout")
        err = printer(f"{name}.stderr")
        yield _stream_subprocess(cmd, out, err)


if __name__ == "__main__":
    cmds = (
        [
            "sh",
            "-c",
            """echo "$SHELL"-stdout && sleep 1 && echo stderr 1>&2 && sleep 1 && echo done""",
        ],
        [
            "bash",
            "-c",
            "echo 'hello, Dave.' && sleep 1 && echo dave_err 1>&2 && sleep 1 && echo done",
        ],
        [sys.executable, "-c", 'print("hello from python");import sys;sys.exit(2)'],
    )

    print(execute(*runners(cmds)))

例のコマンドがシステムで完全に機能する可能性は低く、奇妙なエラーは処理されませんが、このコードは、asyncioを使用して複数のサブプロセスを実行し、出力をストリーミングする1つの方法を示しています。


私はこれをWindowsで実行しているcpython 3.7.4とUbuntu WSLとネイティブのアルパインLinuxで実行しているcpython 3.7.3でテストしました
Terrel Shumway


1

ここにはいくつかの答えがありますが、どれも私の以下の要件を満たしていません:

  1. コマンドが完了するのを待ったり、サブプロセス出力で端末を汚染したりしたくありません。

  2. リダイレクトを使用してbashスクリプトを実行したい。

  3. bashスクリプト内でのパイピングをサポートしたい(たとえばfind ... | tar ...)。

上記の要件を満たす唯一の組み合わせは次のとおりです。

subprocess.Popen(['./my_script.sh "arg1" > "redirect/path/to"'],
                 stdout=subprocess.PIPE, 
                 stderr=subprocess.PIPE,
                 shell=True)

0

これは、「コマンドが非同期で終了するのを待つ」のPython 3サブプロセスの例で説明されています。

import asyncio

proc = await asyncio.create_subprocess_exec(
    'ls','-lha',
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.PIPE)

# do something else while ls is working

# if proc takes very long to complete, the CPUs are free to use cycles for 
# other processes
stdout, stderr = await proc.communicate()

プロセスは、await asyncio.create_subprocess_exec(...)が完了するとすぐに実行を開始します。を呼び出すawait proc.communicate()までに完了していない場合は、出力ステータスを取得するためにそこで待機します。終了した場合は、proc.communicate()すぐに戻ります。

ここの要点はTerrelsの回答に似ていますが、Terrelsの回答は複雑すぎるように見えます。

詳細についてはasyncio.create_subprocess_exec、を参照してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.