翻译
Python Subprocess: Run External Commands
只管 PyPI 上有很多库,但有时你须要在 Python 代码中运行一个外部命令。内置的 Python subprocess 模块使之绝对容易。在这篇文章中,你将学习一些对于过程和子过程的基本知识。
咱们将应用 Python subprocess 模块来平安地执行外部命令,获取输入,并有选择地向它们提供来自规范输出的输出。如果你相熟过程和子过程的实践,你能够跳过第一局部。
过程和子过程
一个在计算机上执行的程序也被称为一个过程。但到底什么是过程?让咱们更正式地定义它。
过程
过程是一个计算机程序的实例,由一个或多个线程执行。
一个过程能够有多个线程,这被称为多线程。反过来,一台计算机能够同时运行多个过程。这些过程能够是不同的程序,但它们也能够是同一个程序的多个实例。在咱们对于 Python 并发性 的文章中,对此有十分具体的解释。上面的图片也来自那篇文章:
如果你想运行一个外部命令,这意味着你须要从你的 Python 过程中创立一个新的过程。这样的过程通常被称为子过程或 sub-process。从视觉上看,这就是一个过程产生两个子过程的状况:
在外部(操作系统内核外部)产生的是所谓的 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 可能蕴含你的数据库明码 …… 哎呀! 你总是须要对用户输出进行适当的清理和查看。不过,如何正确地做到这一点,曾经超出了本文的范畴。
持续学习
以下相干资源将帮忙你更深刻地钻研这个主题:
- 官网文档 中有对于 subprocess 库的所有细节
- 咱们对于 Python 并发 的文章解释了对于过程和线程的更多信息
- 咱们对于 应用 Unix shell 的局部可能会派上用场
- 学习一些 根本的 Unix 命令