【翻译】Python Subprocess:运行外部命令

翻译
Python Subprocess: Run External Commands

尽管 PyPI 上有不少库,但有时你须要在 Python 代码中运行一个外部命令。内置的 Python subprocess 模块使之相对容易。在这篇文章中,你将学习一些关于进程和子进程的基本知识。
html

咱们将使用 Python subprocess 模块来安全地执行外部命令,获取输出,并有选择地向它们提供来自标准输入的输入。 若是你熟悉进程和子进程的理论,你能够跳过第一部分。python

进程和子进程

一个在计算机上执行的程序也被称为一个进程。但究竟什么是进程?让咱们更正式地定义它。
shell

进程
进程是一个计算机程序的实例,由一个或多个线程执行。

一个进程能够有多个线程,这被称为多线程。反过来,一台计算机能够同时运行多个进程。这些进程能够是不一样的程序,但它们也能够是同一个程序的多个实例。在咱们关于 Python并发性 的文章中,对此有很是详细的解释。下面的图片也来自那篇文章:

数据库

若是你想运行一个外部命令,这意味着你须要从你的 Python 进程中建立一个新的进程。这样的进程一般被称为子进程或 sub-process 。从视觉上看,这就是一个进程产生两个子进程的状况:

segmentfault

在内部(操做系统内核内部)发生的是所谓的 fork。进程本身 fork,意味着该进程的一个新副本被建立和启动。若是你想使你的代码并行化,并利用你机器上的多个CPU,这多是有用的。这就是咱们所说的多进程
数组

不过,咱们能够利用相同的技术来启动另外一个进程。首先,进程 fork 本身,建立一个副本。该副本将自身替换为另外一个进程:你但愿执行的进程。
安全

咱们能够采用低级别的方式,使用 Python subprocess 模块来完成这些工做,但幸运的是,Python 还提供了一个包装器,能够处理全部细节,而且这样作也很安全。多亏了包装器,运行外部命令只须要调用一个函数。这个封装器就是 subprocess 库中的函数 run(),这就是咱们将在本文中使用的。
多线程

我认为让你知道内部发生了什么会很好,但若是你感到困惑,请放心,你不须要这些知识就能作到你想要的:用 Python subprocess 模块运行外部命令。并发

使用 subprocess.run 建立一个 Python subprocess

理论讲得够多了,如今是时候动手写一些代码来执行外部命令了。
函数

首先,您须要导入 subprocess 库。 因为它是 Python 3 的一部分,所以你无需单独安装它。 在这个库中,咱们将使用 run 命令。 这个命令是在 Python 3.5 中添加的。 确保你至少有这个 Python 版本,但最好是运行最新版本。 若是你须要帮助,请查看咱们详细的 Python 安装说明

让咱们从对 ls 的简单调用开始,列出当前目录和文件:

>>> import subprocess
>>> subprocess.run(['ls', '-al'])

(a list of your directories will be printed)

事实上,咱们能够从咱们的 Python 代码中调用 Python 二进制文件 。 接下来让咱们获取系统上默认安装的 python 3 版本:

>>> import subprocess
>>> result = subprocess.run(['python3', '--version'])
Python 3.8.5
>>> result
CompletedProcess(args=['python3', '--version'], returncode=0)

逐行解释:

  • 咱们导入 subprocess 库
  • 运行一个 subprocess ,在这里是 python3 二进制文件,有一个参数:--version
  • 查看 result 变量,它的类型是 CompletedProcess

该进程返回代码 0,表示它执行成功。 任何其余返回码都意味着存在某种错误。 这取决于你调用的进程定义的不一样返回代码的含义。

正如你在输出中看到的,Python 二进制文件将其版本号打印在标准输出上,这一般是你的终端。你的结果可能不一样,由于你的 Python 版本可能不一样。也许,你甚至会获得一个看起来像这样的错误。FileNotFoundError: [Errno 2] No such file or directory: 'python3'。在这种状况下,请确保 python3 的 Python 二进制文件在你的系统上,而且也在PATH中。

捕获 Python subprocess 的输出

若是你运行一个外部命令,你极可能想捕获该命令的输出。咱们能够经过 capture_output=True 选项实现这一目的:

>>> import subprocess
>>> result = subprocess.run(['python3', '--version'], capture_output=True, encoding='UTF-8')
>>> result
CompletedProcess(args=['python3', '--version'], returncode=0, stdout='Python 3.8.5\n', stderr='')

正如你所看到的,Python 此次没有把它的版本打印到咱们的终端。subprocess.run 命令重定向了标准输出和标准错误流,因此能够捕获它们并为咱们存储在 result 中 。查看 result 变量,咱们看到 Python 的版本是从标准输出中捕获的。因为没有错误,stderr是空的。

我还添加了 encoding='UTF-8' 选项。若是你不这样作,subprocess.run 会认为输出是一个字节流,由于它没有这个信息。你能够试试。结果是,stdout 和 stderr 将是字节数组。所以,若是你知道输出将是 ASCII文本或 UTF-8 文本,你最好指定它,以便运行函数对捕获的输出也进行相应编码。

另外,你也可使用选项 text=True 而不指定编码。Python 将把输出做为文本捕获。若是你知道编码,我建议明确指定它。

从标准输入输入数据

若是外部命令指望在标准输入上得到数据,咱们也能够经过 Python 的 subprocess.run 函数的 input 选项来轻松实现。请注意,我不会在这里讨论流数据。在这里咱们将创建在前面的例子上:

>>> import subprocess
>>> code = """
... for i in range(1, 3):
...   print(f"Hello world {i}")
... """

>>> result = subprocess.run(['python3'], input=code, capture_output=True, encoding='UTF-8')
>>> print(result.stdout)
>>> print(result.stdout)
Hello world 1
Hello world 2

咱们只是用 Python3 二进制文件来执行一些 Python 代码。彻底无用,但 (但愿) 很是有指导意义!

code 变量是一个多行的 Python 字符串,咱们用 input 选项将其做为输入分配给 subprocess.run 命令。

运行 shell 命令

若是你想在类 Unix 系统上执行 shell 命令,我指的是你一般会在相似 Bash 的 shell 中输入的任何命令,你须要意识到,这些命令一般不是执行的外部二进制文件。例如,像 for 和 while 循环这样的表达式,或者管道和其它操做符,是由 shell 自己解释的。

Python 经常之内置库的形式提供替代方案,你应该更喜欢这些方案。可是若是你须要执行一个 shell 命令,不论是什么缘由,当你使用 shell=True 选项时,subprocess.run 会很乐意这样作。它容许你输入命令,就像你在一个与 Bash 兼容的 shell 中输入同样:

>>> import subprocess
>>> result = subprocess.run(['ls -al | head -n 1'], shell=True)
total 396
>>> result
CompletedProcess(args=['ls -al | head -n 1'], returncode=0)

但有一个警告:使用这种方法容易受到命令注入攻击(见:注意事项)。

须要注意的事项

运行外部命令并不是没有风险。请很是仔细地阅读本节。

os.system vs subprocess.run

你可能会看到 os.system() 用于执行命令的代码示例。 不过,subprocess 模块更增强大,官方 Python 文档推荐使用它而不是 os.system()。os.system 的另外一个问题是,它更容易被注入命令。

命令注入

一种常见的攻击或漏洞,是注入额外的命令来得到对计算机系统的控制。 例如,若是你要求你的用户输入并在调用 os.system() 或调用 subprocess.run(...., shell=True) 时使用这些输入,你就有可能受到命令注入攻击。

为了演示,下面的代码容许咱们运行任何 shell 命令。

import subprocess
thedir = input()
result = subprocess.run([f'ls -al {thedir}'], shell=True)

由于咱们直接使用了用户的输入,用户只需在其后面加上分号,就能够运行任何命令。例如,下面的输入将列出/目录并回显一个文本。本身试试吧。

/; echo "command injection worked!";

解决方案不是尝试清理用户的输入。你可能很想开始寻找分号,并在发现分号时拒绝输入。不要这样作;黑客们在这种状况下至少能想到5种其余的追加命令的方法。这是一场艰苦的战斗。

更好的解决办法是不使用shell=True,而是像咱们在前面的例子中那样在一个列表中输入命令。像这样的输入在这种状况下会失败,由于 subprocess 模块会肯定输入是你正在执行的程序的参数,而不是一个新的命令。

使用一样的输入,但 shell=False,你会获得下面的结果。

import subprocess
thedir = input()
>>> result = subprocess.run([f'ls -al {thedir}'], shell=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/subprocess.py", line 489, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'ls -al /; echo "command injection worked!";'

该命令被看成 ls 的一个参数,而 ls 则告诉咱们,它找不到那个文件或目录。

用户输入老是危险的

事实上,使用用户输入老是危险的,不只仅是由于命令注入。例如,假设你容许用户输入一个文件名。以后,咱们读取该文件并将其显示给用户。虽然这看起来无害,但用户能够输入这样的内容:.../.../.../configuration/settings.yaml。

其中 settings.yaml 可能包含你的数据库密码......哎呀! 你老是须要对用户输入进行适当的清理和检查。不过,如何正确地作到这一点,已经超出了本文的范围。

继续学习

如下相关资源将帮助你更深刻地研究这个主题:

相关文章
相关标签/搜索