在 Python 中启动外部进程
认为将您的 Python 程序连接到 UNIX shell 很复杂?再想想!
在之前的文章中,我研究了 Python 中通过线程实现的并发(请参阅“并发思考:现代网络应用程序如何处理多个连接”和“Python 中的线程”)。线程的好处是它们相对容易使用,并且可以让您在线程之间共享数据,而不会遇到太多麻烦。坏消息是,如果您不小心,您可能会遇到严重的问题——因为数据没有共享,而且 Python 数据结构不是线程安全的。但也许更大的问题是 Python 的全局解释器锁 (GIL) 保证一次只有一个线程运行。
在许多情况下,这实际上不是问题。特别是,如果您正在编写与文件系统或网络交互的程序,您可能不会太感受到 Python 线程的痛苦。这是因为,虽然一次只有一个线程运行,但每当线程使用 I/O 时,它都会放弃对 CPU 的控制。这是因为磁盘和网络比 CPU 慢得多;当您等待文件系统为您提供您请求的数据时,另一个线程可以正在运行。
也就是说,确实有一些时候 Python 的线程会显示出其局限性。特别是,如果您正在编写 CPU 密集型代码——也就是说,CPU 是瓶颈——您会发现线程是有限的。毕竟,如果您有一台不错的 48 核机器可以玩,难道只让其中一个核心实际做一些事情看起来很傻吗?
当然,有一个解决方案可以解决这些问题——许多传统的 UNIX 用户认为在许多情况下,这个方案更优越:进程。与其在新线程中运行函数,不如在新进程中运行它!
因此,在本文中,我初步了解如何在 Python 中使用进程来完成一项非常常见的任务:调用外部命令。在这样做时,我还将介绍如何构建进程的工作方式,从而引出我的下一篇文章的主题:“multiprocessing”模块。
进程基础知识对于 Linux 用户来说,没有什么比进程更基本和日常的了。当我启动 Emacs 时,我启动了一个进程。当我启动 Apache HTTP 服务器时,我启动了一个进程,然后它启动了多个额外的进程。当我在命令行调用 ls
时,我正在启动一个进程。当我告诉我的电脑关机时,它通过杀死每个进程来完成关机。
将进程视为一个数据结构,它代表了计算机在特定时刻的状态。一个进程有正在运行的代码(包括尚未运行的代码);它有程序工作的数据;它可以访问内存来存储和检索额外的数据,并且它可以与外部设备对话,从文件系统和网络到键盘和屏幕。
一台 Linux 机器可以同时运行许多许多进程。在进程运行的短暂瞬间,它会产生完全控制计算机的错觉。这要归功于现代计算机速度如此之快,以至于您可以运行如此多的进程,并且它们都看起来是并发运行的。没错,现代计算机有多个 CPU(也称为“核心”),这使您可以将工作分配到这些核心之间。
在 Python 中有很多种启动进程的方法。在现代版本的语言中,您可以使用“subprocess”模块来启动进程,甚至检索结果。例如,您可以在新进程中调用 ls
程序,然后查看结果
>>> subprocess.check_output('ls')
从这个函数中,您会得到一个包含 ls
命令输出的字符串。这是一个非常难看的字符串,特别是如果您习惯于看到整齐打印的东西。在这种情况下,您不想查看返回的字符串,而是要打印它。问题是,这似乎不起作用,至少在 Python 3 中不起作用
>>> print(subprocess.check_output('ls'))
问题在于,默认情况下,subprocess.check_output
返回一个“字节串”,类似于 Python 2 字符串,因为它包含一个字节序列,而不是一个 Unicode 字符序列。这里的问题是,当您打印字节串时,Python 实际上并没有在新行看到 \n
时换行。
您可以通过告诉 Python 自由地解释换行符并返回字符串而不是字节串来解决这个问题
>>> print(subprocess.check_output('ls', universal_newlines=True))
这似乎效果很好。但是,如果您只想打印当前目录中文件的子集呢?很自然地会想说,例如,ls -l
。让我们试试看
>>> print(subprocess.check_output('ls -l', universal_newlines=True))
当您这样做时,您会得到
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python3/3.6.2/Frameworks/
↪Python.framework/Versions/3.6/lib/python3.6/subprocess.py",
↪line 336, in check_output
**kwargs).stdout
File "/usr/local/Cellar/python3/3.6.2/Frameworks/
↪Python.framework/Versions/3.6/lib/python3.6/subprocess.py",
↪line 403, in run
with Popen(*popenargs, **kwargs) as process:
File "/usr/local/Cellar/python3/3.6.2/Frameworks/
↪Python.framework/Versions/3.6/lib/python3.6/subprocess.py",
↪line 707, in __init__
restore_signals, start_new_session)
File "/usr/local/Cellar/python3/3.6.2/Frameworks/
↪Python.framework/Versions/3.6/lib/python3.6/subprocess.py",
↪line 1333, in _execute_child
raise child_exception_type(errno_num, err_msg)
FileNotFoundError: [Errno 2] No such file or directory: 'ls -l'
这里有什么问题?很简单,Python 正在尝试运行一个外部进程,并给它 Linux 命令 ls -l
。您可能会认为这是正常且合理的,因为运行 ls -l
是您在日常生活中很可能经常做的事情。但是请记住,ls
是命令,而 -l
是该命令的标志。您可以理解其中的区别,并且 shell 通常会为您分隔它们。但是,如果您只是将该命令名称交给 Linux,它会感到困惑并抱怨。
因此,您需要传递字符串列表,而不是传递单个字符串,其中每个字符串代表命令的“单词”。例如
>>> print(subprocess.check_output(['ls', '-l'], universal_newlines=True))
这可以正常工作。您可以添加其他参数,包括文件名
>>> print(subprocess.check_output(['ls', '-l', 'urls.txt'],
>>> universal_newlines=True))
如果您想获取所有“.txt”文件的长列表呢?只需尝试这样做
>>> print(subprocess.check_output(['ls', '-l', *.txt'],
↪universal_newlines=True))
>>> print(subprocess.check_output(['ls', '-l', '*.txt'],
>>> universal_newlines=True))
ls: cannot access '*.txt': No such file or directory
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python3/3.6.2/Frameworks/
↪Python.framework/Versions/3.6/lib/python3.6/subprocess.py",
↪line 336, in check_output
**kwargs).stdout
File "/usr/local/Cellar/python3/3.6.2/Frameworks/
↪Python.framework/Versions/3.6/lib/python3.6/subprocess.py",
↪line 418, in run
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['ls', '-l', '*.txt']'
↪returned non-zero exit status 2.
它抱怨说“*.txt”不是合法的文件。这是因为,虽然您可能认为 Linux 总是知道 * 代表目录中的所有文件,但事实并非如此——是 shell 执行了对诸如“*”之类的字符的解释,将其分解,然后传递给底层操作系统。
那么,如何列出所有带有“*.txt”后缀的文件呢?您可以再次调用相同的调用,但告诉 Python 通过 UNIX shell 传递参数
>>> print(subprocess.check_output(['ls', '-l', '*.txt'],
shell=True,
universal_newlines=True))
啊哈!现在看来工作正常。
那么,这里发生了什么?这启动了一个新进程(如果愿意,可以称为“子进程”),并在该进程中执行了一个 UNIX 程序。该程序返回了一些文本,Python 捕获了该文本,然后将其打印出来。
Python 文档明确指出,在您对 subprocess.check_output
(和其他函数)的调用中包含 shell=True
存在潜在的安全风险。如果您从未知或不受信任的用户那里获取输入,则该人可以将任意命令插入到运行 check_output
的系统中。在使用 shell=True
之前,请务必考虑其安全隐患。
subprocess.check_output
是一个特定的函数,旨在运行程序并检索其输出。如果您想要更大的灵活性,您可以运行“subprocess”中的其他函数。
例如,假设您想从 ls
获取输出并将其放入文件中。在 UNIX 命令行上,您可以说
ls -l > file-list.txt
在 Python 中,这有点复杂,但如果您使用 subprocess.run
,则不会太复杂。此函数是新的(自 Python 3.5 起),但它使生活变得更轻松。
您可以尝试这样做
>>> subprocess.run(['/bin/ls', '-l'], universal_newlines=True)
如您所见,subprocess.run
接受许多与 subprocess.check_output
类似的参数。但不同之处在于,即使将 universal_newlines
设置为 True
,它也不会返回字符串。相反,它返回 subprocess.CompletedProcess
的实例,其中包含有关运行进程的各种信息。
您可以获取它,然后查看 CompletedProcess
包含什么
>>> cp = subprocess.run(['/bin/ls', '-l'], universal_newlines=True)
>>> vars(cp)
您将得到返回
{'args': ['/bin/ls', '-l'], 'returncode': 0, 'stderr': None,
'stdout': None}
嗯,这可能不太符合您的期望。args
很好,并且 returncode
准确地显示为 0,这意味着一切都正常结束。但是输出发生了什么?答案是,当涉及到 subprocess.run
时,您需要指示输出应该去哪里。
指示您想要获取某些内容的方式是将 subprocess.PIPE
作为 stdout
关键字参数的值传递
>>> cp = subprocess.run(['/bin/ls', '-l'], stdout=subprocess.PIPE,
>>> universal_newlines=True)
>>> vars(cp)
您现在将获得以下内容
{'args': ['/bin/ls', '-l'],
'returncode': 0,
'stderr': None,
'stdout': 'total 344\ndrwxr-xr-x 1454 reuven staff 49436
↪Sep 17 09:29 Archive\ndrwxr-xr-x 37 reuven staff 1
我什至不打算向您展示其余部分,因为它太长了,但是 stdout
值完全正确。
您还可以将 stderr
分配给 subprocess.PIPE
以接收它。请注意,对于 stdout 和 stderr,您可以分配的不仅仅是 subprocess.PIPE
,它允许您获取和处理程序的输出,还可以分配一个打开的(可写)文件对象。这意味着您可以调用外部进程并将其输出放入任意文件中。我认为,在大多数情况下,您在 Python 中执行外部进程的原因是您想对文本做一些事情,但这也可以工作。
您可能想知道您是否不仅可以写入 stderr 和 stdout,还可以从 stdin 读取。答案是肯定的。只需提供一个文件对象,subprocess.run
将完成其余的工作。例如
>>> cp = subprocess.run(['/bin/cat', '-n'], stdin=open('/etc/passwd'),
stdout=subprocess.PIPE, universal_newlines=True)
在这种情况下,您使用 -n
选项运行 /bin/cat
,对文件的行进行编号。输入文件是什么?/etc/passwd。输出去哪里?到您的 subprocess.PIPE
对象,这是一种与外部进程通信的通道。
对我来说,最有趣的是 CompletedProcess
对象 (cp
),您可以从中获取有关已完成进程的不同信息。请注意,subprocess.run
仅在外部程序完成运行后才会返回,此时将设置 cp
变量。从那里,您可以获取 stdout
,它通常是字节串,但如果您将 universal_newlines
设置为 True
,则它是实际的(Unicode)字符串。
您现在已经了解了如何使用“subprocess”模块与外部进程通信。但让我们面对现实。这并没有完全解决最初的问题:分解问题并使用不同的进程来处理它。相反,这在某种程度上展示了 Python 如何与进程一起工作,以及它与进程通信的基本方式,使用字节串和管道。这是因为进程是独立的,不能简单地与主线程共享变量,而这正是您在使用线程时所做的事情。