在Python中运行shell命令并读取输出
我有以下内容:
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 个回答
在命令行中,有一个命令是很难用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()
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 中不可用,所以你得使用 Popen
和 communicate()
:
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()
注意,如果你不关心标准错误输出,可以不指定它。这样它会继承当前进程的标准错误输出。
你需要对原始命令的每一部分使用一次 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 的文档有详细的讨论。