如何使用子进程在Python3中运行外部程序

来自菜鸟教程
跳转至:导航、​搜索

作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

Python 3 包含 子进程模块 用于运行外部程序并在 Python 代码中读取它们的输出。

如果您想在 Python 代码中使用计算机上的另一个程序,您可能会发现 subprocess 很有用。 例如,您可能希望从 Python 代码中调用 git 来检索项目中在 git 版本控制中跟踪的文件。 由于您可以在计算机上访问的任何程序都可以由 subprocess 控制,因此此处显示的示例将适用于您可能希望从 Python 代码调用的任何外部程序。

subprocess 包含多个类和函数,但在本教程中,我们将介绍 subprocess 最有用的函数之一:subprocess.run。 我们将回顾它的不同用途和主要关键字参数。

先决条件

为了充分利用本教程,建议您熟悉 Python 3 中的编程。 您可以查看这些教程以获取必要的背景信息:

运行外部程序

您可以使用 subprocess.run 函数从 Python 代码运行外部程序。 不过,首先,您需要将 subprocesssys 模块导入您的程序:

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "print('ocean')"])

如果你运行它,你将收到如下输出:

Outputocean

让我们回顾一下这个例子:

  • sys.executable 是您的程序最初被调用的 Python 可执行文件的绝对路径。 例如,sys.executable 可能是类似 /usr/local/bin/python 的路径。
  • subprocess.run 给出了一个字符串列表,其中包含我们尝试运行的命令的组件。 由于我们传递的第一个字符串是 sys.executable,我们正在指示 subprocess.run 执行一个新的 Python 程序。
  • -c 组件是一个 python 命令行选项,它允许您将字符串传递给整个 Python 程序以执行。 在我们的例子中,我们传递了一个打印字符串 ocean 的程序。

您可以将列表中我们传递给 subprocess.run 的每个条目视为由空格分隔。 例如,[sys.executable, "-c", "print('ocean')"] 大致转换为 /usr/local/bin/python -c "print('ocean')"。 请注意,subprocess 会在尝试在底层操作系统上运行命令组件之前自动引用它们,例如,您可以传递包含空格的文件名。

警告: 切勿将不受信任的输入传递给 subprocess.run。 由于 subprocess.run 具有在您的计算机上执行任意命令的能力,恶意行为者可以使用它以意想不到的方式操纵您的计算机。


从外部程序捕获输出

现在我们可以使用 subprocess.run 调用外部程序,让我们看看如何捕获该程序的输出。 例如,如果我们想使用 git ls-files 输出当前存储在版本控制下的所有文件,此过程可能很有用。

注意: 本节中的示例需要 Python 3.7 或更高版本。 特别是,在 2018 年 6 月发布的 Python 3.7 中添加了 capture_outputtext 关键字参数。


让我们添加到前面的示例:

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "print('ocean')"], capture_output=True, text=True
)
print("stdout:", result.stdout)
print("stderr:", result.stderr)

如果我们运行此代码,我们将收到如下输出:

Outputstdout: ocean

stderr:

这个例子与第一节介绍的例子大体相同:我们仍在运行一个子进程来打印 ocean。 然而,重要的是,我们将 capture_output=Truetext=True 关键字参数传递给 subprocess.run

subprocess.run 返回绑定到 resultsubprocess.CompletedProcess 对象。 subprocess.CompletedProcess 对象包括有关外部程序退出代码及其输出的详细信息。 capture_output=True 确保 result.stdoutresult.stderr 填入外部程序的相应输出。 默认情况下,result.stdoutresult.stderr 绑定为字节,但 text=True 关键字参数指示 Python 将字节解码为字符串。

在输出部分,stdoutocean(加上 print 隐式添加的尾随换行符),我们没有 stderr

让我们尝试一个为 stderr 生成非空值的示例:

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "raise ValueError('oops')"], capture_output=True, text=True
)
print("stdout:", result.stdout)
print("stderr:", result.stderr)

如果我们运行此代码,我们会收到如下输出:

Outputstdout:
stderr: Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: oops

此代码运行一个 Python 子进程,该子进程立即引发 ValueError。 当我们检查最终的 result 时,我们在 stdoutstderr 中的 ValueErrorTraceback 中什么也看不到。 这是因为默认情况下 Python 会将未处理异常的 Traceback 写入 stderr

在错误的退出代码上引发异常

有时,如果我们运行的程序以错误的退出代码退出,则引发异常很有用。 以零代码退出的程序被认为是成功的,但以非零代码退出的程序被认为遇到了错误。 例如,如果我们想在实际上不是 git 存储库的目录中运行 git ls-files 时引发异常,则此模式可能很有用。

如果外部程序返回非零退出代码,我们可以使用 subprocess.runcheck=True 关键字参数来引发异常:

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "raise ValueError('oops')"], check=True)

如果我们运行此代码,我们会收到如下输出:

OutputTraceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: oops
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/subprocess.py", line 512, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['/usr/local/bin/python', '-c', "raise ValueError('oops')"]' returned non-zero exit status 1.

此输出显示我们运行了一个引发错误的子进程,该错误在终端的 stderr 中打印。 然后 subprocess.run 在我们的主要 Python 程序中代表我们尽职尽责地提出了一个 subprocess.CalledProcessError

或者,subprocess 模块还包括 subprocess.CompletedProcess.check_returncode 方法,我们可以调用它来达到类似的效果:

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "raise ValueError('oops')"])
result.check_returncode()

如果我们运行此代码,我们将收到:

OutputTraceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: oops
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/subprocess.py", line 444, in check_returncode
    raise CalledProcessError(self.returncode, self.args, self.stdout,
subprocess.CalledProcessError: Command '['/usr/local/bin/python', '-c', "raise ValueError('oops')"]' returned non-zero exit status 1.

由于我们没有将 check=True 传递给 subprocess.run,因此我们成功地将 subprocess.CompletedProcess 实例绑定到 result,即使我们的程序以非零代码退出。 但是,调用 result.check_returncode() 会引发 subprocess.CalledProcessError,因为它检测到已完成的进程以错误代码退出。

使用超时提前退出程序

subprocess.run 包括 timeout 参数,以允许您在执行时间过长时停止外部程序:

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "import time; time.sleep(2)"], timeout=1)

如果我们运行此代码,我们将收到如下输出:

OutputTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/subprocess.py", line 491, in run
    stdout, stderr = process.communicate(input, timeout=timeout)
  File "/usr/local/lib/python3.8/subprocess.py", line 1024, in communicate
    stdout, stderr = self._communicate(input, endtime, timeout)
  File "/usr/local/lib/python3.8/subprocess.py", line 1892, in _communicate
    self.wait(timeout=self._remaining_time(endtime))
  File "/usr/local/lib/python3.8/subprocess.py", line 1079, in wait
    return self._wait(timeout=timeout)
  File "/usr/local/lib/python3.8/subprocess.py", line 1796, in _wait
    raise TimeoutExpired(self.args, timeout)
subprocess.TimeoutExpired: Command '['/usr/local/bin/python', '-c', 'import time; time.sleep(2)']' timed out after 0.9997982999999522 seconds

我们尝试运行的子进程使用 time.sleep 函数 休眠 2 秒。 但是,我们将 timeout=1 关键字参数传递给 subprocess.run 以在 1 秒后使我们的子进程超时。 这就解释了为什么我们对 subprocess.run 的调用最终会引发 subprocess.TimeoutExpired 异常。

请注意,subprocess.runtimeout 关键字参数是近似的。 Python 将尽最大努力在 timeout 秒数后终止子进程,但不一定准确。

将输入传递给程序

有时程序期望输入通过 stdin 传递给它们。

subprocess.runinput 关键字参数允许您将数据传递给子进程的 stdin。 例如:

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "import sys; print(sys.stdin.read())"], input=b"underwater"
)

运行此代码后,我们将收到如下输出:

Outputunderwater

在这种情况下,我们将字节 underwater 传递给 input。 我们的目标子进程使用 sys.stdin 读取传入的 stdin (underwater) 并将其打印到我们的输出中。

如果要将多个 subprocess.run 调用链接在一起,将一个程序的输出作为输入传递给另一个程序,则 input 关键字参数可能很有用。

结论

subprocess 模块是 Python 标准库的一个强大部分,可让您运行外部程序并轻松检查其输出。 在本教程中,您学习了使用 subprocess.run 来控制外部程序、将输入传递给它们、解析它们的输出并检查它们的返回码。

subprocess 模块公开了我们在本教程中没有介绍的其他类和实用程序。 现在您有了基线,您可以使用 子流程模块的文档 了解更多关于其他可用类和实用程序的信息。