Як я можу запустити зовнішню команду асинхронно з 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
Ви також можете скористатися опитуванням (), щоб перевірити, чи завершився дочірній процес, або ж скористайтеся функцією wait (), щоб дочекатися його завершення.
Адам Розенфілд

Адам, дуже правда, хоча може бути краще використовувати за допомогою комунікації () очікування, оскільки це покращує обробку вхідних та вихідних буферів і є ситуації, коли затоплення може блокуватись.
Алі Афшар

Адам: Документи говорять: "Попередження. Це буде глухим, якщо дочірній процес генерує достатню кількість вихідних даних в трубу stdout або stderr, щоб заблокувати очікування буфера труб ОС для прийняття більше даних. Використовуйте зв'язок (), щоб уникнути цього"
Алі Афшар

14
communication () і wait () блокують операції. Ви не будете паралелізувати команди, як ОП, здається, запитує, чи використовуєте ви їх.
cdleary

1
Cdleary абсолютно коректний, слід зазначити, що спілкуватися і чекати блокування, тому робити це лише тоді, коли ви чекаєте, коли речі завершаться. (Що ви дійсно повинні зробити, щоб бути добре поводитися)
Алі Афшар

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 Це залежить від того, як ви визначаєте зайняте-чекайте. Див. Яка різниця між зайнятими на очікування та опитуваннями?
Пьотр Доброгост

1
Чи є спосіб опитування набору процесів не один?
Пьотр Доброгост

1
Примітка: воно може зависати, якщо процес генерує достатню кількість результатів. Ви повинні споживати stdout одночасно, якщо ви використовуєте PIPE (у документах підпроцесору про це є (занадто багато, але недостатньо) попереджень).
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?
Нік

Це ліцензія на gpl, тому я впевнений, що вона існує багато разів. Ось один: github.com/albertz/helpers/blob/master/asyncproc.py
Ной

Я додав зміст з деякими модифікаціями, щоб він працював з python3. (переважно замінює рядок байтами). Дивіться gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic

1
Крім того, вам потрібно прочитати вихід ще раз після виходу з циклу, інакше ви втратите частину результатів.
Тик

7

Використання pexpect з неблокуючими лініями читання - ще один спосіб зробити це. Pexpect вирішує проблеми з тупиком, дозволяє легко запускати процеси у фоновому режимі та дає прості способи мати зворотні виклики, коли ваш процес випльовує заздалегідь задані рядки, і взагалі значно спрощує взаємодію з процесом.


4

Зважаючи на те, що "мені не потрібно чекати, коли він повернеться", одним із найпростіших рішень буде таке:

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

Але ... З того, що я прочитав, це не "належний спосіб здійснити таку річ" через ризики безпеки, створені subprocess.CREATE_NEW_CONSOLEпрапором.

Ключові речі, які відбуваються тут, - це використання subprocess.CREATE_NEW_CONSOLEдля створення нової консолі та .pid(повертає ідентифікатор процесу, щоб ви могли пізніше перевірити програму, якщо хочете), щоб не чекати, коли програма закінчить свою роботу.


3

У мене є та ж проблема, що намагаюся підключитися до терміналу 3270 за допомогою програмного забезпечення сценаріїв s3270 в Python. Тепер я вирішую проблему з підкласом Process, який я знайшов тут:

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. змусити його працювати на вікнах
  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 та потокового виводу.


Я перевірив це на cpython 3.7.4 під керуванням Windows та cpython 3.7.3 на Ubuntu WSL та рідному альпійському Linux
Terrel Shumway


1

Тут є кілька відповідей, але жоден з них не задовольнив моїх нижче вимог:

  1. Я не хочу чекати, коли команда закінчить або забруднить мій термінал підпроцесорними виходами.

  2. Я хочу запустити bash script із переспрямуванням.

  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()повернеться негайно.

Суть тут схожа на відповідь Террелса але я думаю, що відповідь Террелса, здається, є надскладним.

Див. Для asyncio.create_subprocess_execотримання додаткової інформації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.