当前位置: 首页 > 后端技术 > Python

【译文】PythonSubprocess-RunExternalCommands

时间:2023-03-25 21:38:42 Python

TranslatePythonSubprocess:RunExternalCommands虽然PyPI上有很多库,但有时你需要在Python代码中运行一个外部命令。内置的Python子进程模块使这变得相对容易。在本文中,您将了解有关流程和子流程的一些基础知识。我们将使用Pythonsubprocess模块来安全地执行外部命令、获取输出,并有选择地为它们提供来自标准输入的输入。如果熟悉进程和子进程的理论,可以跳过第一部分。进程和子进程在计算机上执行的程序也称为进程。但究竟什么是过程?让我们更正式地定义它。进程进程是由一个或多个线程执行的计算机程序的实例。一个进程可以有多个线程,称为多线程。反过来,一台计算机可以同时运行多个进程。这些进程可以是不同的程序,但也可以是同一程序的多个实例。这在我们关于Python并发性的文章中有非常详细的解释。下图也来自那篇文章:如果你想运行一个外部命令,这意味着你需要从你的Python进程中创建一个新进程。此类流程通常称为子流程或子流程。从视觉上看,这是当一个进程产生两个子进程时发生的情况:内部(在操作系统内核内部)发生的是所谓的fork。进程分叉自身,这意味着创建并启动了进程的新副本。如果您想并行化代码并利用机器上的多个CPU,这会很有用。这就是我们所说的多处理。但是,我们可以利用相同的技术来启动另一个进程。首先,进程会自己分叉,创建一个副本。副本将自己替换为另一个进程:您希望执行的进程。我们可以使用Python子进程模块以低级方式执行此操作,但幸运的是,Python还提供了一个包装器来处理所有细节并安全地进行处理。多亏了包装器,运行外部命令只需要一个函数调用。这个包装器是subprocess库中的函数run(),这就是我们将在本文中使用的。我认为您最好知道内部发生了什么,但是如果您感到困惑,请不要担心,您不需要这些知识来执行您想要的操作:使用Python子进程模块运行外部命令。了解了使用subprocess.run创建Python子进程的理论,现在是时候动手编写一些代码来执行外部命令了。首先,您需要导入子流程库。由于它是Python3的一部分,因此您不需要单独安装它。在这个库中,我们将使用运行命令。此命令是在Python3.5中添加的。确保你至少有这个版本的Python,但最好运行最新版本。如果您需要帮助,请查看我们详细的Python安装说明。让我们从对ls的简单调用开始,以列出当前目录和文件:>>>importsubprocess>>>subprocess.run(['ls','-al'])(将打印您的目录列表)事实上,我们可以从我们的Python代码中调用Python二进制文件。接下来让我们获取系统默认安装的python3版本:>>>importsubprocess>>>result=subprocess.run(['python3','--version'])Python3.8.5>>>resultCompletedProcess(args=['python3','--version'],returncode=0)逐行解释:我们导入子进程库来运行一个子进程,这里是python3的二进制文件,有个参数:--versionto查看result变量,其Type为CompletedProcess进程返回code0,表示执行成功。任何其他返回代码都意味着存在某种错误。这取决于您调用的进程定义的不同返回码的含义。正如您在输出中看到的那样,Python二进制文件在标准输出(通常是您的终端)上打印其版本号。您的结果可能会有所不同,因为您的Python版本可能不同。也许,您甚至会收到如下所示的错误。FileNotFoundError:[Errno2]没有这样的文件或目录:'python3'。在这种情况下,请确保python3的Python二进制文件在您的系统上并且也在PATH中。捕获Python子进程的输出如果您运行外部命令,您很可能希望捕获该命令的输出。我们可以使用capture_output=True选项来实现:>>>importsubprocess>>>result=subprocess.run(['python3','--version'],capture_output=True,encoding='UTF-8')>>>resultCompletedProcess(args=['python3','--version'],returncode=0,stdout='Python3.8.5\n',stderr='')可以看到,Python这次没有打印它的版本到我们的终端。subprocess.run命令重定向标准输出和标准错误流,因此它们被捕获并存储在结果中。查看结果变量,我们看到Python的版本是从标准输出中捕获的。由于没有错误,stderr为空。我还添加了encoding='UTF-8'选项。如果你不这样做,subprocess.run会认为输出是一个字节流,因为它没有这个信息。你可以试试。因此,stdout和stderr将是字节数组。因此,如果您知道输出将是ASCII文本或UTF-8文本,您最好指定它,以便运行函数相应地对捕获的输出进行编码。或者,您也可以在不指定编码的情况下使用选项text=True。Python会将输出捕获为文本。如果您知道编码,我建议明确指定它。从标准输入输入数据如果外部命令需要标准输入的数据,我们也可以通过Python的subprocess.run函数的输入选项轻松实现。请注意,我不会在这里讨论流数据。这里我们将在前面的示例的基础上构建:>>>importsubprocess>>>code="""...foriinrange(1,3):...print(f"Helloworld{i}")。..""">>>result=subprocess.run(['python3'],input=code,capture_output=True,encoding='UTF-8')>>>print(result.stdout)>>>print(result.stdout)Helloworld1Helloworld2我们只是使用Python3二进制文件来执行一些Python代码。完全没用,但(希望)很有启发性!代码变量是一个多行Python字符串,我们将其作为输入分配给带有输入选项的subprocess.run命令。运行shell命令如果你想在类Unix系统上执行shell命令,我指的是你通常在类Bashshell中输入的任何命令,你需要知道这些命令通常不是执行的外部二进制文件.例如,for和while循环、管道和其他运算符等表达式由shell本身解释。Python通常以内置库的形式提供替代方案,您应该更喜欢。但是,如果您需要执行shell命令,无论出于何种原因,当您使用shell=True选项时,subprocess.run将很乐意这样做。它允许你输入命令,就好像你在Bash兼容的shell中输入它们一样:>>>importsubprocess>>>result=subprocess.run(['ls-al|head-n1'],shell=True)总计396>>>resultCompletedProcess(args=['ls-al|head-n1'],returncode=0)但有一个警告:使用此方法容易受到命令注入攻击(参见:Caveats)。注意事项运行外部命令并非没有风险。请仔细阅读本节。os.systemvssubprocess.run您可能会看到代码示例,其中os.system()用于执行命令。然而,subprocess模块更强大,官方Python文档建议使用它而不是os.system()。os.system的另一个问题是它更容易注入命令。命令注入一种常见的攻击或漏洞是注入额外的命令以获得对计算机系统的控制。例如,如果您要求用户输入并在调用os.system()或调用subprocess.run(....,shell=True)时使用该输入,则您很容易受到命令注入攻击。为了演示,下面的代码允许我们运行任何shell命令。importsubprocessthedir=input()result=subprocess.run([f'ls-al{thedir}'],shell=True)因为我们直接使用用户的输入,用户只需要在它后面加一个分号就可以了Anycommand可以运行。例如,下面的输入将列出/目录并回显文本。自己试试吧。/;echo"commandinjectionworked!";解决方案是不要尝试清理用户的输入。您可能会开始寻找分号,并在找到分号时拒绝输入。不要这样做;在这种情况下,黑客可以想出至少5种其他方法来附加命令。这是一场艰苦的战斗。更好的解决方案是不使用shell=True,而是像我们在前面的示例中那样在列表中输入命令。在这种情况下,这样的输入将失败,因为subprocess模块将确定输入是您正在执行的程序的参数,而不是新命令。使用相同的输入,但shell=False,您将得到以下结果。importsubprocessthedir=input()>>>result=subprocess.run([f'ls-al{thedir}'],shell=False)Traceback(最近调用最后):文件“”,第1行,在文件“/usr/lib/python3.8/subprocess.py”,第489行,以Popen(*popenargs,**kwargs)作为进程运行:文件“/usr/lib/python3.8/subprocess.py”,第854行,在__init__self._execute_child(args,可执行文件,preexec_fn,close_fds,文件“/usr/lib/python3.8/subprocess.py”,第1702行,在_execute_childraisechild_exception_type(errno_num,err_msg,err_filename)FileNotFoundError:[Errno2]Nosuchfileordirectory:'ls-al/;echo"commandinjectionworked!";'该命令用作ls的参数,ls告诉我们找不到该文件或目录.Userinputalwaysdangerous事实上,使用用户输入总是危险的,不仅仅是因为命令注入。例如,假设你允许用户输入一个文件名。之后,我们读取文件并将其显示给用户。虽然这可能看起来无害,用户可以输入如下内容:.../.../.../configuration/settings.yaml。其中settings.yaml可能包含您的数据库密码......哎呀!您始终需要正确清理和检查用户输入。但是,如何正确执行此操作超出了本文的范围。继续研究以下相关资源,它们将帮助您更深入地研究该主题:官方文档包含有关子进程库的所有详细信息我们关于Python并发的文章解释了更多关于进程和线程的信息我们关于使用Unixshell的部分可能会发送有用的学习一些基本的Unix命令