ฉันจะรันคำสั่งภายนอกแบบอะซิงโครนัสจาก 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 () เพื่อรอให้สิ้นสุด
Adam Rosenfield

อดัมจริงมากแม้ว่าจะดีกว่าถ้าใช้การสื่อสาร () เพื่อรอเพราะมีการจัดการบัฟเฟอร์เข้า / ออกได้ดีกว่าและมีสถานการณ์ที่น้ำท่วมเหล่านี้อาจปิดกั้น
Ali Afshar

Adam: docs พูดว่า "คำเตือนสิ่งนี้จะหยุดชะงักหากกระบวนการลูกสร้างเอาท์พุตเพียงพอไปยังท่อ stdout หรือ stderr ซึ่งจะบล็อกการรอให้บัฟเฟอร์ไปป์ OS ยอมรับข้อมูลเพิ่มเติมใช้การสื่อสาร () เพื่อหลีกเลี่ยงปัญหานั้น"
Ali Afshar

14
สื่อสาร () และ wait () กำลังปิดกั้นการดำเนินการแม้ว่า คุณจะไม่ใช้คำสั่งแบบขนานเหมือนที่ OP ดูเหมือนจะถามว่าคุณใช้หรือไม่
cdleary

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

1
มีวิธีใดบ้างในการสำรวจชุดของกระบวนการที่ไม่ใช่เพียงกระบวนการเดียว?
Piotr Dobrogost

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
Noah

ฉันเพิ่มส่วนสำคัญด้วยการปรับเปลี่ยนบางอย่างเพื่อให้ใช้งานได้กับ python3 (ส่วนใหญ่แทนที่ str ด้วยไบต์) ดูgist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic

1
นอกจากนี้คุณต้องอ่านผลลัพธ์อีกครั้งหลังจากออกจากลูปมิฉะนั้นคุณจะสูญเสียเอาต์พุตบางส่วน
Tic

7

การใช้pexpectกับ readlines ที่ไม่ปิดกั้นเป็นอีกวิธีหนึ่งในการดำเนินการนี้ 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(ส่งคืน ID กระบวนการเพื่อให้คุณสามารถตรวจสอบโปรแกรมในภายหลังได้หากต้องการ) เพื่อไม่ให้รอให้โปรแกรมทำงานให้เสร็จ


3

ฉันมีปัญหาเดียวกันกับการพยายามเชื่อมต่อกับเทอร์มินัล 3270 โดยใช้ซอฟต์แวร์สคริปต์ s3270 ใน Python ตอนนี้ฉันกำลังแก้ปัญหาด้วยคลาสย่อยของกระบวนการที่ฉันพบที่นี่:

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 และสตรีมเอาต์พุต


ฉันทดสอบสิ่งนี้บน cpython 3.7.4 ที่ทำงานบน windows และ cpython 3.7.3 ที่ทำงานบน Ubuntu WSL และ Alpine Linux ดั้งเดิม
Terrel Shumway


1

มีคำตอบหลายประการที่นี่ แต่ไม่มีคำตอบใดตรงตามข้อกำหนดด้านล่างของฉัน:

  1. ฉันไม่ต้องการรอให้คำสั่งเสร็จสิ้นหรือสร้างมลพิษให้กับเทอร์มินัลของฉันด้วยเอาต์พุตของกระบวนการย่อย

  2. ฉันต้องการเรียกใช้สคริปต์ทุบตีด้วยการเปลี่ยนเส้นทาง

  3. ฉันต้องการสนับสนุนการวางท่อภายในสคริปต์ทุบตีของฉัน (ตัวอย่าง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.