เอาต์พุตสดจากคำสั่ง subprocess


186

ฉันใช้สคริปต์หลามเป็นไดรเวอร์สำหรับรหัสไฮโดรไดนามิกส์ เมื่อถึงเวลาที่จะทำการจำลองผมใช้subprocess.Popenเพื่อเรียกใช้รหัสรวบรวมผลลัพธ์จาก stdout และ stderr ลงในsubprocess.PIPE--- จากนั้นฉันสามารถพิมพ์ (และบันทึกลงในแฟ้มบันทึก) ข้อมูลผลลัพธ์และตรวจสอบข้อผิดพลาดใด ๆ . ปัญหาคือฉันไม่รู้ว่าโค้ดกำลังดำเนินไปอย่างไร ถ้าฉันเรียกใช้โดยตรงจากบรรทัดคำสั่งมันให้ผลลัพธ์เกี่ยวกับการวนซ้ำของมันที่เวลากี่โมงครั้งต่อไปคืออะไรเป็นต้น

มีวิธีที่จะเก็บเอาท์พุททั้งสอง (สำหรับการบันทึกและการตรวจสอบข้อผิดพลาด) และยังผลิตถ่ายทอดสดสตรีมมิ่ง?

ส่วนที่เกี่ยวข้องของรหัสของฉัน:

ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
output, errors = ret_val.communicate()
log_file.write(output)
print output
if( ret_val.returncode ):
    print "RUN failed\n\n%s\n\n" % (errors)
    success = False

if( errors ): log_file.write("\n\n%s\n\n" % errors)

เดิมทีฉันกำลังทำการส่งrun_commandผ่านteeเพื่อให้สำเนาไปที่ไฟล์บันทึกโดยตรงและกระแสข้อมูลยังคงส่งออกไปยังเทอร์มินัลโดยตรง - แต่วิธีนี้ฉันไม่สามารถเก็บข้อผิดพลาดใด ๆ


แก้ไข:

วิธีแก้ปัญหาชั่วคราว:

ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
    log_file.flush()

จากนั้นในเทอร์มินัลอื่นรันtail -f log.txt(st log_file = 'log.txt')


1
บางทีคุณอาจจะสามารถใช้Popen.pollในขณะที่ก่อนหน้านี้คำถามกองมากเกิน
เปาโลอัลไมด้า

บางคำสั่งที่แสดงการบ่งชี้ความคืบหน้า (เช่นgit) ทำเฉพาะในกรณีที่เอาต์พุตของพวกเขาเป็น "อุปกรณ์ tty" (ทดสอบผ่าน libc isatty()) ในกรณีนี้คุณอาจต้องเปิดหลอก
torek

@torek tty (หลอก) คืออะไร?
DilithiumMatrix

2
อุปกรณ์บนระบบที่เหมือน Unix ที่อนุญาตให้กระบวนการหลอกว่าเป็นผู้ใช้ในพอร์ตอนุกรม นี่คือการทำงานของ ssh (ฝั่งเซิร์ฟเวอร์) ดูห้องสมุดหลาม Ptyและยังpexpect
torek

เรื่องแก้ปัญหาชั่วคราว: ไม่จำเป็นต้องเรียกผู้ใดflushและมีเป็นความจำเป็นที่จะอ่านจากท่อ stderr ถ้ากระบวนการย่อยผลิตเอาท์พุท stderr มาก ที่มีอยู่ไม่เพียงพอที่ห้องพักในด้านความคิดเห็นที่จะอธิบายนี้ ...
torek

คำตอบ:


169

คุณมีสองวิธีในการทำเช่นนี้โดยการสร้างตัววนซ้ำจากreadหรือreadlineฟังก์ชันและทำ:

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for c in iter(lambda: process.stdout.read(1), ''):  # replace '' with b'' for Python 3
        sys.stdout.write(c)
        f.write(c)

หรือ

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for line in iter(process.stdout.readline, ''):  # replace '' with b'' for Python 3
        sys.stdout.write(line)
        f.write(line)

หรือคุณสามารถสร้างreaderและwriterไฟล์ ส่งผ่านwriterไปยังPopenและอ่านจากreader

import io
import time
import subprocess
import sys

filename = 'test.log'
with io.open(filename, 'wb') as writer, io.open(filename, 'rb', 1) as reader:
    process = subprocess.Popen(command, stdout=writer)
    while process.poll() is None:
        sys.stdout.write(reader.read())
        time.sleep(0.5)
    # Read the remaining
    sys.stdout.write(reader.read())

วิธีนี้คุณจะมีข้อมูลที่เขียนในtest.logเช่นเดียวกับในเอาต์พุตมาตรฐาน

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


1
ฮึ :-) เขียนไฟล์อ่านจากมันและพักในลูป? นอกจากนี้ยังมีโอกาสที่กระบวนการจะสิ้นสุดก่อนที่คุณจะอ่านไฟล์เสร็จ
Guy Sirton

13
กับงูหลาม 3 คุณต้องiter(process.stdout.readline, b'')(เช่นแมวมองที่ผ่านมาเราเตอร์จะต้องสตริงไบนารีตั้งแต่b'' != ''.
จอห์นเมลเลอร์

3
สำหรับสตรีมไบนารีให้ทำสิ่งนี้:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane

6
การเพิ่มคำตอบของ @JohnMellor ใน Python 3 จำเป็นต้องมีการแก้ไขต่อไปนี้: process = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) for line in iter(process.stdout.readline, b'') sys.stdout.write(line.decode(sys.stdout.encoding))
bergercookie

4
แต่ผลผลิตไม่ได้อยู่หรือไม่ จากประสบการณ์ของฉันมันรอจนกระทั่งกระบวนการเสร็จสิ้นการดำเนินการแล้วพิมพ์ไปที่คอนโซล Link -> stackoverflow.com/questions/30026045/…
denis631

91

บทสรุปผู้บริหาร (หรือรุ่น "tl; dr"): มันง่ายเมื่อมีมากที่สุดsubprocess.PIPEมิฉะนั้นมันก็ยาก

อาจถึงเวลาอธิบายเล็กน้อยเกี่ยวกับsubprocess.Popenสิ่งที่เกิดขึ้น

(Caveat: นี่สำหรับ Python 2.x แม้ว่า 3.x จะคล้ายกันและฉันค่อนข้างคลุมเครือกับตัวแปร Windows ฉันเข้าใจ POSIX ดีกว่ามาก)

Popenฟังก์ชั่นความต้องการที่จะจัดการกับศูนย์ถึงสาม I / O ลำธารค่อนข้างพร้อมกัน เหล่านี้จะแสดงstdin, stdoutและstderrตามปกติ

คุณสามารถให้:

  • Noneระบุว่าคุณไม่ต้องการเปลี่ยนเส้นทาง มันจะสืบทอดสิ่งเหล่านี้ตามปกติแทน โปรดทราบว่าในระบบ POSIX อย่างน้อยนี่ไม่ได้หมายความว่ามันจะใช้ Python sys.stdoutเพียงแค่stdout จริงของ Python ; ดูการสาธิตที่ส่วนท้าย
  • intค่า นี่เป็นตัวอธิบายไฟล์ "raw" (ใน POSIX อย่างน้อย) (หมายเหตุด้านข้าง: PIPEและSTDOUTแท้จริงแล้วเป็นการintภายใน แต่เป็นคำอธิบายที่ "เป็นไปไม่ได้" -1 และ -2)
  • กระแส - จริง ๆ วัตถุใด ๆ ที่มีfilenoวิธีการ Popenจะค้นหา descriptor สำหรับสตรีมนั้นโดยใช้stream.fileno()จากนั้นดำเนินการตามintค่า
  • subprocess.PIPEระบุว่า Python ควรสร้างไพพ์
  • subprocess.STDOUT(สำหรับstderrเท่านั้น): stdoutบอกงูหลามที่จะใช้อธิบายเช่นเดียวกับ สิ่งนี้เหมาะสมเท่านั้นหากคุณให้ค่า (ไม่ใช่ - None) สำหรับstdoutและถึงตอนนั้นคุณจำเป็นต้องตั้งค่าstdout=subprocess.PIPEเท่านั้น (มิฉะนั้นคุณสามารถระบุอาร์กิวเมนต์เดียวกับที่คุณให้ไว้สำหรับstdoutเช่นPopen(..., stdout=stream, stderr=stream))

กรณีที่ง่ายที่สุด (ไม่มีท่อ)

หากคุณไม่เปลี่ยนเส้นทาง (ปล่อยให้ทั้งสามเป็นค่าเริ่มต้นNoneหรือระบุอย่างชัดเจนNone) คุณPipeสามารถทำได้อย่างง่ายดาย เพียงแค่ต้องหมุนกระบวนการย่อยและปล่อยให้มันทำงาน หรือหากคุณเปลี่ยนเส้นทางไปยังผู้ที่ไม่ใช่PIPE- intหรือสตรีมfileno()- มันยังง่ายเพราะระบบปฏิบัติการทำงานได้ทั้งหมด Python เพียงแค่ต้องการแยกโพรเซสซิงออกเชื่อมต่อ stdin, stdout และ / หรือ stderr กับ file descriptors ที่มีให้

กรณีที่ยังคงง่าย: หนึ่งท่อ

หากคุณเปลี่ยนเส้นทางสตรีมเพียงรายการเดียวPipeก็ยังมีสิ่งที่ค่อนข้างง่าย ลองเลือกสตรีมทีละรายการแล้วดู

สมมติว่าคุณต้องการจัดหาบางอย่างstdinแต่ปล่อยstdoutและstderrไปที่ไม่ได้เปลี่ยนเส้นทางหรือไปที่ตัวให้คำอธิบายไฟล์ ในฐานะที่เป็นกระบวนการหลักโปรแกรม Python ของคุณเพียงแค่ต้องใช้write()ในการส่งข้อมูลลงในไพพ์ คุณสามารถทำได้ด้วยตัวเองเช่น:

proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc

หรือคุณสามารถส่งผ่านข้อมูล stdin ไปยังproc.communicate()ซึ่งจะstdin.writeแสดงด้านบน ไม่มีการส่งออกกลับมาดังนั้นจึงcommunicate()มีงานจริงเพียงงานเดียวเท่านั้นมันปิดไปป์ไลน์สำหรับคุณ (ถ้าคุณไม่โทรproc.communicate()คุณต้องโทรproc.stdin.close()เพื่อปิดไพพ์เพื่อให้กระบวนการย่อยรู้ว่าไม่มีข้อมูลผ่านเข้ามาอีก)

สมมติว่าคุณต้องการที่จะจับstdoutแต่ออกไปstdinและstderrอยู่คนเดียว อีกครั้งง่าย: เพียงแค่โทรproc.stdout.read()(หรือเทียบเท่า) จนกว่าจะไม่มีเอาต์พุตอีกต่อไป เนื่องจากproc.stdout()เป็นสตรีม Python I / O ปกติคุณสามารถใช้โครงสร้างปกติทั้งหมดบนมันได้เช่น:

for line in proc.stdout:

หรืออีกครั้งคุณสามารถใช้proc.communicate()ซึ่งก็ทำread()เพื่อคุณ

หากคุณต้องการที่จะจับเท่านั้นก็ทำงานเช่นเดียวกับกับstderrstdout

มีอีกหนึ่งเคล็ดลับก่อนที่สิ่งต่าง ๆ จะยากขึ้น สมมติว่าคุณต้องการที่จะจับstdoutและยังจับstderrแต่ในไปป์เดียวกับ stdout:

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

ในกรณีนี้subprocess"กลโกง"! มันจะต้องทำสิ่งนี้ดังนั้นจึงไม่ใช่การโกงจริง ๆ : มันเริ่มต้นกระบวนการย่อยด้วย stdout และ stderr ที่นำไปสู่ ​​(descriptor ไปป์เดียว) ที่ป้อนกลับไปสู่กระบวนการหลัก (Python) ในด้านผู้ปกครองนั้นมีเพียงตัวบ่งชี้ไพพ์ตัวเดียวสำหรับการอ่านเอาต์พุตอีกครั้ง เอาต์พุต "stderr" ทั้งหมดจะปรากฏขึ้นproc.stdoutและถ้าคุณเรียกproc.communicate()ผลลัพธ์ stderr (ค่าที่สองใน tuple) จะNoneไม่ใช่สตริง

กรณียาก: สองหรือมากกว่าท่อ

ปัญหาทั้งหมดเกิดขึ้นเมื่อคุณต้องการใช้อย่างน้อยสองท่อ ในความเป็นจริงsubprocessรหัสตัวเองมีบิตนี้:

def communicate(self, input=None):
    ...
    # Optimization: If we are only using one pipe, or no pipe at
    # all, using select() or threads is unnecessary.
    if [self.stdin, self.stdout, self.stderr].count(None) >= 2:

แต่อนิจจาที่นี่เราทำอย่างน้อยสองและอาจจะสามท่อที่แตกต่างกันดังนั้นcount(None)ผลตอบแทนทั้ง 1 หรือ 0 เราต้องทำสิ่งที่ยาก

บน Windows สิ่งนี้ใช้threading.Threadเพื่อรวบรวมผลลัพธ์สำหรับself.stdoutและself.stderrและมีเธรดหลักส่งself.stdinข้อมูลอินพุต (และจากนั้นปิดไพพ์)

บน POSIX สิ่งนี้ใช้pollถ้ามีมิฉะนั้นselectจะสะสมเอาท์พุทและส่งมอบอินพุต stdin ทั้งหมดนี้ทำงานในกระบวนการ / เธรดหลัก (เดี่ยว)

จำเป็นต้องมีเธรดหรือแบบสำรวจ / เลือกที่นี่เพื่อหลีกเลี่ยงการหยุดชะงัก ตัวอย่างเช่นสมมติว่าเราได้เปลี่ยนเส้นทางทั้งสามสตรีมเป็นสามท่อแยก สมมติว่ามีข้อ จำกัด เล็กน้อยเกี่ยวกับจำนวนข้อมูลที่สามารถยัดลงในไพพ์ก่อนที่กระบวนการเขียนจะหยุดชั่วคราวรอกระบวนการอ่านเพื่อ "ล้าง" ไพพ์จากปลายอีกด้าน มาตั้งค่าขีด จำกัด เล็ก ๆ นี้ไว้ที่ไบต์เดียวเพื่อเป็นภาพประกอบ (นี่คือความจริงแล้วสิ่งต่าง ๆ ทำงานอย่างไรยกเว้นว่าขีด จำกัด นั้นใหญ่กว่าหนึ่งไบต์)

หากกระบวนการหลัก (Python) พยายามเขียนหลายไบต์เช่น, 'go\n'ถึงproc.stdin, ไบต์แรกเข้าและจากนั้นกระบวนการที่สองทำให้กระบวนการ Python หยุดชั่วคราว, รอให้กระบวนการย่อยอ่านไบต์แรก, ล้างไพพ์

ในขณะเดียวกันสมมติว่ากระบวนการย่อยตัดสินใจพิมพ์ "Hello! Don't Panic!" ที่เป็นมิตร ทักทาย การHเข้าไปใน stdout pipe แต่eสาเหตุที่ทำให้หยุดชั่วคราวรอให้พาเรนต์อ่านนั่นHทำให้ตะกอน stdout หายไป

ตอนนี้เรากำลังติดอยู่: กระบวนการ Python กำลังหลับรอที่จะเสร็จสิ้นการพูดว่า "ไป" และกระบวนการย่อยยังหลับรอที่จะเสร็จสิ้นการพูดว่า "Hello! Don't Panic!"

subprocess.Popenรหัสหลีกเลี่ยงปัญหานี้กับเกลียวหรือเลือก / การสำรวจความคิดเห็น เมื่อไบต์สามารถผ่านไปป์พวกเขาไป เมื่อไม่สามารถทำได้มีเพียงเธรด (ไม่ใช่กระบวนการทั้งหมด) ที่ต้องพัก - หรือในกรณีของการเลือก / การสำรวจโพรเซสของ Python จะรอพร้อมกันสำหรับ "สามารถเขียน" หรือ "data available", เขียนไปยัง stdin ของกระบวนการ เมื่อมีที่ว่างเท่านั้นและอ่าน stdout และ / หรือ stderr เฉพาะเมื่อข้อมูลพร้อม proc.communicate()รหัส (ที่จริง_communicateที่กรณีที่มีขนดกได้รับการจัดการ) ผลตอบแทนเมื่อมีข้อมูล stdin ทั้งหมด (ถ้ามี) ได้รับการส่งและ stdout และ / หรือ stderr ข้อมูลทั้งหมดได้รับการสะสม

หากคุณต้องการอ่านทั้งสองstdoutและstderrในสองท่อที่แตกต่างกัน (โดยไม่คำนึงถึงการstdinเปลี่ยนเส้นทางใด ๆ) คุณจะต้องหลีกเลี่ยงการหยุดชะงักด้วย สถานการณ์การหยุดชะงักที่นี่แตกต่างกัน - มันเกิดขึ้นเมื่อหน่วยประมวลผลย่อยเขียนอะไรบางอย่างนานstderrในขณะที่คุณกำลังดึงข้อมูลstdoutหรือในทางกลับกัน - แต่มันยังอยู่ที่นั่น


ตัวอย่าง

ฉันสัญญาว่าจะแสดงให้เห็นว่ายกเลิกการเปลี่ยนเส้นทาง, Python subprocessES เขียนไป stdout sys.stdoutพื้นฐานไม่ ดังนั้นนี่คือรหัสบางส่วน:

from cStringIO import StringIO
import os
import subprocess
import sys

def show1():
    print 'start show1'
    save = sys.stdout
    sys.stdout = StringIO()
    print 'sys.stdout being buffered'
    proc = subprocess.Popen(['echo', 'hello'])
    proc.wait()
    in_stdout = sys.stdout.getvalue()
    sys.stdout = save
    print 'in buffer:', in_stdout

def show2():
    print 'start show2'
    save = sys.stdout
    sys.stdout = open(os.devnull, 'w')
    print 'after redirect sys.stdout'
    proc = subprocess.Popen(['echo', 'hello'])
    proc.wait()
    sys.stdout = save

show1()
show2()

เมื่อทำงาน:

$ python out.py
start show1
hello
in buffer: sys.stdout being buffered

start show2
hello

หมายเหตุว่ากิจวัตรประจำวันแรกจะล้มเหลวถ้าคุณเพิ่มstdout=sys.stdoutเป็นวัตถุที่ไม่เคยมีใครStringIO filenoที่สองจะละเว้นhelloถ้าคุณเพิ่มstdout=sys.stdoutตั้งแต่ได้รับการเปลี่ยนเส้นทางไปยังsys.stdoutos.devnull

(หากคุณเปลี่ยนเส้นทาง file-descriptor-1 ของ Python ระบบย่อยจะติดตามการเปลี่ยนเส้นทางนั้นการopen(os.devnull, 'w')โทรจะสร้างสตรีมที่มีfileno()ค่ามากกว่า 2)


อืมมม การสาธิตของคุณดูเหมือนจะแสดงตรงกันข้ามกับการอ้างสิทธิ์ในที่สุด คุณกำลังนำ Python stdout ไปยังบัฟเฟอร์อีกครั้ง แต่ stdout ของ subprocess ยังคงไปที่คอนโซล มันมีประโยชน์อย่างไร? ฉันพลาดอะไรไปรึเปล่า?
Guy Sirton

@GuySirton: การสาธิตแสดงให้เห็นว่า subprocess stdout (เมื่อไม่ได้กำกับโดยตรงsys.stdout) ไปที่Python stdout ไม่ใช่ stython ของ python program ( sys.) ซึ่งฉันยอมรับก็คือ ... ความแตกต่างที่แปลกประหลาด มีวิธีที่ดีกว่าในการใช้วลีนี้หรือไม่?
torek

เป็นเรื่องดีที่รู้ แต่เราต้องการจับเอาท์พุท subprocess ที่นี่ดังนั้นการเปลี่ยน sys.stdout นั้นเจ๋ง แต่ไม่ช่วยเราคิด การสังเกตที่ดีที่การสื่อสารจะต้องใช้บางสิ่งบางอย่างเช่น select (), การสำรวจความคิดเห็นหรือกระทู้
Guy Sirton

2
+1 คำอธิบายที่ดี แต่ไม่มีตัวอย่างรหัสที่ชัดเจน นี่คือasyncioรหัสชั่นที่ใช้ "ส่วนที่ยาก" (จะจัดการกับท่อหลายพร้อมกัน) ในทางแบบพกพา คุณสามารถเปรียบเทียบกับรหัสที่ใช้หลายหัวข้อ ( teed_call()) จะทำเช่นเดียวกัน
jfs

ฉันได้เพิ่มการใช้งานด้วย select ()
sivann

20

นอกจากนี้เรายังสามารถใช้ตัววนไฟล์เริ่มต้นสำหรับการอ่าน stdout แทนการใช้ตัวสร้าง iter พร้อม readline ()

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)

คำตอบที่หรูหราที่สุดที่นี่!
Nir

9
โซลูชันนี้ไม่แสดงผลตามเวลาจริง มันจะรอจนกว่ากระบวนการเสร็จสิ้นและแสดงผลลัพธ์ทั้งหมดในครั้งเดียว ในโซลูชันของ Viktor Kerkez หาก "your_command" แสดงขึ้นอย่างต่อเนื่องเอาต์พุตจะตามหลังไปเรื่อย ๆ ตราบใดที่ "your_command" ฟลัช stdout เป็นครั้งคราว (เนื่องจากไพพ์)
Eric H.

1
@Nir เพราะมันไม่ได้มีชีวิตอยู่
melMass

วิธีการแก้ปัญหานี้ซ้ำใน descriptor เริ่มต้นดังนั้นมันจะปรับปรุงเฉพาะเมื่อสายการปรับปรุงในการส่งออก สำหรับการอัปเดตตามตัวละครคุณจำเป็นต้องทำซ้ำในวิธี read () ดังที่แสดงในโซลูชันของ Viktor แต่นั่นเป็น overkill สำหรับกรณีการใช้งานของฉัน
Jughead

11

หากคุณสามารถใช้ห้องสมุดบุคคลที่สามคุณอาจใช้สิ่งที่ต้องการsarge(การเปิดเผย: ฉันเป็นผู้ดูแล) ไลบรารีนี้อนุญาตการเข้าถึงแบบไม่บล็อกการสตรีมเอาต์พุตจากกระบวนการย่อย - ซึ่งอยู่เหนือsubprocessโมดูล


ทำงานได้ดีบน sarge, BTW ที่จริงแล้วแก้ปัญหาความต้องการของ OP แต่อาจจะหนักหน่วงเล็กน้อยสำหรับกรณีใช้งาน
deepelement

หากคุณกำลังแนะนำเครื่องมืออย่างน้อยแสดงตัวอย่างการใช้งานสำหรับกรณีนี้
Serhiy

4

โซลูชันที่ 1: บันทึกstdoutและstderrพร้อมกันในเรียลไทม์

ทางออกที่ง่ายซึ่งบันทึกทั้ง stdout และ stderr พร้อมกันแบบทีละบรรทัดในเรียลไทม์ลงในไฟล์บันทึก

import subprocess as sp
from concurrent.futures import ThreadPoolExecutor


def log_popen_pipe(p, stdfile):

    with open("mylog.txt", "w") as f:

        while p.poll() is None:
            f.write(stdfile.readline())
            f.flush()

        # Write the rest from the buffer
        f.write(stdfile.read())


with sp.Popen(["ls"], stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    with ThreadPoolExecutor(2) as pool:
        r1 = pool.submit(log_popen_pipe, p, p.stdout)
        r2 = pool.submit(log_popen_pipe, p, p.stderr)
        r1.result()
        r2.result()

โซลูชันที่ 2: ฟังก์ชันread_popen_pipes()ที่อนุญาตให้คุณวนซ้ำทั้งสองท่อ (stdout / stderr) พร้อมกันแบบเรียลไทม์

import subprocess as sp
from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

# The function in use:

with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):
        print(out_line, end='')
        print(err_line, end='')

    return p.poll()

3

ทางออกที่ดี แต่ "เฮฟวี่เวท" คือการใช้ Twisted - ดูที่ด้านล่าง

หากคุณยินดีที่จะอยู่กับ stdout เพียงอย่างเดียวที่ควรใช้:

import subprocess
import sys
popenobj = subprocess.Popen(["ls", "-Rl"], stdout=subprocess.PIPE)
while not popenobj.poll():
   stdoutdata = popenobj.stdout.readline()
   if stdoutdata:
      sys.stdout.write(stdoutdata)
   else:
      break
print "Return code", popenobj.returncode

(ถ้าคุณใช้ read () มันพยายามที่จะอ่าน "ไฟล์" ทั้งหมดซึ่งไม่มีประโยชน์อะไรที่เราสามารถใช้ที่นี่จริงๆคือสิ่งที่อ่านข้อมูลทั้งหมดที่อยู่ในไพพ์ตอนนี้)

เราอาจลองใช้วิธีนี้กับเธรดเช่น:

import subprocess
import sys
import threading

popenobj = subprocess.Popen("ls", stdout=subprocess.PIPE, shell=True)

def stdoutprocess(o):
   while True:
      stdoutdata = o.stdout.readline()
      if stdoutdata:
         sys.stdout.write(stdoutdata)
      else:
         break

t = threading.Thread(target=stdoutprocess, args=(popenobj,))
t.start()
popenobj.wait()
t.join()
print "Return code", popenobj.returncode

ตอนนี้เราอาจเพิ่ม stderr ด้วยการมีสองเธรด

โปรดทราบว่าเอกสารย่อยกระบวนการไม่สนับสนุนการใช้ไฟล์เหล่านี้โดยตรงและแนะนำให้ใช้communicate()(ส่วนใหญ่เกี่ยวข้องกับการหยุดชะงักซึ่งฉันคิดว่าไม่ใช่ปัญหาข้างต้น) และการแก้ปัญหาเป็น klunky เล็กน้อยดังนั้นจึงดูเหมือนว่าโมดูลย่อยไม่มาก งาน (ดูที่: http://www.python.org/dev/peps/pep-3145/ ) และเราต้องดูอย่างอื่น

วิธีแก้ปัญหาที่เกี่ยวข้องมากขึ้นคือการใช้Twistedดังที่นี่: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

วิธีที่คุณทำกับTwistedคือการสร้างกระบวนการของคุณโดยใช้reactor.spawnprocess()และให้สิ่งProcessProtocolนั้นจากนั้นประมวลผลเอาต์พุตแบบอะซิงโครนัส ตัวอย่างโค้ด Python แบบ Twisted อยู่ที่นี่: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py


ขอบคุณ! ฉันเพิ่งลองอะไรแบบนี้ (ตามความคิดเห็นของ @PauloAlmeida แต่การเรียกไปที่ subprocess.Popen กำลังปิดกั้น - นั่นคือมาถึงขณะที่ลูปเมื่อส่งคืนเท่านั้น ...
DilithiumMatrix

1
นั่นไม่ใช่สิ่งที่เกิดขึ้น มันเข้าสู่ห่วงขณะที่ทันทีจากนั้นปิดกั้นการread()โทรจนกว่าจะออกจากกระบวนการย่อยและกระบวนการหลักที่ได้รับEOFในไปป์
Alp

@Alp ที่น่าสนใจ! ดังนั้นมันจึงเป็น
DilithiumMatrix

ใช่ฉันเร็วเกินไปที่จะโพสต์นี้ อันที่จริงมันทำงานไม่ถูกต้องและไม่สามารถแก้ไขได้ง่าย กลับไปที่ตารางการวาด
Guy Sirton

1
@zhermes: ดังนั้นปัญหาของ read () ก็คือมันจะพยายามอ่านผลลัพธ์ทั้งหมดจนถึง EOF ซึ่งไม่มีประโยชน์ readline () ช่วยและอาจเป็นสิ่งที่คุณต้องการ (เส้นที่ยาวจริงๆอาจเป็นปัญหาได้เช่นกัน) คุณต้องระวังการบัฟเฟอร์ในกระบวนการที่คุณกำลังจะเปิดตัว ...
Guy Sirton

3

นอกจากคำตอบทั้งหมดเหล่านี้แล้ววิธีการง่ายๆหนึ่งวิธีอาจเป็นดังนี้:

process = subprocess.Popen(your_command, stdout=subprocess.PIPE)

while process.stdout.readable():
    line = process.stdout.readline()

    if not line:
        break

    print(line.strip())

วนซ้ำผ่านสตรีมที่อ่านได้ตราบเท่าที่สามารถอ่านได้และหากได้รับผลลัพธ์ที่ว่างให้หยุด

กุญแจสำคัญในที่นี้คือreadline()ส่งคืนบรรทัด ( \nที่ส่วนท้าย) ตราบใดที่มีเอาต์พุตและว่างเปล่าถ้ามันอยู่ที่ท้ายจริงๆ

หวังว่านี่จะช่วยใครซักคน


3

จากทั้งหมดข้างต้นฉันขอแนะนำรุ่นดัดแปลงเล็กน้อย (python3):

  • ขณะที่วนรอบเรียก readline (วิธีแก้ปัญหาที่แนะนำก็ดูเหมือนจะบล็อกตลอดไปสำหรับฉัน - Python 3, Windows 7)
  • มีโครงสร้างดังนั้นการจัดการข้อมูลการอ่านไม่จำเป็นต้องทำซ้ำหลังจากการสำรวจไม่ได้ส่งคืนNone
  • stderr ไปป์ที่ stdout ดังนั้นเอาต์พุตทั้งคู่จึงอ่านได้
  • เพิ่มรหัสเพื่อรับค่าการออก cmd

รหัส:

import subprocess
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT, universal_newlines=True)
while True:
    rd = proc.stdout.readline()
    print(rd, end='')  # and whatever you want to do...
    if not rd:  # EOF
        returncode = proc.poll()
        if returncode is not None:
            break
        time.sleep(0.1)  # cmd closed stdout, but not exited yet

# You may want to check on ReturnCode here

returncodeส่วนหนึ่งเป็นเรื่องสำคัญมากในกรณีของฉัน
ละอองดาว

2

ดูเหมือนว่าเอาต์พุตบรรทัดบัฟเฟอร์จะเหมาะกับคุณซึ่งในบางกรณีเช่นนี้อาจเหมาะกับ (Caveat: ยังไม่ได้ทดสอบ) สิ่งนี้จะให้ stdout ของกระบวนการย่อยตามเวลาจริงเท่านั้น หากคุณต้องการมีทั้ง stderr และ stdout ตามเวลาจริงคุณจะต้องทำอะไรที่ซับซ้อนselectกว่านี้

proc = subprocess.Popen(run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
while proc.poll() is None:
    line = proc.stdout.readline()
    print line
    log_file.write(line + '\n')
# Might still be data on stdout at this point.  Grab any
# remainder.
for line in proc.stdout.read().split('\n'):
    print line
    log_file.write(line + '\n')
# Do whatever you want with proc.stderr here...

2

ทำไมไม่ตั้งstdoutโดยตรงไปยังsys.stdout? และถ้าคุณต้องการส่งออกไปยังบันทึกเช่นกันคุณก็สามารถแทนที่วิธีการเขียนของ f

import sys
import subprocess

class SuperFile(open.__class__):

    def write(self, data):
        sys.stdout.write(data)
        super(SuperFile, self).write(data)

f = SuperFile("log.txt","w+")       
process = subprocess.Popen(command, stdout=f, stderr=f)

ไม่สามารถใช้งานได้: โมดูลย่อยประมวลผลและตั้งค่าstdoutfile descriptor เป็น file descriptor ของอ็อบเจ็กต์ไฟล์ที่ส่งผ่าน วิธีการเขียนจะไม่ถูกเรียก (อย่างน้อยนั่นคือสิ่งที่ subprocess ทำสำหรับ stderr ฉันคิดว่ามันเหมือนกันสำหรับ stdout)
t.animal

2

โซลูชันด้านบนทั้งหมดที่ฉันลองทำไม่สามารถแยกเอาต์พุต stderr และ stdout, (หลายไพพ์) หรือถูกบล็อกตลอดไปเมื่อบัฟเฟอร์ของไพพ์ OS เต็มซึ่งเกิดขึ้นเมื่อคำสั่งที่คุณรันเอาต์พุตเร็วเกินไป (มีคำเตือนสำหรับ python แบบสำรวจความคิดเห็น () คู่มือกระบวนการย่อย) วิธีเดียวที่เชื่อถือได้ที่ฉันพบคือผ่านการเลือก แต่นี่เป็นวิธีแก้ปัญหา posix เท่านั้น:

import subprocess
import sys
import os
import select
# returns command exit status, stdout text, stderr text
# rtoutput: show realtime output while running
def run_script(cmd,rtoutput=0):
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    poller = select.poll()
    poller.register(p.stdout, select.POLLIN)
    poller.register(p.stderr, select.POLLIN)

    coutput=''
    cerror=''
    fdhup={}
    fdhup[p.stdout.fileno()]=0
    fdhup[p.stderr.fileno()]=0
    while sum(fdhup.values()) < len(fdhup):
        try:
            r = poller.poll(1)
        except select.error, err:
            if err.args[0] != EINTR:
                raise
            r=[]
        for fd, flags in r:
            if flags & (select.POLLIN | select.POLLPRI):
                c = os.read(fd, 1024)
                if rtoutput:
                    sys.stdout.write(c)
                    sys.stdout.flush()
                if fd == p.stderr.fileno():
                    cerror+=c
                else:
                    coutput+=c
            else:
                fdhup[fd]=1
    return p.poll(), coutput.strip(), cerror.strip()

อีกทางเลือกหนึ่งคือการปั่นด้ายหนึ่งต่อท่อ แต่ละเธรดสามารถบล็อก I / O บนไพพ์ได้โดยไม่ต้องบล็อกเธรดอื่น แต่สิ่งนี้จะแนะนำชุดปัญหาของตนเอง วิธีการทั้งหมดมีปัญหาคุณเพียงแค่เลือกว่าคุณคิดว่าน่ารำคาญน้อยที่สุด :-)
2560

2

คล้ายกับคำตอบก่อน แต่การแก้ปัญหาต่อไปนี้ทำงานสำหรับฉันในหน้าต่างโดยใช้ Python3 เพื่อให้วิธีการทั่วไปในการพิมพ์และเข้าสู่ระบบเรียลไทม์ ( รับเรียลไทม์เอาท์พุทโดยใช้หลาม ):

def print_and_log(command, logFile):
    with open(logFile, 'wb') as f:
        command = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)

        while True:
            output = command.stdout.readline()
            if not output and command.poll() is not None:
                f.close()
                break
            if output:
                f.write(output)
                print(str(output.strip(), 'utf-8'), flush=True)
        return command.poll()

2

ผมคิดว่าsubprocess.communicateวิธีการที่เป็นบิตทำให้เข้าใจผิด: มันจริงเติมstdoutและstderrsubprocess.Popenที่คุณระบุใน

แต่อ่านจากsubprocess.PIPEที่คุณสามารถให้กับsubprocess.Popen's stdoutและstderrพารามิเตอร์ในที่สุดจะเติมบัฟเฟอร์ท่อ OS และการหยุดชะงักของแอป (โดยเฉพาะอย่างยิ่งถ้าคุณได้หลายกระบวนการ / หัวข้อที่ใช้ต้องsubprocess)

โซลูชั่นที่นำเสนอของฉันคือการให้stdoutและstderrกับไฟล์ - และอ่านเนื้อหาไฟล์แทนการอ่านจาก PIPEdeadlocking ไฟล์เหล่านี้สามารถtempfile.NamedTemporaryFile()- subprocess.communicateซึ่งยังสามารถเข้าถึงได้สำหรับการอ่านในขณะที่พวกเขากำลังถูกเขียนลงโดย

ด้านล่างคือตัวอย่างการใช้งาน:

        try:
            with ProcessRunner(('python', 'task.py'), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

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

หากคุณใช้ python 2 โปรดตรวจสอบให้แน่ใจว่าได้ติดตั้งแพ็คเกจย่อย subprocess32เวอร์ชันล่าสุดจาก pypi


import os
import sys
import threading
import time
import tempfile
import logging

if os.name == 'posix' and sys.version_info[0] < 3:
    # Support python 2
    import subprocess32 as subprocess
else:
    # Get latest and greatest from python 3
    import subprocess

logger = logging.getLogger(__name__)


class ProcessError(Exception):
    """Base exception for errors related to running the process"""


class ProcessTimeout(ProcessError):
    """Error that will be raised when the process execution will exceed a timeout"""


class ProcessRunner(object):
    def __init__(self, args, env=None, timeout=None, bufsize=-1, seconds_to_wait=0.25, **kwargs):
        """
        Constructor facade to subprocess.Popen that receives parameters which are more specifically required for the
        Process Runner. This is a class that should be used as a context manager - and that provides an iterator
        for reading captured output from subprocess.communicate in near realtime.

        Example usage:


        try:
            with ProcessRunner(('python', task_file_path), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

        :param args: same as subprocess.Popen
        :param env: same as subprocess.Popen
        :param timeout: same as subprocess.communicate
        :param bufsize: same as subprocess.Popen
        :param seconds_to_wait: time to wait between each readline from the temporary file
        :param kwargs: same as subprocess.Popen
        """
        self._seconds_to_wait = seconds_to_wait
        self._process_has_timed_out = False
        self._timeout = timeout
        self._process_done = False
        self._std_file_handle = tempfile.NamedTemporaryFile()
        self._process = subprocess.Popen(args, env=env, bufsize=bufsize,
                                         stdout=self._std_file_handle, stderr=self._std_file_handle, **kwargs)
        self._thread = threading.Thread(target=self._run_process)
        self._thread.daemon = True

    def __enter__(self):
        self._thread.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._thread.join()
        self._std_file_handle.close()

    def __iter__(self):
        # read all output from stdout file that subprocess.communicate fills
        with open(self._std_file_handle.name, 'r') as stdout:
            # while process is alive, keep reading data
            while not self._process_done:
                out = stdout.readline()
                out_without_trailing_whitespaces = out.rstrip()
                if out_without_trailing_whitespaces:
                    # yield stdout data without trailing \n
                    yield out_without_trailing_whitespaces
                else:
                    # if there is nothing to read, then please wait a tiny little bit
                    time.sleep(self._seconds_to_wait)

            # this is a hack: terraform seems to write to buffer after process has finished
            out = stdout.read()
            if out:
                yield out

        if self._process_has_timed_out:
            raise ProcessTimeout('Process has timed out')

        if self._process.returncode != 0:
            raise ProcessError('Process has failed')

    def _run_process(self):
        try:
            # Start gathering information (stdout and stderr) from the opened process
            self._process.communicate(timeout=self._timeout)
            # Graceful termination of the opened process
            self._process.terminate()
        except subprocess.TimeoutExpired:
            self._process_has_timed_out = True
            # Force termination of the opened process
            self._process.kill()

        self._process_done = True

    @property
    def return_code(self):
        return self._process.returncode



1

นี่คือคลาสที่ฉันใช้ในโปรเจคหนึ่งของฉัน มันเปลี่ยนเส้นทางการส่งออกของกระบวนการย่อยไปยังบันทึก ตอนแรกฉันพยายามเขียนทับวิธีการเขียน แต่ไม่ได้ผลเพราะ subprocess จะไม่เรียกมันว่า (การเปลี่ยนเส้นทางเกิดขึ้นในระดับ filescriptor) ดังนั้นฉันจึงใช้ไพพ์ของตัวเองคล้ายกับที่ทำในโมดูลย่อย นี่เป็นข้อดีของการห่อหุ้มตรรกะการบันทึก / การพิมพ์ทั้งหมดในอะแดปเตอร์และคุณสามารถส่งอินสแตนซ์ของตัวบันทึกไปที่Popen:subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))

class LogAdapter(threading.Thread):

    def __init__(self, logname, level = logging.INFO):
        super().__init__()
        self.log = logging.getLogger(logname)
        self.readpipe, self.writepipe = os.pipe()

        logFunctions = {
            logging.DEBUG: self.log.debug,
            logging.INFO: self.log.info,
            logging.WARN: self.log.warn,
            logging.ERROR: self.log.warn,
        }

        try:
            self.logFunction = logFunctions[level]
        except KeyError:
            self.logFunction = self.log.info

    def fileno(self):
        #when fileno is called this indicates the subprocess is about to fork => start thread
        self.start()
        return self.writepipe

    def finished(self):
       """If the write-filedescriptor is not closed this thread will
       prevent the whole program from exiting. You can use this method
       to clean up after the subprocess has terminated."""
       os.close(self.writepipe)

    def run(self):
        inputFile = os.fdopen(self.readpipe)

        while True:
            line = inputFile.readline()

            if len(line) == 0:
                #no new data was added
                break

            self.logFunction(line.strip())

หากคุณไม่ต้องการเข้าสู่ระบบ แต่ต้องการใช้งานprint()คุณสามารถลบส่วนที่มีขนาดใหญ่ของรหัสและทำให้ชั้นสั้นลง นอกจากนี้คุณยังสามารถขยายโดย__enter__และ__exit__วิธีการและโทรfinishedใน__exit__เพื่อให้คุณได้อย่างง่ายดายสามารถใช้เป็นบริบท


1

ไม่มีวิธีการแก้ปัญหาแบบ Pythonic ที่เหมาะกับฉัน ปรากฎว่าproc.stdout.read()หรือคล้ายกันอาจบล็อกตลอดไป

ดังนั้นฉันใช้teeสิ่งนี้:

subprocess.run('./my_long_running_binary 2>&1 | tee -a my_log_file.txt && exit ${PIPESTATUS}', shell=True, check=True, executable='/bin/bash')

วิธีนี้สะดวกถ้าคุณใช้shell=Trueอยู่

${PIPESTATUS}จับสถานะความสำเร็จของกลุ่มคำสั่งทั้งหมด (มีเฉพาะใน Bash เท่านั้น) หากฉันเว้นไว้สิ่ง&& exit ${PIPESTATUS}นี้จะส่งกลับศูนย์เสมอเนื่องจากteeไม่เคยล้มเหลว

unbufferอาจจำเป็นสำหรับการพิมพ์แต่ละบรรทัดในเทอร์มินัลทันทีแทนที่จะรอนานเกินไปจนกว่า "บัฟเฟอร์ของท่อ" เต็ม อย่างไรก็ตาม unbuffer กลืนสถานะทางออกของ assert (SIG Abort) ...

2>&1 ยังบันทึก stderror ไปยังไฟล์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.