Python 可信手拈来写系统脚本,那么在 Python 中调用系统命令应该会比较便捷。所以本文来看看 Python 有几种方式调用系统命令,以及与回味一下其他几种脚本语言的类似操作。简单说来,Python 执行系统命令的方式有四种方式,即
- os.system(cmd) (建议用 subprocess 模块)
- os.popen(cmd) (Python 3 中还能用,但不推荐使用了)
commands 模块(在 Python 3 中已移除了该模块,基本是不必去了解它)- subprocess 模块(总是上面的矛头全指向它的,重点)
os.system(cmd)
启动一个子进程来执行系统命令,可以获得标准输入,不能获到命令输出, 但可以得到一个状态码
1 2 3 |
import os status_code = os.system('cat a.py | grep username') # username = input("Username:") |
它是调用 C 函数 system(), 命令直接输出到终端,它返回的错误状态码与直接执行命令的值可能不一样
os.popen(cmd)
建立一个到命令的管道,可以捕获到命令(shell) 执行后的输出
1 2 3 4 |
import os out = os.popen("cat a.json | grep id") print(out.read()) |
不知如何得到状态码,不建议使用了,推荐用 subprocess 模块
subprocess 模块
从 os.system(cmd) 和 os.popen(cmd) 的官方文档都引导我们去使用 subprocess 模块
关于使用 subprocess 模块,这儿有个很好的学习视频, 隔着墙的朋友们请忍着些日子。
关于 subprocess 模块的用法基本就是讲述 subprocess.run(...)
函数的用法,了解几个重要的参数。该方法的原型为
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)
subprocess.run()
是在 Python 3.5 加入的,之前的版本要调用 subprocess.call()
方法。
基本用法
1 2 3 4 |
import subprocess p1 = subprocess.run('ls') # 这行与终端执行 ls 效果一样,直接输出 ls 的结果 print('****\n', p1, p1.stdout) |
scratch.py
scratch_1.py
****
CompletedProcess(args='ls', returncode=0) None
我们看到 p1 的类型是一个 CompletedProcess
, 此时 p1.stdout
的内容是 None
,也就是说我们没有捕获到命令的执行输出
捕获命令输出
1 2 |
p1 = subprocess.run('ls', capture_output=True) print('****\n', type(p1.stdout), p1.stdout) # p1.stdout.decode() 显示文本 |
加了 capture_output=True 参数,命令的执行结果不直接打印到控制台,而是可以通过 p1.stdout
获得的
****
<class 'bytes'> b'scratch.py\nscratch_1.py\n'
capture_output=True
实质上会设置两个参数值 stdout=subprocess.PIPE, stderr=subprocess.PIPE
,也就是它同时也会捕获标准错误输出,放置到 p1.stderr
中。如果只需捕获标准输出的话,只要 stdout=subprocess.PIPE
.
默认时 p1.stdout
是一个bytes
类型,要显示为文本需要用 p1.stdout.decode()
,或者再加上参数 text=True
就无需 stdout.decode()
了。用 encoding="utf-8"
也能免除 decode()
。
1 2 |
p1 = subprocess.run('ls', capture_output=True, text=True) print('****\n', type(p1.stdout), '\n', p1.stdout) |
****
<class 'str'>
scratch.py
scratch_1.py
加了 text=True
后,p1.stdout
的类型也变成了字符串 str
。
命令参数与 shell 命令
前面的命令 ls
没有带参数,那怎么执行带参数的命令呢?先尝试着把命令与参数写在一块
1 |
subprocess.run('ls -la') |
出错了
FileNotFoundError: [Errno 2] No such file or directory: 'ls -la': 'ls -la'
subprocess.run() 并不知道 ls -la
中的 ls
是命令,其余为参数,而是把整个字符串包括中间的空间当作命令,所以提示找不到 ls -la
这样的命令。解决方法有一,加上 shell=True
参数
1 |
subprocess.run('ls -la', shell=True) |
total 16
drwxr-xr-x 4 yanbin 2022363315 128 Aug 12 21:17 .
drwxr-xr-x 11 yanbin 2022363315 352 Aug 12 20:49 ..
-rw-r--r-- 1 yanbin 2022363315 933 Aug 12 09:59 scratch.py
-rw-r--r-- 1 yanbin 2022363315 56 Aug 12 21:17 scratch_1.py
有了 shell=True
的话,run()
的第一个参数可以写的更复杂,管道操作也行,如下
1 |
subprocess.run('ls -la; pwd && ls | grep scratch', shell=True) |
但 shell=True
不那么安全,比如说对于 ls
命令,它后面的目录部分是用户输入的话就可被注入
1 |
subprocess.run(f'ls -la ${folder}', shell=True) |
假如用户输入的 folder="; rm -rf /
, 就能随意的干活了。当 shell=True
是可以借助于 shlex.quote()
函数来抵御不安全的输入。
为安全起见,我们可保持默认的 shell=False
值,命令参数则必须以列表形式提供
1 |
subprocess.run(['ls', '-la', folder]) |
列表第一个元素为命令,其他为参数,这时候 folder
能够防御住攻击了。
输出重定向(文件或设备)
1 2 |
with open('output.txt', 'w') as file: subprocess.run(['ls', '-la'], stdout=file, text=True) |
现在命令的输出进到文件 output.txt
中了。要让标准错误输出也重定向到文件的话,就加上 stderr=file
,或者 stdout=file, stderr=subprocess.STDOUT
。这一连贯的操作就好比执行如下的 shell 命令
1 2 3 4 5 6 |
with open('output.txt', 'w') as file: p1 = subprocess.run(['ls', '-la', 'lala'], stdout=file, stderr=subprocess.STDOUT text=True) print(p1.returncode) # 上面的代码效果与这个 shell 命令差不多, 2 为标准错误输出,1 为标准输出 ls -la lala> output.txt 2>&1 |
由于 lala
目录不存在,所以 p1.returncode
为 1
, 非零即为有错误,看看 output.txt
中的内容为
ls: abc: No such file or directory
如果想忽略错误输出,例如一个程序中既有标准输出又有标准错误输出,且返回状态码为 2,用下面 Python 程序来模拟
1 2 3 4 5 6 7 |
#!/usr/bin/env python3 import sys print('hello stdout') print('hello stderr', file=sys.stderr) exit(2) |
想要执行出下方第四行中命令的效果
1 2 3 4 5 6 7 8 |
$ ./test.py hello stdout hello stderr $ ./test.py > output.txt 2> /dev/null $ echo $? 2 $ cat output.txt hello stdout |
相应的 subprocess.run()
代码如下
1 2 3 4 |
with open('output.txt', 'w') as file: p1 = subprocess.run('./test.py', stdout=file, text=True, stderr=subprocess.DEVNULL) print(p1.returncode) # 这时候的 p1.stderr 就是 None |
报告错误
如果我们捕获了输出后,subprocess.run()
执行命令出错时不会报告给 Python 解释器
1 |
p1 = subprocess.run(['ls', '-la', 'lala'], capture_output=True, text=True) |
lala
目录不存在,但上面的代码会没有任何输出,我们只得在执行后检查 p1
的返回码及 p1.stderr
的值
1 2 |
if p1.returncode != 0: print(p1.stderr) |
输出
ls: lala: No such file or directory
要是加上 check=True
参数就不一样的了
1 |
subprocess.run(['ls', '-la', 'lala'], capture_output=True, text=True, check=True) |
直接报告异常
Traceback (most recent call last):
File "/Users/yanbin/Library/Preferences/PyCharmCE2019.1/scratches/scratch_1.py", line 4, in <module>
p1 = subprocess.run(['ls', '-la', 'lala'], capture_output=True, text=True, check=True)
File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 487, in run
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['ls', '-la', 'lala']' returned non-zero exit status 1.
这对于希望在有任何命令执行异常时退出很有用
管道操作
前面提到过,用 subprocess.run()
设置 shell=True
时,命令中可以随意使用复杂的 shell 命令,包括管道操作。如果 shell=False
默认值时,管道操作就需要显式的执行多个 subprocess.run()
操作并且通过像 input=p1.stdout
这样串联起来,这原本就是管道的意义
1 2 3 |
p1 = subprocess.run(['cat', 'a.json'], capture_output=True, text=True) p2 = subprocess.run(['grep', 'id'], capture_output=True, text=True, input=p1.stdout) print(p2.stdout) |
相当于执行了 shell 命令
1 |
cat a.json | grep id |
其他
encoding 和 errors 参数是 Python 3.6 加入的。encoding='utf-8'
与 text=True
有着类似的功效
text, universal_newlines 和 capture_output 是 Python 3.7 加入的,之前版本捕获输出就要用 stdout=subprocess.PIPE
, 且得到的 stdout 是 bytes
类型,stderr 相类似。
timeout=<in seconds> 会在多少秒之后还没执行完命令后得到 TimeoutExpired
异常,并且终止掉该命令的执行
其他脚本语言参考
学习过了 Python 调用系统命令之后不妨顺道体验一下 Perl, Groovy, Scala 怎么操作的
Perl 执行系统命令
1 2 3 4 5 6 7 |
$out = `ls -l | grep http`; # 捕获输出到 $out 变量,不会输出到控制台 print $out; $code = system('ls -l | grep http'); # 直接输出到控制台,返回值只是一个状态码 print $code; exec('ls -l | grep http'); # 直接输出到控制台,没有返回值 |
Perl 执行的是 shell 命令,所以直接支持管道操作
Groovy 执行系统命令
1 2 3 4 5 |
def p1 = 'ls -l'.execute().text //不能直接执行 'ls -l | grep http' print(p1) def p2 = 'ls -l'.execute() | 'grep http'.execute() //管道操作 print(p2.text) |
Scala 执行系统命令
1 2 3 4 5 6 7 8 |
import scala.sys.process._ import scala.language.postfixOps val code = "ls -l" ! //直接输出到控制台,返回值为状态码 val out = "ls -l" !! //返回值为命令的输出 val code = "ls -l" #| "grep http" ! //管道操作 val out1 = "ls -l" #| "grep http" !! //管道操作 |
管道操作要用 #|
操作符,不能在一个字符串中用管道连接多个命令,这不是一个 shell
本文链接 https://yanbin.blog/python-execute-system-command/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。