รับเอาต์พุตแบบเรียลไทม์โดยใช้กระบวนการย่อย


138

ฉันกำลังพยายามเขียนสคริปต์ wrapper สำหรับโปรแกรมบรรทัดคำสั่ง (ตรวจสอบ svnadmin) ซึ่งจะแสดงตัวบ่งชี้ความคืบหน้าที่ดีสำหรับการดำเนินการ สิ่งนี้ต้องการให้ฉันสามารถเห็นแต่ละบรรทัดของเอาต์พุตจากโปรแกรมที่รวมไว้ได้ทันทีที่เอาต์พุต

ฉันคิดว่าฉันจะดำเนินการโปรแกรมโดยsubprocess.Popenใช้ใช้stdout=PIPEแล้วอ่านแต่ละบรรทัดเมื่อเข้ามาและดำเนินการตามนั้น อย่างไรก็ตามเมื่อฉันรันโค้ดต่อไปนี้ผลลัพธ์ดูเหมือนจะถูกบัฟเฟอร์ที่ใดที่หนึ่งทำให้ปรากฏเป็นสองส่วนบรรทัดที่ 1 ถึง 332 จากนั้น 333 ถึง 439 (บรรทัดสุดท้ายของเอาต์พุต)

from subprocess import Popen, PIPE, STDOUT

p = Popen('svnadmin verify /var/svn/repos/config', stdout = PIPE, 
        stderr = STDOUT, shell = True)
for line in p.stdout:
    print line.replace('\n', '')

หลังจากดูเอกสารเกี่ยวกับกระบวนการย่อยเล็กน้อยฉันค้นพบbufsizeพารามิเตอร์เป็นPopenดังนั้นฉันจึงลองตั้งค่า bufsize เป็น 1 (บัฟเฟอร์แต่ละบรรทัด) และ 0 (ไม่มีบัฟเฟอร์) แต่ค่าทั้งสองดูเหมือนจะไม่เปลี่ยนวิธีการส่งบรรทัด

ณ จุดนี้ฉันเริ่มเข้าใจฟางดังนั้นฉันจึงเขียนลูปเอาต์พุตต่อไปนี้:

while True:
    try:
        print p.stdout.next().replace('\n', '')
    except StopIteration:
        break

แต่ได้ผลลัพธ์เดียวกัน

เป็นไปได้ไหมที่จะได้รับเอาต์พุตโปรแกรม 'เรียลไทม์' ของโปรแกรมที่เรียกใช้งานโดยใช้กระบวนการย่อย มีตัวเลือกอื่นใน Python ที่รองรับการส่งต่อ (ไม่ใช่exec*) หรือไม่?


1
คุณได้ลองละเว้นsydout=PIPEกระบวนการย่อยเพื่อให้กระบวนการย่อยเขียนลงในคอนโซลของคุณโดยตรงโดยข้ามกระบวนการหลักหรือไม่?
ล็อต

5
สิ่งนี้คือฉันต้องการอ่านผลลัพธ์ หากส่งออกไปยังคอนโซลโดยตรงฉันจะทำอย่างไร นอกจากนี้ฉันไม่ต้องการให้ผู้ใช้เห็นผลลัพธ์จากโปรแกรมที่ห่อไว้เพียงแค่ผลลัพธ์ของฉัน
Chris Lieb

แล้วทำไมต้องแสดงผลแบบ "เรียลไทม์"? ฉันไม่ได้รับกรณีการใช้งาน
ล็อต

8
อย่าใช้ shell = True มันไม่จำเป็นต้องเรียกใช้เปลือกของคุณ ใช้ p = Popen (['svnadmin', 'verification', '/ var / svn / repos / config'], stdout = PIPE, stderr = STDOUT) แทน
nosklo

2
@ S.Lott โดยทั่วไป svnadmin ตรวจสอบจะพิมพ์บรรทัดของเอาต์พุตสำหรับทุกการแก้ไขที่ตรวจสอบแล้ว ฉันต้องการสร้างตัวบ่งชี้ความคืบหน้าที่ดีที่จะไม่ทำให้เกิดผลลัพธ์มากเกินไป ประเภทที่เหมือน wget เช่น
Chris Lieb

คำตอบ:


82

ฉันลองสิ่งนี้และด้วยเหตุผลบางอย่างในขณะที่รหัส

for line in p.stdout:
  ...

บัฟเฟอร์อย่างก้าวร้าวตัวแปร

while True:
  line = p.stdout.readline()
  if not line: break
  ...

ไม่. เห็นได้ชัดว่านี่เป็นข้อบกพร่องที่ทราบ: http://bugs.python.org/issue3907 (ขณะนี้ "ปิด" แล้วเมื่อวันที่ 29 ส.ค. 2018)


นี่ไม่ใช่ความยุ่งเหยิงเพียงอย่างเดียวในการใช้งาน Python IO แบบเก่า นี่คือเหตุผลที่ Py2.6 และ Py3k ลงเอยด้วยไลบรารี IO ใหม่ทั้งหมด
Tim Lin

3
รหัสนี้จะแตกหากกระบวนการย่อยส่งคืนบรรทัดว่าง ทางออกที่ดีกว่าคือใช้while p.poll() is Noneแทนwhile Trueและลบif not line
exhuma

7
@exhuma: มันใช้งานได้ดี readline ส่งคืน "\ n" ในบรรทัดว่างซึ่งไม่ได้ประเมินว่าเป็นจริง จะส่งคืนสตริงว่างเมื่อไปป์ปิดซึ่งจะเป็นเมื่อกระบวนการย่อยสิ้นสุดลง
Alice Purcell

1
@Dave สำหรับการอ้างอิงในอนาคต: พิมพ์ utf-8 บรรทัดใน py2 + ด้วยprint(line.decode('utf-8').rstrip()).
Jonathan Komar

3
นอกจากนี้สำหรับการอ่านผลลัพธ์ของกระบวนการแบบเรียลไทม์คุณจะต้องบอก python ว่าคุณไม่ต้องการบัฟเฟอร์ใด ๆ เรียน Python เพียงแค่ให้ผลลัพธ์โดยตรง และนี่คือวิธีการที่: PYTHONUNBUFFERED=1คุณจำเป็นต้องตั้งค่าตัวแปรสภาพแวดล้อม สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับเอาต์พุตที่ไม่มีที่สิ้นสุด
George Pligoropoulos

39
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()

1
@nbro อาจเป็นเพราะp.stdout.close()ไม่มีความชัดเจน
anatoly techtonik

2
@nbro อาจเป็นเพราะได้รับรหัสโดยไม่มีคำอธิบาย ... : /
แอรอนฮอล

4
b '' เกี่ยวกับอะไร?
ManuelSchneid3r

@ ManuelSchneid3r iter(<callable>, <string>)สร้างซ้ำได้โดยใช้แต่ละเอาต์พุตของ <callable> จนกว่าจะส่งคืน <string> (เรียกว่าsentinel) หากคุณพยายามเรียกใช้p.stdout.readlineหลาย ๆ ครั้งคุณจะเห็นว่าเมื่อไม่มีสิ่งอื่นใดที่จะพิมพ์มันจะพิมพ์ออกb''มาและนี่คือยามที่เหมาะสมที่จะใช้ในกรณีนี้
สบู่

30

คุณสามารถกำหนดเอาต์พุตของกระบวนการย่อยไปยังสตรีมได้โดยตรง ตัวอย่างง่าย:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)

สิ่งนี้ช่วยให้คุณได้รับเนื้อหาหลังจากข้อเท็จจริง.communicate()หรือไม่? หรือเนื้อหาสูญหายไปยังสตรีม stderr / stdout หลักหรือไม่
theferrit32

Nope ไม่มีวิธีการในการส่งคืนcommunicate() CompletedProcessนอกจากนี้capture_outputเป็นพิเศษร่วมกันด้วยและstdout stderr
Aidan Feldman

นี่ไม่ใช่ "เรียลไทม์" ซึ่งเป็นประเด็นทั้งหมดของคำถามนี้ สิ่งนี้จะรอจนกว่าจะlsทำงานเสร็จและไม่ได้ให้คุณเข้าถึงเอาต์พุต (นอกจากนี้อาร์กิวเมนต์stdoutและstderrคีย์เวิร์ดยังไม่จำเป็น - คุณแค่ระบุค่าเริ่มต้นอย่างชัดเจน)
tripleee

20

คุณสามารถลองสิ่งนี้:

import subprocess
import sys

process = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

while True:
    out = process.stdout.read(1)
    if out == '' and process.poll() != None:
        break
    if out != '':
        sys.stdout.write(out)
        sys.stdout.flush()

หากคุณใช้ readline แทนการอ่านจะมีบางกรณีที่ไม่ได้พิมพ์ข้อความเข้า ลองใช้คำสั่งที่ต้องมีการป้อนข้อมูลแบบอินไลน์และดูด้วยตัวคุณเอง


ใช่การใช้ readline () จะหยุดพิมพ์ (แม้จะเรียก sys.stdout.flush ())
Mark Ma

3
นี่ควรจะแขวนไปเรื่อย ๆ หรือไม่? ฉันต้องการให้โซลูชันที่กำหนดรวมรหัสสำเร็จรูปสำหรับแก้ไขลูปเมื่อกระบวนการย่อยเริ่มต้นเสร็จสิ้น ขออภัยไม่ว่าฉันจะดูกี่ครั้งก็ตามกระบวนการย่อย ฯลฯ เป็นสิ่งที่ฉันไม่สามารถทำงานได้
ThorSummoner

1
ทำไมต้องทดสอบ '' เมื่ออยู่ใน Python เราสามารถใช้ได้ถ้าไม่ออก
Greg Bell

2
นี่คือทางออกที่ดีที่สุดสำหรับงานระยะยาว แต่ควรใช้ไม่ใช่ไม่มีและไม่ใช่! = ไม่มี คุณไม่ควรใช้! = กับไม่มี
Cari

stderr แสดงด้วยหรือไม่
Pieter Vogelaar

7

ขั้นตอนย่อยสตรีม stdin และ stdout ด้วย asyncio ในบล็อกPythonโพสต์โดยKevin McCarthyแสดงวิธีดำเนินการกับ asyncio:

import asyncio
from asyncio.subprocess import PIPE
from asyncio import create_subprocess_exec


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


async def run(command):
    process = await create_subprocess_exec(
        *command, stdout=PIPE, stderr=PIPE
    )

    await asyncio.wait(
        [
            _read_stream(
                process.stdout,
                lambda x: print(
                    "STDOUT: {}".format(x.decode("UTF8"))
                ),
            ),
            _read_stream(
                process.stderr,
                lambda x: print(
                    "STDERR: {}".format(x.decode("UTF8"))
                ),
            ),
        ]
    )

    await process.wait()


async def main():
    await run("docker build -t my-docker-image:latest .")


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

สวัสดี @Jeef คุณสามารถชี้ให้เห็นการแก้ไขเพื่อให้ฉันสามารถอัปเดตคำตอบได้หรือไม่
Pablo

1
สวัสดีนั่นใช้ได้ผลสำหรับฉัน แต่ฉันต้องเพิ่มสิ่งต่อไปนี้เพื่อกำจัดข้อความแสดงข้อผิดพลาด: import nest_asyncio; nest_asyncio.apply()และใช้คำสั่งเชลล์เช่นprocess = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)แทนที่จะเป็นprocess = await create_subprocess_exec(...). ไชโย!
user319436

5

ใน Python 3.x กระบวนการอาจหยุดทำงานเนื่องจากเอาต์พุตเป็นอาร์เรย์ไบต์แทนที่จะเป็นสตริง ตรวจสอบให้แน่ใจว่าคุณได้ถอดรหัสเป็นสตริง

เริ่มต้นจากงูหลาม 3.6 คุณสามารถทำได้โดยใช้พารามิเตอร์encodingในPopen สร้าง ตัวอย่างที่สมบูรณ์:

process = subprocess.Popen(
    'my_command',
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    shell=True,
    encoding='utf-8',
    errors='replace'
)

while True:
    realtime_output = process.stdout.readline()

    if realtime_output == '' and process.poll() is not None:
        break

    if realtime_output:
        print(realtime_output.strip(), flush=True)

หมายเหตุว่ารหัสนี้เปลี่ยนเส้นทาง stderrไปstdoutและจับข้อผิดพลาดของการส่งออก


1
คำตอบนี้เท่านั้นที่ใช้งานได้!
РоманСергеевич

4

แก้ไขปัญหาเอาต์พุตแบบเรียลไทม์: ฉันพบปัญหาที่คล้ายกันใน Python ในขณะที่จับเอาต์พุตเรียลไทม์จากโปรแกรม C ฉันเพิ่มfflush(stdout);รหัส C ของฉัน มันได้ผลสำหรับฉัน นี่คือรหัส

โปรแกรม C:

#include <stdio.h>
void main()
{
    int count = 1;
    while (1)
    {
        printf(" Count  %d\n", count++);
        fflush(stdout);
        sleep(1);
    }
}

โปรแกรม Python:

#!/usr/bin/python

import os, sys
import subprocess


procExe = subprocess.Popen(".//count", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

while procExe.poll() is None:
    line = procExe.stdout.readline()
    print("Print:" + line)

เอาท์พุต:

Print: Count  1
Print: Count  2
Print: Count  3

1
นี่เป็นสิ่งเดียวที่ช่วยได้จริง ฉันใช้รหัสเดียวกัน ( flush(stdout)) ใน C ++ ขอบคุณ!
Gerhard Hagerer

ฉันประสบปัญหาเดียวกันกับสคริปต์ python ที่เรียกสคริปต์ python อื่นเป็นกระบวนการย่อย ในการพิมพ์กระบวนการย่อยจำเป็นต้องใช้ "flush" (พิมพ์ ("hello", flush = True) ใน python 3) นอกจากนี้ยังมีตัวอย่างอีกมากมายที่ยังมี (2020) python 2 นี่คือ python 3 ดังนั้น +1
smajtkst

3

ฉันพบปัญหาเดิมในขณะที่กลับมา วิธีแก้ปัญหาของฉันคือทิ้งการทำซ้ำสำหรับreadวิธีการซึ่งจะกลับมาทันทีแม้ว่ากระบวนการย่อยของคุณจะยังไม่เสร็จสิ้นก็ตาม


3

คุณอาจต้องการปิดการใช้งานบัฟเฟอร์ในกระบวนการย่อยเองทั้งนี้ขึ้นอยู่กับกรณีการใช้งาน

หากกระบวนการย่อยเป็นกระบวนการ Python คุณสามารถทำได้ก่อนการเรียก:

os.environ["PYTHONUNBUFFERED"] = "1"

หรือมิฉะนั้นผ่านนี้ในอาร์กิวเมนต์envPopen

มิฉะนั้นหากคุณใช้ Linux / Unix คุณสามารถใช้stdbufเครื่องมือนี้ได้ เช่น:

cmd = ["stdbuf", "-oL"] + cmd

ดูที่นี่เกี่ยวกับstdbufหรือตัวเลือกอื่น ๆ

(ดูเพิ่มเติมที่นี่สำหรับคำตอบเดียวกัน.)


2

ฉันใช้วิธีนี้เพื่อรับเอาต์พุตเรียลไทม์ในกระบวนการย่อย การวนซ้ำนี้จะหยุดลงทันทีที่กระบวนการเสร็จสิ้นโดยไม่จำเป็นต้องมีคำสั่งหยุดพักหรือการวนซ้ำที่เป็นไปได้

sub_process = subprocess.Popen(my_command, close_fds=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while sub_process.poll() is None:
    out = sub_process.stdout.read(1)
    sys.stdout.write(out)
    sys.stdout.flush()

5
เป็นไปได้ไหมว่าสิ่งนี้จะออกจากลูปโดยที่บัฟเฟอร์ stdout ว่างเปล่า
jayjay

ฉันได้มองหาคำตอบที่เหมาะสมมากมายซึ่งไม่ได้แขวนไว้เมื่อเสร็จสิ้น! ฉันพบว่านี่เป็นวิธีแก้ปัญหาโดยเพิ่มif out=='': breakafterout = sub_process...
Sos

2

พบนี้ "Plug-and-play" ฟังก์ชั่นที่นี่ ทำงานอย่างมีเสน่ห์!

import subprocess

def myrun(cmd):
    """from http://blog.kagesenshi.org/2008/02/teeing-python-subprocesspopen-output.html
    """
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = []
    while True:
        line = p.stdout.readline()
        stdout.append(line)
        print line,
        if line == '' and p.poll() != None:
            break
    return ''.join(stdout)

1
การเพิ่มเข้ามาstderr=subprocess.STDOUTช่วยได้มากในการจับข้อมูลสตรีมมิ่ง ฉันกำลังโหวตให้มัน
khan

1
เนื้อวัวหลักที่นี่ดูเหมือนจะมาจากคำตอบที่ยอมรับ
tripleee

2

คุณอาจใช้ตัววนซ้ำบนแต่ละไบต์ในเอาต์พุตของกระบวนการย่อย สิ่งนี้อนุญาตให้อัปเดตแบบอินไลน์ (บรรทัดที่ลงท้ายด้วย '\ r' เขียนทับบรรทัดเอาต์พุตก่อนหน้า) จากกระบวนการย่อย:

from subprocess import PIPE, Popen

command = ["my_command", "-my_arg"]

# Open pipe to subprocess
subprocess = Popen(command, stdout=PIPE, stderr=PIPE)


# read each byte of subprocess
while subprocess.poll() is None:
    for c in iter(lambda: subprocess.stdout.read(1) if subprocess.poll() is None else {}, b''):
        c = c.decode('ascii')
        sys.stdout.write(c)
sys.stdout.flush()

if subprocess.returncode != 0:
    raise Exception("The subprocess did not terminate correctly.")

1

โซลูชั่นที่สมบูรณ์:

import contextlib
import subprocess

# Unix, Windows and old Macintosh end-of-line
newlines = ['\n', '\r\n', '\r']
def unbuffered(proc, stream='stdout'):
    stream = getattr(proc, stream)
    with contextlib.closing(stream):
        while True:
            out = []
            last = stream.read(1)
            # Don't loop forever
            if last == '' and proc.poll() is not None:
                break
            while last not in newlines:
                # Don't loop forever
                if last == '' and proc.poll() is not None:
                    break
                out.append(last)
                last = stream.read(1)
            out = ''.join(out)
            yield out

def example():
    cmd = ['ls', '-l', '/']
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        # Make all end-of-lines '\n'
        universal_newlines=True,
    )
    for line in unbuffered(proc):
        print line

example()

1
เนื่องจากคุณกำลังใช้universal_newlines=TrueการPopen()โทรคุณอาจไม่จำเป็นต้องจัดการด้วยตัวเองนั่นคือจุดรวมของตัวเลือกนี้
martineau

1
ดูเหมือนซับซ้อนโดยไม่จำเป็น ไม่แก้ปัญหาการบัฟเฟอร์ ดูการเชื่อมโยงในคำตอบของฉัน
jfs

นี่เป็นวิธีเดียวที่ฉันจะได้รับเอาต์พุตความคืบหน้า rsync แบบเรียลไทม์ (- outbuf = L)! ขอบคุณ
Mohammadhzp

1

นี่คือโครงกระดูกพื้นฐานที่ฉันใช้สำหรับสิ่งนี้เสมอ ทำให้ง่ายต่อการใช้การหมดเวลาและสามารถจัดการกับกระบวนการแขวนที่หลีกเลี่ยงไม่ได้

import subprocess
import threading
import Queue

def t_read_stdout(process, queue):
    """Read from stdout"""

    for output in iter(process.stdout.readline, b''):
        queue.put(output)

    return

process = subprocess.Popen(['dir'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           bufsize=1,
                           cwd='C:\\',
                           shell=True)

queue = Queue.Queue()
t_stdout = threading.Thread(target=t_read_stdout, args=(process, queue))
t_stdout.daemon = True
t_stdout.start()

while process.poll() is None or not queue.empty():
    try:
        output = queue.get(timeout=.5)

    except Queue.Empty:
        continue

    if not output:
        continue

    print(output),

t_stdout.join()

1

การใช้pexpectกับการอ่านบรรทัดที่ไม่ปิดกั้นจะช่วยแก้ปัญหานี้ได้ เกิดจากข้อเท็จจริงที่ว่าไพพ์ถูกบัฟเฟอร์ดังนั้นเอาต์พุตของแอปของคุณจึงถูกบัฟเฟอร์โดยไพพ์ดังนั้นคุณจึงไม่สามารถเข้าถึงเอาต์พุตนั้นได้จนกว่าบัฟเฟอร์จะเต็มหรือกระบวนการนั้นตาย


0

(โซลูชันนี้ได้รับการทดสอบด้วย Python 2.7.15)
คุณเพียงแค่ต้อง sys.stdout.flush () หลังจากอ่าน / เขียนแต่ละบรรทัด:

while proc.poll() is None:
    line = proc.stdout.readline()
    sys.stdout.write(line)
    # or print(line.strip()), you still need to force the flush.
    sys.stdout.flush()

0

คำตอบไม่กี่คำที่แนะนำ python 3.x หรือ pthon 2.x โค้ดด้านล่างจะใช้ได้กับทั้งสองอย่าง

 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,)
    stdout = []
    while True:
        line = p.stdout.readline()
        if not isinstance(line, (str)):
            line = line.decode('utf-8')
        stdout.append(line)
        print (line)
        if (line == '' and p.poll() != None):
            break

0

หากคุณต้องการส่งต่อบันทึกไปยังคอนโซลแบบเรียลไทม์

โค้ดด้านล่างจะใช้ได้กับทั้งสองอย่าง

 p = subprocess.Popen(cmd,
                         shell=True,
                         cwd=work_dir,
                         bufsize=1,
                         stdin=subprocess.PIPE,
                         stderr=sys.stderr,
                         stdout=sys.stdout)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.