Python subprocess子进程模块

本文详细介绍了Python中os.popen和subprocess模块在创建、管理子进程和处理输入输出方面的用法,包括命令行交互、管道、错误处理和shell模式。同时讨论了shell=True的风险以及psutil.popen的便捷接口。
摘要由CSDN通过智能技术生成

子进程

正在运行的程序称为进程。每个进程都有自己的系统状态,包括内存、打开文件列表、跟踪正在执行的指令的程序计数器以及用于保存函数局部变量的调用堆栈。

通常,一个进程在单个控制流序列中一个接一个地执行语句,有时称为进程的主线程。在任何给定时间,程序只做一件事。

程序可以使用库函数创建新进程,例如在 os 或子进程模块(例如os.fork()subprocess.Popen()等中找到的那些。但是,这些进程,称为进程,作为完全独立的实体运行 -每个都有自己的私有系统状态和主执行线程。

因为子进程是独立的,它与原始进程并发执行。也就是说,创建子流程的流程可以继续处理其他事情,而子流程在幕后执行自己的工作。

subprocess 模块

subprocess 模块允许我们:

  1. 产生新进程
  2. 连接到他们的输入/输出/错误管道
  3. 获取他们的返回码

它提供了比其他一些可用模块更高级别的接口,旨在取代以下功能:

  1. os.system()
  2. os.spawn*()
  3. os.popen*()
  4. popen2.*()
  5. commands.*()

我们不能在 Python 脚本中使用 UNIX 命令,就好像它们是 Python 代码一样。例如,echo name会导致语法错误,因为echo不是 Python 中的内置语句或函数。因此,在 Python 脚本中,我们使用print (name)代替。

要运行 UNIX 命令,我们需要创建一个运行命令的子进程。调用子流程的推荐方法是对它们可以处理的所有用例使用便利函数。或者我们可以使用底层的Popen接口直接使用即可。

os.system()

运行 UNIX 命令的最简单方法是使用os.system()

>>> import os
>>> os.system('echo $HOME')
/user/khong
0

>>> # or we can use
>>> os.system('echo %s' %'$HOME')
/user/khong
0

正如预期的那样,我们将$HOME作为标准输出(到终端)。另外,我们得到了一个返回值0,这是执行这个命令的结果,这意味着执行没有错误。

os.system ('command with args') 将命令和参数传递给我们系统的 shell。通过使用它实际上可以一次运行多个命令并设置管道和输入/输出重定向。:

os.system('command_1 < input_file | command_2 > output_file')

如果我们在 Python IDLE 中运行os.system('echo $HOME')上面的代码,我们只会看到0,因为 stdout 表示终端。要查看命令输出,我们应该将其重定向到一个文件,并从中读取:

>>> import os
>>> os.system('echo $HOME > outfile')
0
>>> f = open('outfile','r')
>>> f.read()
'/user/khong\n'
os.popen()

打开一个管道到或从命令。返回值是连接到管道的打开文件对象,可以根据模式是'r'(默认)还是'w'读取或写入。bufsize 参数与内置open()函数的相应参数具有相同的含义。命令的退出状态(以wait()指定的格式编码)可作为文件对象的close()方法的返回值,除了当退出状态为零(无错误终止)时,None是回。

>>> import os
>>> stream = os.popen('echo $HOME')
>>> stream.read()
'/user/khong\n'

os.popen()做同样的事情,使用os.system但它为我们提供了一个类似文件的流对象,我们可以用它来访问标准输入/输出该进程。还有 3 种其他的popen变体,它们对 i/o 的处理略有不同。

如果我们将所有内容作为字符串传递,那么我们的命令就会传递给 shell;如果我们将它们作为列表传递,那么我们无需担心转义任何内容。

但是,它自 2.6 版起已被弃用:此功能已过时。使用子流程模块。docs.python.org

subprocess.call()

这基本上就像Popen类并采用所有相同的参数,但它只是等待命令完成并为我们提供返回码。

subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False)

运行args描述的命令。等待命令完成,然后返回returncode属性。

>>> import os
>>> os.chdir('/')
>>> import subprocess
>>> subprocess.call(['ls','-l'])
total 181
drwxr-xr-x    2 root root  4096 Mar  3  2012 bin
drwxr-xr-x    4 root root  1024 Oct 26  2012 boot
...

命令行参数作为字符串列表传递,这避免了转义引号或其他可能被 shell 解释的特殊字符的需要。

>>> import subprocess
>>> subprocess.call('echo $HOME')
Traceback (most recent call last):
...
OSError: [Errno 2] No such file or directory
>>>
>>> subprocess.call('echo $HOME', shell=True)
/user/khong
0

将 shell 参数设置为 true 值会导致subprocess产生一个中间 shell 进程,并告诉它运行命令。换句话说,使用中间 shell 意味着在命令运行之前处理命令字符串中的变量、glob 模式和其他特殊的 shell 功能。在这里,在示例中,$HOMEecho命令之前处理。实际上,这是带有外壳扩展的命令的情况,而命令ls -l被认为是一个简单的命令。

这是一个示例代码(PyGoogle/FFMpeg/iframe_extract.py)。它下载 YouTube 视频,然后将 I 帧提取到子文件夹:

'''
iframe_extract.py - download video and ffmpeg i-frame extraction
Usage: 
(ex) python iframe_extract.py -u https://www.youtube.com/watch?v=dP15zlyra3c
This code does two things:
1. Download using youtube-dl
cmd = ['youtube-dl', '-f', videoSize, '-k', '-o', video_out, download_url]
2. Extract i-frames via ffmpeg
cmd = [ffmpeg,'-i', inFile,'-f', 'image2','-vf',
        "select='eq(pict_type,PICT_TYPE_I)'",'-vsync','vfr', imgFilenames]
'''

from __future__ import unicode_literals
import youtube_dl

import sys
import os
import subprocess
import argparse
import glob

if sys.platform == "Windows":
    FFMPEG_BIN = "ffmpeg.exe"
    MOVE = "move"
    MKDIR = "mkdir"
else:
    FFMPEG_BIN = "ffmpeg"
    MOVE = "mv"
    MKDIR = "md"


def iframe_extract(inFile):
# ffmpeg -i inFile -f image2 -vf \
#   "select='eq(pict_type,PICT_TYPE_I)'" -vsync vfr oString%03d.png

    # infile : video file name 
    #          (ex) 'FoxSnowDive-Yellowstone-BBCTwo.mp4'
    imgPrefix = inFile.split('.')[0]
    # imgPrefix : image file 

    # start extracting i-frames
    home = os.path.expanduser("~")
    ffmpeg = home + '/bin/ffmpeg'

    imgFilenames = imgPrefix + '%03d.png'
  
    cmd = [ffmpeg,'-i', inFile,'-f', 'image2','-vf',
        "select='eq(pict_type,PICT_TYPE_I)'",'-vsync','vfr', imgFilenames]
    
    # create iframes
    print "creating iframes ...."
    subprocess.call(cmd)

    # Move the extracted iframes to a subfolder
    # imgPrefix is used as a subfolder name that stores iframe images
    cmd = 'mkdir -p ' + imgPrefix
    os.system(cmd)
    print "make subdirectoy", cmd
    mvcmd = 'mv ' + imgPrefix + '*.png ' + imgPrefix
    print "moving images to subdirectoy", mvcmd
    os.system(mvcmd)



def get_info_and_download(download_url):

    # Get video meta info and then download using youtube-dl

    ydl_opts = {}

    # get meta info from the video
    with youtube_dl.YoutubeDL(ydl_opts) as ydl:
        meta = ydl.extract_info(download_url, download=False)

    # renaming the file 
    # remove special characters from the file name
    print('meta[title]=%s' %meta['title'])
    out = ''.join(c for c in meta['title'] if c.isalnum() or c =='-' or c =='_' ) 
    print('out=%s' %out)
    extension = meta['ext']
    video_out = out + '.' + extension
    print('video_out=%s' %video_out)
    videoSize = 'bestvideo[height<=540]+bestaudio/best[height<=540]'
    cmd = ['youtube-dl', '-f', videoSize, '-k', '-o', video_out, download_url]
    print('cmd=%s' %cmd)

    # download the video
    subprocess.call(cmd)

    # Sometimes output file has format code in name such as 'out.f248.webm'
    # so, in this case, we want to rename it 'out.webm' 
    found = False
    extension_list = ['mkv', 'mp4', 'webm']
    for e in extension_list:
       glob_str = '*.' + e
       for f in glob.glob(glob_str):
          if out in f:
             if os.path.isfile(f):
                video_out = f
                found = True
                break
       if found:
          break
       
    # call iframe-extraction : ffmpeg
    print('before iframe_extract() video_out=%s' %video_out)
    iframe_extract(video_out)
    return meta



def check_arg(args=None):

# Command line options
# Currently, only the url option is used

    parser = argparse.ArgumentParser(description='download video')
    parser.add_argument('-u', '--url',
                        help='download url',
                        required='True')
    parser.add_argument('-i', '--infile',
                        help='input to iframe extract')
    parser.add_argument('-o', '--outfile',
                        help='output name for iframe image')

    results = parser.parse_args(args)
    return (results.url,
            results.infile,
            results.outfile)


# Usage sample:
#    syntax: python iframe_extract.py -u url
#    (ex) python iframe_extract.py -u https://www.youtube.com/watch?v=dP15zlyra3c

if __name__ == '__main__':
    u,i,o = check_arg(sys.argv[1:])
    meta = get_info_and_download(u)

subprocess.check_call()

subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False)

check_call()函数类似于调用()除了退出代码检查,如果它表明发生了随后的错误CalledProcessError异常。

>>> import subprocess
>>> subprocess.check_call(['false'])
Traceback (most recent call last):
...
subprocess.CalledProcessError: Command '['false']' returned non-zero exit status 1

subprocess.check_output()

subprocess.check_output(args, *, stdin=None, stderr=None, 
                                 shell=False, universal_newlines=False)

call()启动的进程的标准输入和输出通道绑定到父进程的输入和输出。这意味着调用程序无法捕获命令的输出。为了捕获输出,我们可以使用check_output()进行后续处理。

>>> import subprocess
>>> output = subprocess.check_output(['ls','-l'])
>>> print output
total 181
drwxr-xr-x    2 root root  4096 Mar  3  2012 bin
drwxr-xr-x    4 root root  1024 Oct 26  2012 boot
...
>>> output = subprocess.check_output(['echo','$HOME'], shell=True)
>>> print output
/user/khong

这个函数是在 Python 2.7 中添加的。

subprocess.Popen()

此模块中的底层进程创建和管理由Popen类处理。它提供了很大的灵活性,因此开发人员能够处理便利功能未涵盖的不太常见的情况。

subprocess.Popen()在新进程中执行子程序。在 Unix 上,该类使用os.execvp() 之类的行为来执行子程序。在 Windows 上,该类使用 Windows CreateProcess()函数。

 

subprocess.Popen() 的参数
class subprocess.Popen(args, bufsize=0, executable=None, 
                       stdin=None, stdout=None, stderr=None, 
                       preexec_fn=None, close_fds=False, 
                       shell=False, cwd=None, env=None, universal_newlines=False, 
                       startupinfo=None, creationflags=0)
  1. args
    应该是程序参数序列或单个字符串。默认情况下,如果 args 是一个序列,则要执行的程序是 args 中的第一项。如果 args 是字符串,则解释取决于平台。建议将 args 作为序列传递。
  2. shell :
    shell参数(默认为False)指定是否使用 shell 作为要执行的程序。如果 shell 是True,建议将 args 作为字符串而不是序列传递。
    在带有shell=True 的Unix 上,shell 默认为/bin/sh
    1. 如果argsstring,则该字符串指定要通过 shell 执行的命令。这意味着字符串的格式必须与在 shell 提示符下键入时完全相同。例如,这包括引用或反斜杠转义其中包含空格的文件名。
    2. 如果args是一个序列,则第一项指定命令字符串,任何附加项都将被视为 shell 本身的附加参数。也就是说,Popen 的作用相当于:
      Popen(['/bin/sh', '-c', args[0], args[1], ...])

  3. bufsize
    如果给定,则与内置open()函数的相应参数具有相同的含义:
    1. 0 表示无缓冲
    2. 1 表示行缓冲
    3. 任何其他正值意味着使用(大约)该大小的缓冲区
    4. 负的 bufsize 意味着使用系统默认值,这通常意味着完全缓冲
    5. bufsize 的默认值为 0(无缓冲)

  4. 可执行文件
    指定要执行的替换程序。很少需要它。
  5. 标准输入、标准输出和标准错误
    1. 分别指定执行程序的标准输入、标准输出和标准错误文件句柄。
    2. 有效值为PIPE、现有文件描述符(正整数)、现有文件对象和 None。
    3. PIPE表示应该创建一个到孩子的新管道。
    4. 默认设置为None,不会发生重定向;子级的文件句柄将从父级继承。
    5. 此外,stderr 可以是 STDOUT,这表明来自子进程的 stderr 数据应该被捕获到与 stdout 相同的文件句柄中。

  6. preexec_fn
    设置为可调用对象,该对象将在子进程执行之前在子进程中调用。(仅限 Unix)
  7. close_fds :
    为真,在子进程执行前,除 0、1、2 之外的所有文件描述符都将被关闭。(仅限 Unix)。或者,在 Windows 上,如果close_fds为真,则子进程将不会继承任何句柄。请注意,在 Windows 上,我们不能将close_fds设置为 true,也不能通过设置 stdin、stdout 或 stderr 来重定向标准句柄。
  8. cwd :
    is not None 在执行之前,子目录的当前目录将更改为cwd。请注意,搜索可执行文件时不考虑此目录,因此我们无法指定程序相对于cwd的路径。
  9. env :
    不是 None,它必须是一个映射,定义了新进程的环境变量;使用这些而不是继承当前进程的环境,这是默认行为。
  10. Universal_newlines :
    为真,文件对象 stdout 和 stderr 在通用换行模式下作为文本文件打开。行可以由'\n'、Unix 行尾约定、'\r'、旧的 Macintosh 约定或'\r\n' 中的任何一个终止,Windows 约定。所有这些外部表示都被Python 程序视为“\n”
  11. startupinfo :
    将是一个STARTUPINFO对象,它被传递给底层的CreateProcess函数。
  12. creationflags
    可以是CREATE_NEW_CONSOLECREATE_NEW_PROCESS_GROUP。(仅限 Windows)
     

os.popen 与 subprocess.Popen()

这旨在替代os.popen,但它更复杂。例如,我们使用

subprocess.Popen("echo Hello World", stdout=subprocess.PIPE, shell=True).stdout.read()

代替

os.popen("echo Hello World").read()

但它是全面的,它在一个统一的类中拥有所有选项,而不是不同的os.popen函数。


 

subprocess.Popen() - 标准输出和标准错误
>>> import subprocess
>>> proc = subprocess.Popen(['echo', '"Hello world!"'], 
...                          stdout=subprocess.PIPE)
>>> stddata = proc.communicate()
>>> stddata
('"Hello world!"\n', None)

请注意,communication()方法返回一个元组(stdoutdata, stderrdata) : ('"Hello world!"\n' ,None)。如果我们在Popen调用中不包括stdout=subprocess.PIPEstderr= subprocess.PIPE,我们只会得到None

Popen.communicate(input=None)

Popen.communicate()与进程交互:将数据发送到stdin。从stdoutstderr读取数据,直到到达文件尾。等待进程终止。可选的输入参数应该是要发送到子进程的字符串,或者None,如果不应该向子进程发送数据。

所以,实际上,我们可以这样做:

>>> import subprocess
>>> proc = subprocess.Popen(['echo', '"Hello world!"'],
...                          stdout=subprocess.PIPE)
>>> (stdoutdata, stderrdata) = proc.communicate()
>>> stdoutdata
'"Hello world!"\n'

或者我们可以从proc.communicate() 中明确指定我们想要的:

>>> import subprocess
>>> proc = subprocess.Popen(['echo', '"Hello world!"'],
...                          stdout=subprocess.PIPE)
>>> stdoutdata = proc.communicate()[0]
>>> stdoutdata
'"Hello world!"\n'

上面示例中最简单的代码可能是将流直接发送到控制台:

>>> import subprocess
>>> proc = subprocess.Popen(['echo', '"Hello world!"'],
...                          stdout=subprocess.PIPE)
>>> proc.communicate()[0]
'"Hello world!"\n'

下面的代码用于测试 stdout 和 stderr 行为:

# std_test.py
import sys
sys.stdout.write('Testing message to stdout\n')
sys.stderr.write('Testing message to stderr\n')

如果我们运行它:

>>> proc = subprocess.Popen(['python', 'std_test.py'],
...                          stdout=subprocess.PIPE)
>>> Testing message to stderr
>>> proc.communicate()
(Testing message to stdout\n', None)
>>>

请注意,发送到stderr的消息会在生成时显示,但发送到stdout的消息是通过管道读取的。这是因为我们只设置了一个到stdout的管道。

然后,让我们从 Python 访问 stdout 和 stderr:

>>> proc = subprocess.Popen(['python', 'std_test.py'],
...                          stdout=subprocess.PIPE,
...                          stderr=subprocess.PIPE)
>>> proc.communicate()
(Testing message to stdout\n', Testing message to stderr\n')

通信()方法仅读取从数据标准输出标准错误达到-的文件结束为止。因此,在打印完所有消息后,如果我们再次调用communication(),我们会得到一个错误:

>>> proc.communicate()
Traceback (most recent call last):
...
ValueError: I/O operation on closed file

如果我们希望将发送到stderr 的消息通过管道传输到stderr,我们可以这样做:stderr=subprocess.STDOUT

>>> proc = subprocess.Popen(['python', 'std_test.py'],
...                          stdout=subprocess.PIPE,
...                          stderr=subprocess.STDOUT)
>>> proc.communicate()
('Testing message to stdout\r\nTesting message to stderr\r\n', None)

正如我们从输出中看到的,我们没有stderr,因为它已被重定向到stderr

subprocess.Popen() - 标准输入

可以以非常相似的方式写入进程。如果我们想将数据发送到进程的stdin,我们需要使用stdin= subprocess.PIPE创建Popen对象。

为了测试它,让我们编写另一个程序(write_to_stdin.py),它只打印 Received: 然后重复我们发送的消息:

# write_to_stdin.py
import sys
input = sys.stdin.read()
sys.stdout.write('Received: %s'%input)

要向stdin发送消息,我们将要发送的字符串作为输入参数传递给communication()

>>> proc = subprocess.Popen(['python', 'write_to_stdin.py'],  stdin=subprocess.PIPE)
>>> proc.communicate('Hello?')
Received: Hello?(None, None)

请注意,在write_to_stdin.py过程中创建的消息被打印到stdout,然后返回值(None, None)被打印出来。那是因为没有为stdoutstderr设置管道。

这是我们在设置管道之前指定stdout=subprocess.PIPEstderr=subprocess.PIPE之后的另一个输出。

>>> proc = subprocess.Popen(['python', 'write_to_stdin.py'],  
...                          stdin=subprocess.PIPE, 
...                          stdout=subprocess.PIPE, 
...                          stderr=subprocess.PIPE)
>>> proc.communicate('Hello?')
('Received: Hello?', '')
subprocess.Popen() - 外壳管道
>>> p1 = subprocess.Popen(['df','-h'], stdout=subprocess.PIPE)
>>> p2 = subprocess.Popen(['grep', 'sda1'], stdin=p1.stdout, stdout=subprocess.PIPE)
>>> p1.stdout.close()  # Allow p1 to receive a SIGPIPE if p2 exits
>>> output = p2.communicate()[0]
>>> output
'/dev/sda1              19G  9.2G  8.3G  53% /\n'

启动p2后的p1.stdout.close()调用很重要,如果p2p1之前退出,则p1会收到SIGPIPE

这是管道命令的另一个示例。该代码获取当前活动窗口的窗口 ID。该命令如下所示:

xprop -root | awk '/_NET_ACTIVE_WINDOW\(WINDOW\)/{print $NF}'

蟒蛇代码:

# This code runs the following awk to get a window id for the currently active X11 window
# xprop -root | awk '/_NET_ACTIVE_WINDOW\(WINDOW\)/{print $NF}'

import subprocess

def py_xwininfo():
	winId = getCurrentWinId()
	print 'winId = %s' %winId

def getCurrentWinId():
	cmd_1 = ['xprop', '-root']
	cmd_2 = ['awk', '/_NET_ACTIVE_WINDOW\(WINDOW\)/{print $NF}']
	p1 = subprocess.Popen(cmd_1, stdout = subprocess.PIPE)
	p2 = subprocess.Popen(cmd_2, stdin = p1.stdout, stdout=subprocess.PIPE)
	id = p2.communicate()[0]

	return id

if __name__ == '__main__':
	py_xwininfo()

输出:

winId = 0x3c02035

subprocess.Popen() - shell=True

一定要避免shell=True

shell=True表示通过 shell 执行代码。换句话说,通过 shell 执行程序意味着传递给程序的所有用户输入都根据调用的 shell 的语法和语义规则进行解释。充其量,这只会给用户带来不便,因为用户必须遵守这些规则。例如,必须对包含特殊 shell 字符(如引号或空格)的路径进行转义。在最坏的情况下,它会导致安全漏洞,因为用户可以执行任意程序。

shell=True有时可以方便地使用特定的 shell 功能,例如分词或参数扩展。但是,如果需要这样的功能,请使用其他模块(例如os.path.expandvars()用于参数扩展或 shlex 用于分词)。这意味着更多的工作,但避免了其他问题。- 来自subprocess 中 'shell=True' 的实际含义

如果argsstring,则该字符串指定要通过 shell 执行的命令。这意味着字符串的格式必须与在 shell 提示符下键入时完全相同。例如,这包括引用或反斜杠转义其中包含空格的文件名:

>>> proc = subprocess.Popen('echo $HOME', shell=True)
>>> /user/khong

该字符串的格式与在 shell 提示符下键入的格式完全相同:

$ echo $Home

因此,以下操作不起作用:

>>> proc = subprocess.Popen('echo $HOME', shell=False)
Traceback (most recent call last):
...
OSError: [Errno 2] No such file or directory

以下也行不通:

>>> subprocess.Popen('echo "Hello world!"', shell=False)
Traceback (most recent call last):
...
OSError: [Errno 2] No such file or directory

那是因为我们仍然将它作为字符串传递,Python 假定整个字符串是要执行的程序的名称,并且没有名为echo "Hello world!"的程序所以它失败了。相反,我们必须分别传递每个参数。
 

psutil 和子进程 - psutil.popen()

psutil.Popen(*指定参数时,** kwargs)是一种更方便的接口STDLIB subprocess.Popen() 

“它启动一个子进程,并处理它使用时完全一样subprocess.Popen类,但除此之外还提供了所有的属性和方法psutil.Process在一个单一的接口类”。
- 请参阅http://code.google.com/p/psutil/wiki/Documentation

以下代码在子进程上运行python -c "p​​rint 'hi, psutil'"

>>> import psutil
>>> import subprocess
>>> proc = psutil.Popen(["/usr/bin/python", "-c", "print 'hi, psuti'"], stdout=subprocess.PIPE)
>>> proc

>>> proc.uids
user(real=1000, effective=1000, saved=1000)
>>> proc.username
'khong'
>>> proc.communicate()
('hi, psuti\n', None)


参考
  1. docs.python.org
  2. 子流程 - 使用其他流程
  3. subprocess - 使用 Python 子进程 - Shell、进程、流、管道、重定向等
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值