Python3.5+ subprocess用法概括以及使用场景分析

Python是一门很给力的语言,可以完成想要的所有操作,但在极少数情况下,你可能需要调用外部程序。比如Linux命令或者运行Shell脚本。以前写Python脚本的时候,我最排斥的就是调用外部命令,可能因为出了错误不好处理,要么纯用Shell脚本,要么纯用Python脚本,“杂交算什么东西”! 那我现在的想法呢,也很纠结,看下去吧。以前代码最常见的是使用Python的原始方法是使用** os.system**,但是后来被subprocess模块替代,也是这篇文章所要讲的。

1. 侃侃run()用法

我只用**subprocess.run()**,其他的都是垃圾?!

Python中关于多进程协程异步等一些较新用法,官方一直在更新完善其用法,然后趋于稳定。subprocess模块旨在替换几个较旧的模块和功能,比如**os.systemos.spawn*。而对于Python3.5 +** **subprocess.run()**用来代替之前的call等几个老方法,万剑归一。

1.1 示例1:运行命令并获取返回代码

run()的行为与call()的行为大致相同,Python3.5+之后应该放弃使用call(),但是call()方法还是保留着,可以使用

1
2
3
4
5
import subprocess

cp = subprocess.run(["ls","-lha"])
cp
# CompletedProcess(args=['ls', '-lha'], returncode=0)

1.2 示例2:运行命令,如果底层进程报错,强制抛出异常

加入check=True即可

1
2
3
4
5
6
7
8
9
10
11
12
13
import subprocess

subprocess.run(["ls","foo bar"], check=True)
# -------------------------------------------------------------------
# CalledProcessError Traceback (most recent call last)
# ----> 1 subprocess.run(["ls","foo bar"], check=True)
# /usr/lib/python3.6/subprocess.py in run(input, timeout, check, *popenargs, **kwargs)
# 416 if check and retcode:
# 417 raise CalledProcessError(retcode, process.args,
# --> 418 output=stdout, stderr=stderr)
# 419 return CompletedProcess(process.args, retcode, stdout, stderr)
# 420
# CalledProcessError: Command '['ls', 'foo bar']' returned non-zero exit status 2.

1.3 示例3:流畅使用shell

如果想要正常使用shell命令,只要加入shell=True

1
2
3
4
5
import subprocess

cp = subprocess.run("ls -lha",shell=True)
cp
# CompletedProcess(args='ls -lha', returncode=0)

如果使用用户输入作为参数来构建命令字符串,加上shell = True的话可能有潜在的安全威胁(代码注入)

1.4 示例4:以字符串形式存储输出和错误消息

如果底层进程返回非零退出代码,则不会出现异常;可以通过CompletedProcess对象中的stderr属性访问错误消息。

不报错的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import subprocess

cp = subprocess.run(["ls","-lha"], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

cp.stdout
# total 20K
# drwxrwxr-x 3 felipe felipe 4,0K Nov 4 15:28 .
# drwxrwxr-x 39 felipe felipe 4,0K Nov 3 18:31 ..
# drwxrwxr-x 2 felipe felipe 4,0K Nov 3 19:32 .ipynb_checkpoints
# -rw-rw-r-- 1 felipe felipe 5,5K Nov 4 15:28 main.ipynb
cp.stderr
# '' (empty string)
cp.returncode
# 0

报错的例子:

1
2
3
4
5
6
7
8
9
10
import subprocess

cp = subprocess.run(["ls","foo bar"], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

cp.output
# '' (empty string)
cp.stderr
# ls: cannot access 'foo bar': No such file or directory
cp.returncode
# 2

命令不存在的例子:

1
2
3
4
5
6
7
import subprocess

try:
cp = subprocess.run(["xxxx","foo bar"], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except FileNotFoundError as e:
print(e)
# [Errno 2] No such file or directory: 'xxxx'

2. 更底层的Popen用法

关于调用子进程,官方的推荐方法是用可以处理所有用例的run(函数。对于更高级的用例,可以直接使用底层的Popen接口。但这个方法在我看来用不太到(个人拙见),上面的run能满足99%的场景,理由我会在下面的使用场景中说出。

这里推荐几篇好的文章,它们都在讲解subprocess的时候讲了popen方法:
1. Python 3 Subprocess Examples
2. subprocess — Spawning Additional Processes
3. How to Execute Shell Commands with Python

3. 使用场景分析与选择

我表达下我个人拙见。
Python能做很多事,大多数脚本都能胜任。但是对于Linux本身的操作,像是重启服务命令,一般系统特有命令,或者是Linux强大的三剑客(sed,awk,grep)在处理文本时有很大的优势,这些Python脚本都不好去代替。

  1. 对于一些单条的管道命令,Python中最精确的是用Popen命令,但是我们完全可以用类似于run(‘grep python | wc > out’, shell=True)来代替,虽然上面说这不安全,但是写在脚本内部,不把命令作为一个参数输入,是没有问题的。而且写之前肯定是要进行测试的,所以我上面才觉得底层的popen必要性不大。
  2. 对于一些麻烦的命令集合,类似于部署脚本、驱动类、开启关闭脚本,我们可以采用写shell脚本,把相关的一部分用shell脚本来代替,然后如果要在Python中调用的话只要**run('./xx.sh',shell=True)**(或者调皮一点bash中前嵌入运行python脚本),我不知道这优不优雅,有空去查查。

4. 总结

如何在Python中运行外部命令,最有效的方法是使用子进程模块及其提供的所有功能。最值得注意的是,应该考虑使用subprocess.run。
还有其他有用的库支持Python中的shell命令,拓展了一些其他功能,比如与tty终端交互,获取系统CPU信息等,如plumbum,sh,psutils和pexpect,这里不做拓展。