φυβλαςのβλογ
phyblas的博客



การใช้ subprocess.Popen ใน python เพื่อควบคุม shell ไปในขณะรันโปรแกรม
เขียนเมื่อ 2020/03/07 21:18
แก้ไขล่าสุด 2024/02/22 10:38


บทความนี้เป็นส่วนเสริมของเนื้อหาเรื่องการใช้มอดูล subprocess https://phyblas.hinaboshi.com/20200306

โดยจะพูดถึงการใช้ subprocess.Popen ซึ่งจะซับซ้อนขึ้นมากว่าการใช้ subprocess.run แต่เหมาะจะใช้ในบางกรณีมากกว่า เช่นเมื่อต้องมีการป้อนค่าเพิ่มเข้าไป หรือเมื่อต้องการสั่งหลายๆคำสั่งพร้อมกันคู่ขนาดกันไป




ว่าด้วยเรื่องคำสั่งที่ต้องมีการป้อนค่าเข้า

ขอเริ่มจากเกริ่นถึงลักษณะของโปรแกรมที่ควรใช้ subprocess.Popen

สมมุติว่ามีโปรแกรมไพธอนอันนึง sanpo.py เขียนไว้แบบนี้
a = float(input())
print(a/11.)

เนื่องจากมีคำสั่ง input ดังนั้นโปรแกรมนี้จะต้องรอรับค่าเพื่อที่จะไปต่อได้ ไม่เช่นนั้นโปรแกรมก็จะไม่มีทางทำงานต่อจนจบ

ในกรณีแบบนี้หากใช้ subprocess.run ละก็ จะไม่ได้
import subprocess
subprocess.run(['python','sanpo.py'])

ได้
Traceback (most recent call last):
  File "sanpo.py", line 1, in <module>
    a = float(input())
EOFError: EOF when reading a line

ในกรณีแบบนี้ก็จะต้องใส่ input เข้าไปใน run ด้วย แบบนี้ ก็จะสามารถรันออกมาได้
import subprocess
subprocess.run(['python','sanpo.py'],input='2',text=True)

ได้
0.18181818181818182

แต่ว่าถ้าหากโปรแกรมต้องการค่าป้อนเข้า ๒ ตัว เช่น
a = int(input())
b = int(input())
print('%d/%d = %s'%(a,b,a/b))

แบบนี้ก็ต้องใส่ค่าเข้าไป ๒ ตัว โดยใช้ \n คั่น
subprocess.run(['python','sanpo.py'],input='2\n3',text=True)

ได้
2/3 = 0.6666666666666666

ปัญหาคือการทำแบบนี้ทำให้ต้องเตรียมข้อมูลป้อนเข้าไว้ตั้งแต่แรกทั้งหมด ถ้ายังมีข้อมูลป้อนเข้าที่ต้องการอีกแต่ไม่มีเตรียมค่าป้อนเข้าไปแล้วก็จะไปต่อไม่ได้ทันที

และปกติแทนที่จะเตรียมข้อมูลป้อนเข้าไว้ทั้งหมดตั้งแต่แรก ปกติจะค่อยๆใส่ลงไปทีละตัวตามลำดับมากกว่า ซึ่งถ้าใช้ subprocess.Popen ก็จะสามารถทำแบบนั้นได้




การใช้ .Popen โดยป้อนค่าผ่าน .stdin

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

วิธีการใช้มีความซับซ้อนเล็กน้อย ขอเริ่มจากยกตัวอย่างการใช้

สมมุติมีโปรแกรม sanpo.py ซึ่งมีการใช้ input เพื่อรับค่า ๒ ตัว
a = int(input())
b = int(input())
print('%d×%d = %s'%(a,b,a*b))

เราสามารถเขียน subprocess.Popen เพื่อควบคุมได้ดังนี้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po.stdin.write('11\n') # ป้อนค่าตัวแรก
po.stdin.flush() # สิ้นสุดการป้อนตัวแรก
po.stdin.write('12\n') # ป้อนค่าตัวที่สอง
po.stdin.flush() # สิ้นสุดการป้อนตัวที่สอง
po.wait() # รอจนสิ้นสุดโปรแกรม

ได้
11×12 = 132

ในที่นี้เมื่อใช้ subprocess.Popen ก็จะได้ออบเจ็กต์ Popen มาเก็บไว้ในตัวแปร po จากนั้นก็นำมันมาใช้

ในที่นี้จำเป็นต้องระบุ stdin=subprocess.PIPE ลงไปด้วย แล้วจะทำให้ในออบเจ็กต์นี้จะมีตัวกลางสำหรับป้อนค่าเข้า คือ .stdin

วิธีการป้อนค่าจะคล้ายกับเวลาใช้ open เพื่อเปิดไฟล์มาในโหมดเขียน (เช่น open('ชื่อไฟล์','w')) นั่นคือจะใช้เมธอด .write() เพื่อเขียนค่าลงไปได้

ข้อความที่ป้อนเข้าไปต้องปิดท้ายด้วย \n คือแทนการกด enter ด้วย และหลังป้อนข้อมูลไปก็ใช้เมธอด .flush() เพื่อสิ้นสุดการป้อน

และสุดท้ายใช้ .wait() เพื่อให้รอจนสิ้นสุดการทำงานของคำสั่ง

อาจใช้โครงสร้าง with เพื่อให้คำสั่งรอจนสิ้นสุดเองโดยไม่ต้องใช้ .wait()
with subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True) as po:
    po.stdin.write('11\n')
    po.stdin.flush()
    po.stdin.write('12\n')
    po.stdin.flush()

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




การเอาผลลัพธ์จาก .stdout และ .stderr เมื่อใช้ .Popen

หากใส่คีย์เวิร์ด stdout=subprocess.PIPE แทนที่ผลลัพธ์จะแสดงออกมาทันทีก็จะเก็บไว้ในออบเจ็กต์ Popen โดยอยู่ใน .stdout

ซึ่ง .stdout นี้จะเป็นตัวกลางสำหรับดึงข้อมูลออกมา การใช้จะเหมือนกับเวลาใช้ open แล้วเปิดโหมดอ่านข้อมูล คือใช้เมธอด .read, .readline, readlines ได้

ตัวอย่างเช่น ถ้าใน sanpo.py เป็น

a = int(input())
print('2^%d = %s'%(a,2**a))

สั่งรันแล้วเอาผลลัพธ์มาแสดงได้โดยเขียนแบบนี้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
po.stdin.write('13\n')
po.stdin.flush()
print(po.stdout.read()) # ได้ 2^13 = 8192

เมื่ออ่านค่าจาก .stdout นั่นหมายความว่าคำสั่งจะต้องถูกทำจนจบและให้ผลลัพธ์ออกมา ดังนั้นไม่จำเป็นต้องใช้ with หรือ .wait()

ถ้าต้องการจะเอาผลลัพธ์ใส่ลงไฟล์ก็ใช้ open ได้เช่นเดียวกันเมื่อตอนใช้ subprocess.run
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=open('xxx.txt','w'),text=True)
po.stdin.write('15\n')
po.stdin.flush()

ผลลัพธ์ที่ผิดพลาดก็เช่นกัน เอาออกมาได้โดยใส่ stderr=subprocess.PIPE แล้วเอา .stderr
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
po.stdin.write('x\n')
po.stdin.flush()
print(po.stderr.read())

ก็จะได้ผลลัพธ์ความผิิดพลาดออกมา
Traceback (most recent call last):
  File "sanpo.py", line 1, in 
    a = int(input())
ValueError: invalid literal for int() with base 10: 'x'

หรือเอาผลลัพธ์ที่ผิดพลาดออกมาใส่ในส่วนของ .stdout ได้โดยใส่ stderr=subprocess.STDOUT
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
po.stdin.write('x\n')
po.stdin.flush()
print(po.stdout.read())




การใช้ผลลัพธ​์จากคำสั่งแรกมาป้อนให้อีกคำสั่งทันที

.stdout ที่ได้จากคำสั่งหนึ่งยังเอามาใช้เป็น stdin ของ subprocess.Popen ได้ด้วย

ตัวอย่างเช่น เตรียมโปรแกรม sanpo.py ซึ่งจะรับค่าตัวเลขมาแล้วเอามาคูณ 2
a = int(input())
print(a*2)

ลองสั่งคำสั่งต่อกัน ๒ คำสั่งด้วย subprocess.Popen โดยเอาคำตอบของคำสั่งแรกมาป้อนให้คำสั่งถัดไป
po1 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
po1.stdin.write('7\n')
po1.stdin.flush()

po2 = subprocess.Popen(['python','sanpo.py'],stdin=po1.stdout,stdout=subprocess.PIPE,text=True)
print(po2.stdout.read()) # ได้ 28

ค่าที่ป้อนเข้าไปตอนแรกคือ 7 เมื่อเข้าไปในคำสั่ง po1. จะให้ค่า 7×2 เท่ากับ 14 ออกมาทาง .stdout แล้วเมื่อใช้ค่านี้ป้อนให้ po2 ก็จะได้คำตอบออกมาเป็น 14×2 เท่ากับ 28




การป้อนค่าสุดท้ายโดยใช้ .communicate()

สามารถใช้เมธอด .communicate เพื่อทำการป้อนค่าครั้งสุดท้ายและเป็นการจบคำสั่งไปในตัว

สมมุติโค้ดใน sanpo.py เป็นแบบนี้
a = int(input())
b = int(input())
print('%d^%d = %s'%(a,b,a**b))

เนื่องจากมี input รับค่า ๒ ตัว ดังนั้นอาจใช้ .stdin.write ๒ ครั้ง หรืออาจใช้ .communicate เขียนแทนตัวหลังแบบนี้ได้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po.stdin.write('5\n')
po.communicate('4')

ได้
5^4 = 625

เมื่อใช้ .communicate จะได้ค่าคืนกลับคือทูเพิลของค่า (ผลลัพธ์ปกติ, ผลลัพธ์ที่ผิดพลาด)

ลองให้ stdout หรือ stderr เป็น subprocess.PIPE ก็จะได้ค่าผลลัพธ์ปกติและผลลัพธ์​ที่ผิดพลาดออกมา เช่น
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
po.stdin.write('5\n')
res = po.communicate('4')
print(res) # ได้ ('5^4 = 625\n', None)

po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
po.stdin.write('\n')
res = po.communicate('x')
print(res) # ได้ (None, 'Traceback (most recent call last):\n  File "sanpo.py", line 1, in \n    a = int(input())\nValueError: invalid literal for int() with base 10: \'\'\n')

po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
po.stdin.write('\n')
res = po.communicate('x')
print(res) # ได้ ('Traceback (most recent call last):\n  File "sanpo.py", line 1, in \n    a = int(input())\nValueError: invalid literal for int() with base 10: \'\'\n', None)

เช่นเดียวกับ .stdout การใช้ .communicate จะทำให้คำสั่งสิ้นสุดและปิดตัวลงอยู่แล้วจึงไม่จำเป็นต้องใช้คู่กับ with หรือปิดท้ายด้วย .wait()




การใช้ timeout เพื่อจำกัดเวลาใน .communicate()

.communicate สามารถกำหนดเวลาจำกัด timeout ได้เหมือนตอนใช้ subprocess.run ถ้าหลังป้อนค่าไปแล้วคำสั่งเสร็จไม่ทันตามเวลาที่กำหนดก็จะทำให้เกิดข้อผิดพลาด

ตัวอย่างให้ sanpo.py เป็นโปรแกรมที่ใช้ฟังก์ชัน time.sleep เผื่อถ่วงเวลาตามที่ระบุไป
import time
a = float(input())
time.sleep(a)
print('ผ่านไปแล้ว %f วินาที'%a)

ลองใช้ .communicate โดยใส่ timeout ได้ดังนี้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
res,_ = po.communicate('2',timeout=2.2)
print(res) # ได้ ผ่านไปแล้ว 2.000000 วินาที

po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
res,_ = po.communicate('2.5',timeout=2.2)
print(res) # ได้ TimeoutExpired: Command '['python', 'sanpo.py']' timed out after 2.2 seconds




เมื่อใช้ .Popen หลายตัวพร้อมกัน

เมื่อใช้ subprocess.Popen สั่งคอมมานด์ไลน์ไปแล้ว คำสั่งต่อไปในโค้ดก็จะทำงานทันทีโดยที่ไม่ได้สนว่าคำสั่งในคอมมานด์ไลน์ที่สั่งไปนั้นจะทำเสร็จหรือยัง ยกเว้นว่าจะใช้เมธอด .communicate() หรือดึงเอาผลลัพธ์มาด้วย .stdout.read() ซึ่งจะเป็นการรอจนได้ผลออกมา

นั่นหมายความว่าถ้าใช้ subprocess.Popen ต่อๆกันหลายอันในโปรแกรมเดียว โปรแกรมไม่จำเป็นต้องรอให้ subprocess.Popen ตัวแรกเสร็จจึงจะทำตัวต่อไป สามารถทำคู่ขนานไปพร้อมๆกันได้เลย และตัวที่สั่งไปก่อนก็อาจจะเสร็จทีหลังก็ได้

เช่น ลองเขียน sanpo.py ให้กินเวลาตามค่าที่ใส่เข้าไป แบบนี้
import time
a = int(input())
time.sleep(a)
print('เวลาล่วงไป %s วินาที'%a)

จากนั้นลองสั่งรันโปรแกรมนี้พร้อมกันหลายๆตัวดู
import subprocess,time

po3 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po3.stdin.write('3\n')
po3.stdin.flush()

po2 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po2.stdin.write('2\n')
po2.stdin.flush()

po1 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po1.stdin.write('1\n')
po1.stdin.flush()

time.sleep(4) # รอไปสัก ๔ วินาที ให้ทำเสร็จทั้งหมดก่อน

แบบนี้ผลที่ได้จะออกมาตามลำดับดังนี้
เวลาล่วงไป 1 วินาที
เวลาล่วงไป 2 วินาที
เวลาล่วงไป 3 วินาที

จะเห็นว่าแม้ว่า po3 จะสั่งก่อน แต่มันต้องใช้เวลา ๓ วินาที จึงออกมาลำดับท้ายสุด ในขณะที่ po1 สั่งทีหลังสุด แต่ใช้เวลาแค่ ๑ วินาที จึงเสร็จก่อน

อาจใช้ for วนซ้ำเพื่อรันคำสั่งที่ใช้เวลาต่างกัน
tt = [2,6,5,1]
for t in tt:
    po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
    po.stdin.write('%d\n'%t)
    po.stdin.flush()

time.sleep(max(tt)+1) # รอจนทำเสร็จหมด

ก็จะได้ผลออกมาตามลำดับเวลาดังนี้
เวลาล่วงไป 1 วินาที
เวลาล่วงไป 2 วินาที
เวลาล่วงไป 5 วินาที
เวลาล่วงไป 6 วินาที




การตรวจสอบสถานะการรันโดยใช้ .poll()

จากหัวข้อที่แล้วจะเห็นว่าเมื่อใช้ subprocess.Popen ไปแล้วคำสั่งต่อไปจะทำงานโดยไม่รอให้คำสั่งนั้นทำเสร็จ

แบบนั้นแล้วหากคำสั่งต่อไปขึ้นกับคำสั่งที่สั่งก่อนหน้า ผลของคำสั่งต่อไปที่ได้ก็อาจต่างกันไปโดยขึ้นอยู่กับว่าคำสั่งในคอมมานด์ไลน์ไปถึงไหนแล้ว บางครั้งจึงจำเป็นต้องตรวจดูว่าคำสั่งนั้นทำเสร็จไปหรือยัง

ถ้าต้องการจะดูว่าคำสั่งนั้นทำเสร็จสิ้นหรือยังสามารถใช้เมธอด .poll() ถ้าหากว่ายังไม่เสร็จจะได้ค่า None แต่ถ้าเสร็จแล้วก็จะคืนค่า returncode มา (ถ้าไม่เกิดข้อผิดพลาดจะได้ 0)

ตัวอย่าง ลองเตรียมไฟล์ sanpo.py ให้มีการใช้เวลา ๒ วินาทีดังนี้
import time
time.sleep(2)

ลองใช้ subprocess.Popen แล้วใช้ .poll() แล้วดูค่าที่ได้ในจังหวะต่างๆ
import subprocess,time

po = subprocess.Popen(['python','sanpo.py'])
time.sleep(1) # รอสัก 1 วินาที
print(po.poll()) # ได้ None
time.sleep(3) # รอต่ออีก 3 วินาที
print(po.poll()) # ได้ 0

สำหรับ .poll() อันแรกนั้นเพิ่งผ่านไป ๑ วินาที ยังรันไม่เสร็จ จึงได้ None แต่ .poll() ตัวหลังเกินเวลามาแล้วจึงได้ returncode ออกมา ในที่นี้ไม่มีข้อผิดพลาดอะไรจึงให้ค่า 0

อาจใช้ .poll() เป็นเงื่อนไขใน if หรือ while เพื่อให้โปรแกรมเลือกทำโดยดูว่าคำสั่งรันเสร็จหรือยัง เช่น
po = subprocess.Popen(['python','sanpo.py'])
t0 = time.time()
while(po.poll()==None):
    print(f'ผ่านไปแล้ว {time.time()-t0} วินาที')
    time.sleep(0.3)

แบบนี้ก็จะวนซ้ำไปเรื่อยๆใช้เวลารอบละ ๐.๓ วินาที จนกว่าจะเกิน ๒ นาที ได้ผลออกมาดังนี้
ผ่านไปแล้ว 4.696846008300781e-05 วินาที
ผ่านไปแล้ว 0.30647993087768555 วินาที
ผ่านไปแล้ว 0.6083638668060303 วินาที
ผ่านไปแล้ว 0.912254810333252 วินาที
ผ่านไปแล้ว 1.215419054031372 วินาที
ผ่านไปแล้ว 1.5203897953033447 วินาที
ผ่านไปแล้ว 1.8238389492034912 วินาที



ทั้งหมดนี้เป็นวิธีการใช้ subprocess.Popen เพื่อสั่งคำสั่งไปในคอมมานด์ไลน์จากภายในโปรแกรมไพธอน อาจมีความยุ่งยากสักหน่อย ต้องตั้งใจทำความเข้าใจให้ดี




-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python
-- คอมพิวเตอร์ >> shell

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

目录

从日本来的名言
模块
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
机器学习
-- 神经网络
javascript
蒙古语
语言学
maya
概率论
与日本相关的日记
与中国相关的日记
-- 与北京相关的日记
-- 与香港相关的日记
-- 与澳门相关的日记
与台湾相关的日记
与北欧相关的日记
与其他国家相关的日记
qiita
其他日志

按类别分日志



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  查看日志

  推荐日志

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ