Python subprocess调用Shell脚本时控制台无法实时打印输出的问题
嘿,我完全懂你遇到的这个坑!我之前用subprocess调用批量脚本的时候也碰到过一模一样的情况——明明子进程在跑,但控制台就是没输出,非得等所有进程都跑完才一次性把内容吐出来,哪怕加了set -x或者手动读管道都没用。
先把你的场景再理清楚:
- 你的Python调用脚本(
test.py)是这样写的:import subprocess sub = subprocess.Popen(["sh", "test.sh"]) sub.wait() - 你的
test.sh里会调用多个Python脚本,但这些子进程的输出不会实时显示在控制台。 - 你试过在
test.sh里加set -x,或者改成下面的Python代码,结果还是所有输出一次性打印:import subprocess import sys sub = subprocess.Popen(["sh", "test.sh"], shell=False, stdout=subprocess.PIPE ,stderr=subprocess.PIPE) while sub.poll() == None or sub.poll() != 0: print(sub.stdout.readline()) sub.stdout.flush() sys.stdout.flush()
问题根源:输出缓冲
其实这是因为系统的输出缓冲机制在搞鬼:当进程的输出不是直接连接到终端(比如被subprocess的管道捕获,或者脚本里的子进程输出到非终端环境),系统会启用「块缓冲」——也就是攒够一大块数据才会输出,而不是有一行就输出一行。哪怕你手动flush,也绕不开底层的缓冲设置。
几个可行的解决方案
1. 让脚本里的Python子进程禁用缓冲
最简单的办法就是给test.sh里调用的每个Python命令加上-u参数,这个参数会让Python禁用标准输出的缓冲,强制行输出:
# 在test.sh里改成这样调用Python脚本 python -u script1.py python -u script2.py
或者更省心一点,在test.sh的开头加一行,全局设置Python无缓冲:
export PYTHONUNBUFFERED=1 # 后面正常调用Python脚本就行 python script1.py python script2.py
2. 让subprocess直接继承当前终端的输出
如果你不需要在Python里处理子进程的输出,只是想让它实时显示在控制台,那完全不用搞管道,直接让subprocess继承当前进程的stdout和stderr就行:
import subprocess import sys sub = subprocess.Popen(["sh", "test.sh"], stdout=sys.stdout, stderr=sys.stderr) sub.wait()
Python 3.5+的话,用subprocess.run更简洁,默认就是继承输出:
import subprocess subprocess.run(["sh", "test.sh"])
这样子进程的输出会直接连到终端,系统会自动用行缓冲,就能实时看到输出了。
3. 必须用管道读取?用线程避免死锁+实时读取
如果你的需求是要在Python里处理子进程的输出,那之前的循环逻辑有问题——当sub.poll() !=0的时候进程已经结束了,这时候readline可能会卡住,而且只处理stdout会忽略stderr,容易导致死锁。
可以用线程分别读取stdout和stderr,这样能实时拿到每一行输出:
import subprocess import sys from threading import Thread def read_stream(stream, print_func): # 迭代读取每一行,直到流结束 for line in iter(stream.readline, ''): print_func(line.strip()) sys.stdout.flush() # text=True让输出直接是字符串,不用手动解码 sub = subprocess.Popen(["sh", "test.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # 启动线程分别处理stdout和stderr stdout_thread = Thread(target=read_stream, args=(sub.stdout, print)) # 把stderr输出到标准错误流 stderr_thread = Thread(target=read_stream, args=(sub.stderr, lambda x: print(x, file=sys.stderr))) stdout_thread.start() stderr_thread.start() # 等待线程结束 stdout_thread.join() stderr_thread.join() # 等待子进程结束 sub.wait()
这个方法用线程分开处理两个流,避免了因为其中一个流满了导致进程阻塞的问题,同时能实时输出每一行内容。
内容的提问来源于stack exchange,提问作者MIEE IEEE

