จับ stdout แบบเรียลไทม์จากกระบวนการย่อย


89

ฉันต้องการsubprocess.Popen()rsync.exe ใน Windows และพิมพ์ stdout ใน Python

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

ตอนนี้ใช้ Python 3.1 เนื่องจากฉันได้ยินมาว่าควรจัดการ IO ได้ดีกว่า

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()


1
(มาจาก Google หรือไม่) PIPE ทั้งหมดจะหยุดชะงักเมื่อบัฟเฟอร์ของ PIPE ตัวใดตัวหนึ่งเต็มและไม่อ่าน เช่น stdout deadlock เมื่อ stderr เต็ม อย่าผ่าน PIPE ที่คุณไม่ได้ตั้งใจอ่าน
Nasser Al-Wohaibi

มีใครช่วยอธิบายได้ไหมว่าทำไมคุณถึงตั้งค่า stdout เป็น sys.stdout แทน subprocess.PIPE ไม่ได้
Mike

คำตอบ:


101

subprocessกฎระเบียบบางส่วนของหัวแม่มือสำหรับ

  • ไม่เคยshell=Trueใช้งาน โดยไม่จำเป็นต้องเรียกใช้กระบวนการเชลล์เพิ่มเติมเพื่อเรียกโปรแกรมของคุณ
  • เมื่อเรียกกระบวนการอาร์กิวเมนต์จะถูกส่งผ่านเป็นรายการ sys.argvใน python เป็นรายการและก็อยู่argvใน C ดังนั้นคุณจึงส่งผ่านรายการไปPopenเพื่อเรียกกระบวนการย่อยไม่ใช่สตริง
  • อย่าเปลี่ยนเส้นทางstderrไปยังข้อความPIPEเมื่อคุณไม่ได้อ่าน
  • อย่าเปลี่ยนเส้นทางstdinเมื่อคุณไม่ได้เขียนถึงมัน

ตัวอย่าง:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

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

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

ที่กล่าวว่าเป็นไปได้ว่า rsync จะบัฟเฟอร์เอาต์พุตเมื่อตรวจพบว่าเชื่อมต่อกับท่อแทนที่จะเป็นเทอร์มินัล นี่คือลักษณะการทำงานเริ่มต้น - เมื่อเชื่อมต่อกับไพพ์โปรแกรมต่างๆต้องล้าง stdout อย่างชัดเจนเพื่อให้ได้ผลลัพธ์แบบเรียลไทม์มิฉะนั้นไลบรารี C มาตรฐานจะบัฟเฟอร์

หากต้องการทดสอบให้ลองรันสิ่งนี้แทน:

cmd = [sys.executable, 'test_out.py']

และสร้างtest_out.pyไฟล์ที่มีเนื้อหา:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

การดำเนินการตามกระบวนการย่อยนั้นควรให้คุณ "สวัสดี" และรอ 10 วินาทีก่อนที่จะให้ "World" หากสิ่งนี้เกิดขึ้นกับรหัส python ด้านบนและไม่เกิดขึ้นrsyncนั่นหมายความว่าrsyncตัวมันเองกำลังบัฟเฟอร์เอาต์พุตดังนั้นคุณจะโชคไม่ดี

วิธีการแก้ปัญหาที่จะเชื่อมต่อโดยตรงกับการใช้สิ่งที่ต้องการptypexpect


12
shell=Falseเป็นสิ่งที่ถูกต้องเมื่อคุณสร้างบรรทัดคำสั่งโดยเฉพาะจากข้อมูลที่ผู้ใช้ป้อน แต่อย่างไรก็ตามshell=Trueก็มีประโยชน์เช่นกันเมื่อคุณรับบรรทัดคำสั่งทั้งหมดจากแหล่งที่เชื่อถือได้ (เช่นฮาร์ดโค้ดในสคริปต์)
Denis Otkidach

10
@Denis Otkidach: ฉันไม่คิดว่าจะรับประกันการใช้งานshell=True. ลองคิดดู - คุณกำลังเรียกใช้กระบวนการอื่นบนระบบปฏิบัติการของคุณซึ่งเกี่ยวข้องกับการจัดสรรหน่วยความจำการใช้ดิสก์การตั้งเวลาโปรเซสเซอร์เพียงเพื่อแยกสตริง ! และอีกหนึ่งคุณเข้าร่วมเอง !! คุณสามารถแบ่ง python ได้ แต่การเขียนแต่ละพารามิเตอร์แยกกันจะง่ายกว่าอยู่ดี นอกจากนี้การใช้รายการหมายความว่าคุณไม่ต้องหลบหนีตัวอักษรพิเศษเปลือก: ช่องว่าง;, >, <, &.. พารามิเตอร์ของคุณสามารถมีตัวอักษรเหล่านั้นและคุณจะได้ไม่ต้องกังวล! ฉันไม่เห็นเหตุผลที่จะใช้shell=Trueจริงๆเว้นแต่คุณจะใช้คำสั่งเชลล์อย่างเดียว
nosklo

nosklo ที่ควรจะเป็น: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick: ฉันไม่แน่ใจว่าทำไมคุณถึงดำเนินการเหล่านั้นเป็นกระบวนการแยกกัน ... คุณสามารถตัดเนื้อหาไฟล์และแตกฟิลด์แรกได้อย่างง่ายดายใน python โดยใช้csvโมดูล แต่ตัวอย่างเช่นไปป์ไลน์ของคุณใน python จะเป็น: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultโปรดทราบว่าคุณสามารถทำงานกับชื่อไฟล์แบบยาวและเชลล์อักขระพิเศษได้โดยไม่ต้องหลบหนีตอนนี้เชลล์ไม่เกี่ยวข้องแล้ว นอกจากนี้ยังเร็วกว่ามากเนื่องจากมีกระบวนการน้อยกว่า
nosklo

11
ใช้for line in iter(p.stdout.readline, b'')แทนfor line in p.stdoutใน Python 2 มิฉะนั้นบรรทัดจะไม่ถูกอ่านแบบเรียลไทม์แม้ว่ากระบวนการซอร์สจะไม่บัฟเฟอร์เอาต์พุตก็ตาม
jfs

43

ฉันรู้ว่านี่เป็นหัวข้อเก่า แต่ตอนนี้มีวิธีแก้ไข เรียก rsync ด้วยตัวเลือก --outbuf = L ตัวอย่าง:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
วิธีนี้ใช้ได้ผลและควรได้รับการโหวตเพื่อช่วยผู้อ่านในอนาคตจากการเลื่อนดูกล่องโต้ตอบทั้งหมดด้านบน
VectorVictor

1
@VectorVictor มันไม่ได้อธิบายว่าเกิดอะไรขึ้นและทำไมมันถึงเกิดขึ้น อาจเป็นไปได้ว่าโปรแกรมของคุณใช้งานได้จนกระทั่ง: 1. คุณเพิ่มpreexec_fn=os.setpgrpเพื่อให้โปรแกรมอยู่รอดสคริปต์หลักของมัน 2. คุณข้ามการอ่านจากไพพ์ของกระบวนการ 3. กระบวนการส่งออกข้อมูลจำนวนมากเติมไปป์ 4. คุณติดอยู่เป็นเวลาหลายชั่วโมง พยายามที่จะคิดออกว่าทำไมโปรแกรมที่คุณกำลังใช้งานลาออกหลังจากที่บางจำนวนสุ่มของเวลา คำตอบจาก @nosklo ช่วยฉันได้มาก
danuker

16

บน Linux ฉันมีปัญหาเดียวกันในการกำจัดบัฟเฟอร์ ในที่สุดฉันก็ใช้ "stdbuf -o0" (หรือ unbuffer จากที่คาดหวัง) เพื่อกำจัด PIPE บัฟเฟอร์

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

จากนั้นฉันสามารถใช้ select.select บน stdout

ดูเพิ่มเติมที่/unix/25372/


2
สำหรับใครก็ตามที่พยายามคว้ารหัส C stdout จาก Python ฉันสามารถยืนยันได้ว่าโซลูชันนี้เป็นโซลูชันเดียวที่ใช้ได้กับฉัน เพื่อความชัดเจนฉันกำลังพูดถึงการเพิ่ม 'stdbuf', '-o0' ในรายการคำสั่งที่มีอยู่ใน Popen
ประมาท

ขอขอบคุณ! stdbuf -o0พิสูจน์แล้วว่ามีประโยชน์จริง ๆกับการทดสอบ pytest / pytest-bdd มากมายที่ฉันเขียนว่าวางไข่แอป C ++ และตรวจสอบว่ามันส่งข้อความบันทึกบางอย่าง หากไม่มีstdbuf -o0การทดสอบเหล่านี้ต้องใช้เวลา 7 วินาทีเพื่อรับเอาต์พุต (บัฟเฟอร์) จากโปรแกรม C ++ ตอนนี้พวกเขาทำงานเกือบจะในทันที!
evadeflow

คำตอบนี้ช่วยฉันได้แล้ววันนี้! การเรียกใช้แอปพลิเคชันเป็นกระบวนการย่อยเป็นส่วนหนึ่งpytestฉันไม่สามารถรับเอาต์พุต stdbufทำมัน.
Janos

14

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

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

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

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

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

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

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


1
คุณช่วยวันของฉันขอบคุณสำหรับ PYTHONUNBUFFERED = 1
diewland

9
for line in p.stdout:
  ...

บล็อกเสมอจนกว่าจะถึงฟีดบรรทัดถัดไป

สำหรับพฤติกรรม "เรียลไทม์" คุณต้องทำสิ่งนี้:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

while-loop จะเหลืออยู่เมื่อกระบวนการลูกปิด stdout หรือออก read()/read(-1)จะบล็อกจนกว่ากระบวนการย่อยจะปิด stdout หรือออก


1
incharไม่เคยNoneใช้if not inchar:แทน ( read()ส่งคืนสตริงว่างบน EOF) btw มันแย่กว่านั้นคือfor line in p.stdoutไม่พิมพ์แม้แต่เต็มบรรทัดในแบบเรียลไทม์ใน Python 2 (สามารถใช้for line in iter (p.stdout.readline, '') `แทนได้)
jfs

1
ฉันได้ทดสอบสิ่งนี้กับ python 3.4 บน osx แล้วและไม่ได้ผล
qed

1
@qed: for line in p.stdout:ทำงานบน Python 3 อย่าลืมเข้าใจความแตกต่างระหว่าง''(Unicode string) และb''(bytes) ดูPython: อ่านอินพุตการสตรีมจาก subprocess.communicate ()
jfs

8

ปัญหาของคุณคือ:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

ตัววนซ้ำเองมีการบัฟเฟอร์พิเศษ

ลองทำดังนี้

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

5

คุณไม่สามารถรับ stdout เพื่อพิมพ์ unbuffered ไปยังไพพ์ (เว้นแต่คุณจะสามารถเขียนโปรแกรมที่พิมพ์ไปยัง stdout ใหม่ได้) ดังนั้นนี่คือทางออกของฉัน

เปลี่ยนเส้นทาง stdout ไปที่ sterr ซึ่งไม่ได้บัฟเฟอร์ '<cmd> 1>&2'ควรทำ เปิดกระบวนการดังนี้: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
คุณไม่สามารถแยกความแตกต่างจาก stdout หรือ stderr ได้ แต่คุณจะได้รับผลลัพธ์ทั้งหมดทันที

หวังว่านี่จะช่วยทุกคนที่แก้ปัญหานี้ได้


4
คุณลองหรือยัง? เพราะมันไม่ทำงาน .. ถ้า stdout ถูกบัฟเฟอร์ในกระบวนการนั้นจะไม่ถูกเปลี่ยนเส้นทางไปยัง stderr ในลักษณะเดียวกับที่ไม่เปลี่ยนเส้นทางไปยัง PIPE หรือไฟล์ ..
Filipe Pina

5
นี่เป็นเรื่องผิดธรรมดา stdout buffering เกิดขึ้นภายในโปรแกรมเอง ไวยากรณ์ของเชลล์1>&2จะเปลี่ยนไฟล์ที่ตัวอธิบายไฟล์ชี้ไปก่อนที่จะเปิดโปรแกรม ตัวโปรแกรมเองไม่สามารถแยกความแตกต่างระหว่างการเปลี่ยนเส้นทาง stdout เป็น stderr ( 1>&2) หรือกลับกัน ( 2>&1) ดังนั้นสิ่งนี้จะไม่มีผลต่อพฤติกรรมการบัฟเฟอร์ของโปรแกรมและไม่ว่าด้วยวิธีใดที่1>&2เชลล์จะตีความไวยากรณ์ จะล้มเหลวเพราะคุณไม่ได้ระบุsubprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE) shell=True
Will Manley

ในกรณีที่ผู้คนกำลังอ่านสิ่งนี้: ฉันลองใช้ stderr แทน stdout มันแสดงพฤติกรรมเดียวกันทุกประการ
martinthenext

3

เปลี่ยน stdout จากกระบวนการ rsync เป็น unbuffered

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

3
การบัฟเฟอร์เกิดขึ้นที่ด้าน rsync การเปลี่ยนแอตทริบิวต์ bufsize ในด้าน python จะไม่ช่วย
nosklo

14
สำหรับใครก็ตามที่กำลังค้นหาคำตอบของ nosklo นั้นผิดทั้งหมด: การแสดงความคืบหน้าของ rsync ไม่ได้บัฟเฟอร์ ปัญหาที่แท้จริงคือกระบวนการย่อยส่งคืนอ็อบเจ็กต์ไฟล์และอินเทอร์เฟซตัววนซ้ำไฟล์มีบัฟเฟอร์ภายในที่มีเอกสารไม่ดีแม้จะมี bufsize = 0 ทำให้คุณต้องเรียก readline () ซ้ำ ๆ หากคุณต้องการผลลัพธ์ก่อนที่บัฟเฟอร์จะเติม
Chris Adams

3

เพื่อหลีกเลี่ยงการแคชผลลัพธ์คุณอาจต้องการลองใช้ pexpect

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : ฉันรู้ว่าคำถามนี้ค่อนข้างเก่า แต่ยังคงให้คำตอบที่เหมาะกับฉัน

PPS : ได้คำตอบจากคำถามอื่น


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

ฉันกำลังเขียน GUI สำหรับ rsync ใน python และมีคำสั่งเดียวกัน ปัญหานี้ทำให้ฉันทุกข์ใจมาหลายวันจนกระทั่งฉันพบสิ่งนี้ใน pyDoc

ถ้า universal_newlines เป็น True อ็อบเจ็กต์ไฟล์ stdout และ stderr จะเปิดเป็นไฟล์ข้อความในโหมด newlines สากล บรรทัดอาจถูกยกเลิกโดย '\ n', อนุสัญญา Unix end-of-line, '\ r', แบบดั้งเดิมของ Macintosh หรือ '\ r \ n', อนุสัญญา Windows การแสดงภายนอกทั้งหมดเหล่านี้ถูกมองว่าเป็น '\ n' โดยโปรแกรม Python

ดูเหมือนว่า rsync จะแสดงผล '\ r' เมื่อแปลภาษา


1

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

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

มันไม่ได้เป็นแบบเรียลไทม์ ไฟล์ไม่สามารถแก้ปัญหาการบัฟเฟอร์ในด้านของ rsync
jfs

tempfile.Tem ContemporaryFile สามารถลบตัวเองเพื่อให้ทำความสะอาดได้ง่ายขึ้นในกรณีที่มีข้อยกเว้น
jfs

3
while not p.poll()นำไปสู่การวนซ้ำที่ไม่มีที่สิ้นสุดหากกระบวนการย่อยออกจาก 0 สำเร็จให้ใช้p.poll() is Noneแทน
jfs

Windows อาจห้ามไม่ให้เปิดไฟล์ที่เปิดอยู่แล้วดังนั้นopen(file_name)อาจล้มเหลว
jfs

1
ฉันเพิ่งพบคำตอบนี้น่าเสียดายสำหรับ linux เท่านั้น แต่ใช้งานได้เหมือน charm link ดังนั้นฉันจึงขยายคำสั่งของฉันดังนี้: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvและโทร: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) และตอนนี้ฉันสามารถอ่านได้โดยไม่ต้องบัฟเฟอร์
Arvid Terzibaschian

0

หากคุณเรียกใช้สิ่งนี้ในเธรดและบันทึกคุณสมบัติ ffmpeg_time ในคุณสมบัติของวิธีการเพื่อให้คุณสามารถเข้าถึงได้มันจะใช้งานได้ดีมากฉันได้รับผลลัพธ์เช่นนี้: เอาต์พุตจะเป็นเช่นถ้าคุณใช้เธรดใน tkinter

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

ใน Python 3 นี่คือวิธีแก้ปัญหาซึ่งใช้คำสั่งจากบรรทัดคำสั่งและส่งสตริงที่ถอดรหัสอย่างดีแบบเรียลไทม์เมื่อได้รับ

ผู้รับ ( receiver.py):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

ตัวอย่างโปรแกรมง่ายๆที่สามารถสร้างเอาต์พุตแบบเรียลไทม์ ( dummy_out.py):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

เอาท์พุต:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.