在Python中运行shell命令并读取输出

2 投票
3 回答
2594 浏览
提问于 2025-04-18 13:31

我有以下内容:

cmd = "ps aux | grep 'java -jar' | grep -v grep | awk '//{print $2}'".split(' ')
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
print out

当我在控制台(不在python里面)运行这个命令时,能得到我想要的结果。但是在python中运行上面的代码却只打印出一个空行。我在想可能是cmd(特别是|这个操作符)出了问题,但我不太确定。

我需要在标准的Python 2.6.6安装环境下实现这个功能(不使用额外的模块)。

3 个回答

1

在命令行中,有一个命令是很难用Python代码直接替代的。你可以启动几个外部程序,并把它们的输入和输出连接起来,但我建议你直接运行 ps aux 命令,然后加一些Python代码来过滤和提取你想要的数据:

from subprocess import PIPE, Popen


def main():
    process = Popen(['ps', 'aux'], stdout=PIPE)
    pids = [
        line.split(None, 2)[1] for line in process.stdout if 'java -jar' in line
    ]
    process.wait()
    print '\n'.join(pids)


if __name__ == '__main__':
    main()
3

Popen 默认只执行可执行文件,而不是命令行指令。当你传递参数列表给 Popen 时,它们应该是调用一个可执行文件及其参数:

import subprocess

proc = subprocess.Popen(['ps', 'aux'])

另外要注意的是,你不应该使用 str.split 来拆分命令,因为:

>>> "ps aux | grep 'java -jar'     | grep -v grep | awk '//{print $2}'".split(' ')
['ps', 'aux', '|', 'grep', "'java", "-jar'", '', '', '', '', '|', 'grep', '-v', 'grep', '|', 'awk', "'//{print", "$2}'"]

注意以下几点:

  • 被引号包裹的参数(例如 'java -jar')会被拆分。
  • 如果有多个连续的空格,你会得到一些空的参数。

Python 已经提供了一个模块,可以合理地拆分命令行,这个模块叫做 shlex

>>> shlex.split("ps aux | grep 'java -jar'     | grep -v grep | awk '//{print $2}'")
['ps', 'aux', '|', 'grep', 'java -jar', '|', 'grep', '-v', 'grep', '|', 'awk', '//{print $2}']

注意被引号包裹的参数是被保留的,而多个空格也得到了妥善处理。不过你仍然不能将结果传递给 Popen,因为 Popen 默认不会| 解释为管道。

如果你想运行一个命令行(也就是使用任何 shell 特性,比如管道、路径扩展、重定向等),你必须传递 shell=True。在这种情况下,你不应该将字符串列表作为参数传递给 Popen,而是只传递一个完整的命令行字符串:

proc = subprocess.Popen("ps aux | grep 'java -jar' | grep -v grep | awk '//{print $2}'", shell=True)

如果你在 shell=True 的情况下传递一个字符串列表,它的意思就不同了:第一个元素应该是完整的命令行,而其他元素则作为选项传递给使用的 shell。例如在我的机器上,默认的 shell (sh) 有一个 -x 选项,可以在 stderr 上显示所有执行的进程:

>>> from subprocess import Popen
>>> proc = Popen(['ps aux | grep python3', '-x'], shell=True)
>>> 
username   7301  0.1  0.1  39440  7408 pts/9    S+   12:57   0:00 python3
username   7302  0.0  0.0   4444   640 pts/9    S+   12:58   0:00 /bin/sh -c ps aux | grep python3 -x
username   7304  0.0  0.0  15968   904 pts/9    S+   12:58   0:00 grep python3

在这里你可以看到启动了一个 /bin/sh,它执行了命令 ps aux | python3,并带有 -x 选项。

(这些内容在 Popen 的文档中都有说明。)


说到这里,达到你想要的效果的一种方法是使用 subprocess.check_output

subprocess.check_output("ps aux | grep 'java -jar' | grep -v grep | awk '//{print $2}'", shell=True)

不过这个在 python<2.7 中不可用,所以你得使用 Popencommunicate()

proc = subprocess.Popen("ps aux | grep 'java -jar' | grep -v grep | awk '//{print $2}'", shell=True, stdout=subprocess.PIPE)
out, err = proc.communicate()

另一种选择是避免使用 shell=True(这通常是一个非常好的选择,因为 shell=True 会带来一些安全风险),而是手动使用多个进程来编写管道:

from subprocess import Popen, PIPE

ps = Popen(['ps', 'aux'], stdout=PIPE)
grep_java = Popen(['grep', 'java -jar'], stdin=ps.stdout, stdout=PIPE)
grep_grep = Popen(['grep', '-v', 'grep'], stdin=grep_java.stdout, stdout=PIPE)
awk = Popen(['awk', '//{print $2}'], stdin=grep_grep.stdout, stdout=PIPE)
out, err = awk.communicate()

grep_grep.wait()
grep_java.wait()
ps.wait()

注意,如果你不关心标准错误输出,可以不指定它。这样它会继承当前进程的标准错误输出。

3

你需要对原始命令的每一部分使用一次 Popen(),这些部分通过管道连接起来,就像下面这样:

import subprocess

p1 = subprocess.Popen(["ps", "aux"], stdout=subprocess.PIPE,  stderr=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "java -jar"], stdin=p1.stdout, stdout=subprocess.PIPE,  stderr=subprocess.PIPE)
p3 = subprocess.Popen(["grep", "-v", "grep"], stdin=p2.stdout, stdout=subprocess.PIPE,  stderr=subprocess.PIPE)
p4 = subprocess.Popen(["awk", "//{print $2}"], stdin=p3.stdout, stdout=subprocess.PIPE,  stderr=subprocess.PIPE)
out, err = p4.communicate()

print out

关于这个内容,subprocess 的文档有详细的讨论。

撰写回答