[求助] Python中subprocess的stdout堵塞问题如何解决?

要求是这样的:
1 ) 登入 mysql 服务器( mysql -h localhost -uroot -p1234 )。
2 ) 输入 mysql 内部命令 show databases,如果返回的内容出现 mysql (存在 mysql DB )就立刻强制退出整个 python 程序。
(关于要求 2 的解释,假如 show databases 的返回内容是 information_schema \r\n mysql \r\n test。不要等到 test 出现,马上就退出或者杀死该程序)

我认为只有用 subprocess 能够比较好的完成以上功能,所以以下都是以使用 subprocess 为前提。

个人试了好多方法,都不成功。
方法 1:把 stdout 放到线程里


def stdout_theard(p_stdout):
time.sleep(0.01)
for i in range(3000):
print p_stdout.readline()

s_command = ‘mysql -h localhost -uroot -p1234’
sub_process = subprocess.Popen(command , stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True)
thread_read_output = threading.Thread(target=stdout_theard, args=(sub_process.stdout,))
thread_read_output.setDaemon(‘True’)
thread_read_output.start()

sub_process.stdin.write(‘show databases;\r\n’)

方法 2:把 stdout 重定向到文件里
s_command = ‘mysql -h localhost -uroot -p1234’
f_out = tempfile.TemporaryFile(mode=‘w+’)
f_err = tempfile.TemporaryFile(mode=‘w+’)
sub_process = subprocess.Popen(command , stdin = subprocess.PIPE, stdout = f_out, stderr = f_err, shell = True)

或者:

os.dup2(sub_process.stdout.fileno(), f_out.fileno())

以上方法都没成功。
希望前辈高手们指点。

另,以上只是用 mysql 打了个比方,实际的环境不太好说。
必须得调用一个 exe ( cisco anyConnect ),得到返回值。但登陆这个 exe 的时候,一旦给定的用户名密码错误,它会一直用这组错误的用户名密码试,直到该用户被锁死。
退出的原因就是,在第一次用户名密码错误时,就退出,避免 exe 反复试,导致锁死。
[求助] Python中subprocess的stdout堵塞问题如何解决?


27 回复

没成功具体是什么问题?


subprocess.Popen 时,如果子进程输出数据量很大,stdout 管道满了就会卡住。关键是要及时把数据读出来。

最直接的办法是用 communicate(),它会自动处理读写,适合一次性获取所有输出:

import subprocess

result = subprocess.run(['ls', '-la'], 
                       capture_output=True, 
                       text=True)
print(result.stdout)

如果输出是流式的,或者需要实时处理,就得开线程来读:

import subprocess
import threading

def read_output(pipe, func):
    for line in iter(pipe.readline, ''):
        func(line.strip())
    pipe.close()

proc = subprocess.Popen(['ping', '-c', '5', 'localhost'],
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       text=True)

t = threading.Thread(target=read_output, 
                    args=(proc.stdout, print))
t.start()
proc.wait()
t.join()

还有个更简单的办法,直接把输出重定向到文件或者 subprocess.DEVNULL,这样就不会堵管道了。

总结:及时读取管道数据是关键。

放到线程里仍旧是阻塞。
重定向到文件里,写不进去。文件总是空,只有强制退出( Ctrl+C )后,才能写到文件里,看来也是阻塞的问题。

大牛,或者说有没有成功的经验。很可能我的写法也有问题。

经过测试你需要 添加一个 -n 在 mysql 的命令行里
它默认开启了缓冲

我用 Perl 6 来测试。。
my $p = Proc::Async.new(<mysql -n -P3306 -u ovirt -pdefault>, :w);
$p.stdout.tap(&say);
$p.stderr.tap(&say);
my $pp = $p.start;
await $p.put(“show databases;\r\n”);
say “WAITING OVER”;
await $pp;

输出
Database
information_schema
mysql
performance_schema


linux pipe size 大小 512B*8= 4096Bytes,Pipe 满了就会阻塞。
处理方法
1 )即时取出 stdout,边读边写入文件,适用输出无穷大
2 ) Pipe.commucate(),读入内存,适用输出小的情况

因为 python 的 subprocess 的 stdin,stdout 有阻塞的问题,所以我不会处理。
perl 没有阻塞的问题么?这到是个好消息,我可以用 perl 试试。

你看了我说的话?我是说 mysql 默认开启了输出的缓冲,加上 -n 关掉估计就可以了。。

谢谢回复,学到了。

所谓“及时取出 stdout,边读边写”,的意思是 p.stdout.flush() 么?
好像抓到点什么了?

stdout,stderr = Pipe.commucate() 是不是等到进程结束才输出?这个在我的程序里不适用。不能等到结束,还要有后续的动作。(事实上是用 cisco anyConnect 建立连接之后,测试上网)

边读边写:
while 1:
line = p.stdout.readline()
write(line)

前边没仔细看你的需求,如果仅仅是 show databases 的输出,output 多大,会引起 pipe 阻塞?

前面多余,你用一条 mysql 命令搞好了

mysql -u -p -e
此帖终结

缓冲 IO 的问题。PIPE 和普通文件默认都是全缓冲的,缓冲区没满就不会进行实际 IO,所以读不到数据。
两个办法:
1. mysql 加参数,让它强制冲洗缓冲区
2. 使用伪终端(pty),它默认是行缓冲的

分享篇博客 深入理解子进程 : http://www.kkblog.me/notes/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%AD%90%E8%BF%9B%E7%A8%8B

大神留步。
我改了一下我的代码。试了,不打印。求大神给看看。

def stdout_theard(stdout_lock, p_stdout):
for i in range(3000):
s = p_stdout.readline()
if len(s)>0:
print s # 这里没有打印
time.sleep(0.01)

if name == “main”:
os.chdir(‘C:\Program Files (x86)\MySQL\MySQL Server 5.0\bin’)
s_mian_command = ‘mysql -h localhost -uroot -p1234’
l_command = [‘show databases;’, ‘use mysql;’, ‘show tables;’]

sub_process = subprocess.Popen(s_mian_command,
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
shell = True)

stdout_lock = threading.Lock()
thread_read_output = threading.Thread(target=stdout_theard,
args=(stdout_lock,sub_process.stdout))
thread_read_output.setDaemon(‘True’)
thread_read_output.start()

for s_command in l_command:
time.sleep(1)
sub_process.stdin.write(s_command + ‘\r\n’)
print s_command # 这里是打印的

打印的结果就是:
show databases;
use mysql;
show tables;

密码啥的都没问题。后台也确确实实执行了的(我用 PowerCmd 能看到后台是执行了的),就是没打印。
黑人问号黑人问号。

为什么没有 sub_process.p.communicate()

感谢大家一直在帮助我,怎奈我天资有限,一直没有解决。
out,err = sub_process.communicate()的意思是线程结束后才返回 stdout 和 stderr 吧?
我的程序不能停止后才返回,还有后续的动作。

…真是不可救药了,我都说了是 mysql 的问题,跟你的用法没有关系。。
这就如同 程序本身没有输出 你还能 capture 到输出?

这里排版不太好,我在 OSC 写了段 https://my.oschina.net/u/3573498/blog/1524999

mysql 不能这样执行吗? mysql -h localhost -uroot -p1234 -e “show databases”

谢谢,按你说的确实是有输出。赞啊~~

接下来就是我的问题了,我以为用 mysql,大家都有这个环境,就能很好的说明这个问题。看来大能果然不能糊弄。
我实际的问题和 mysql 类似,是叫“ cisco anyConnect ” 的一个程序( vpncli.exe )。
流程是差不多,通过 vpncli.exe 登录⇒建立 VPN 连接⇒返回状态⇒访问测试页面⇒退出 vpncli.exe 。
所以 mysql -n 参数我是学到了,但 vpncli.exe 没有-n 参数。


那这个没有你说的那个选项的话不好说, 你可以考虑上面楼层说的伪终端
不过看你是 win 下,具体情况那就不清楚具体支持不支持 WIN 不了
不过还有一个办法就是你去和 cmd/shell 交互,而不是和你的应用程序交互。。

再次感谢。
客户那边限制,只能使用 win。

>>> 不过还有一个办法就是你去和 cmd/shell 交互,而不是和你的应用程序交互
这是个思路。

感谢,那篇文章让我受益良多。

不要用 readline 老老实实 read

还有 不要只顾 stdout 有些软件不标准
普通错误都往 stderr 里塞

回到顶部