Python高级技术之:`subprocess`模块的`run()`、`popen()`和`call()`:阻塞与非阻塞的命令执行。

各位观众,晚上好!我是今晚的讲师,很高兴和大家一起聊聊Python subprocess模块这三个重量级选手:run()popen()call()。它们就像厨房里的三把刀,各有千秋,用对了能让你料理各种系统命令得心应手。

今天,我们不搞虚的,直接上干货,用最接地气的方式,把这三个方法扒个底朝天,让大家以后遇到需要执行系统命令的情况,不再抓瞎。

开胃小菜:subprocess模块是干啥的?

简单来说,subprocess模块就是Python连接操作系统shell的桥梁。有了它,你就可以在Python程序里像直接在命令行里敲命令一样,执行各种系统命令,比如lspingmkdir等等。这对于自动化运维、脚本编写、甚至简单的程序交互都非常有用。

第一道主菜:subprocess.run() – 现代化的瑞士军刀

run()方法是Python 3.5之后引入的,可以说是官方推荐的用法。它就像一把瑞士军刀,功能强大且使用方便。它会等待命令执行完毕,并返回一个CompletedProcess对象,包含了命令执行的所有信息,例如返回值、标准输出、标准错误等等。

  • 阻塞式执行: run()会阻塞程序的执行,直到命令执行完毕。这意味着你的Python代码会停在那里,等待命令执行完成,才能继续往下走。

  • 基本用法:

    import subprocess
    
    result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
    
    if result.returncode == 0:
        print("命令执行成功!")
        print("标准输出:", result.stdout)
        print("标准错误:", result.stderr)
    else:
        print("命令执行失败!")
        print("错误码:", result.returncode)
        print("标准错误:", result.stderr)

    这段代码执行了ls -l命令,并将结果捕获到result对象中。capture_output=True告诉run()捕获标准输出和标准错误,text=True告诉run()以文本模式处理输出,这样你就可以直接用字符串的方式访问result.stdout

  • 常用参数:

    • args: 要执行的命令,可以是一个字符串,也可以是一个列表。建议使用列表,可以避免shell注入的风险。
    • capture_output: 是否捕获标准输出和标准错误。默认为False,表示不捕获。
    • text: 是否以文本模式处理标准输出和标准错误。默认为False,表示以字节流模式处理。
    • check: 是否检查命令的返回值。如果为True,并且命令的返回值不为0,则会抛出一个subprocess.CalledProcessError异常。
    • timeout: 命令执行的超时时间,单位为秒。超过这个时间,命令会被强制终止,并抛出一个subprocess.TimeoutExpired异常。
    • shell: 是否使用shell执行命令。默认为False。如果为True,则会将args作为一个字符串传递给shell执行。不建议使用shell=True,除非你真的需要shell的特性,比如管道和通配符。
  • check=True的威力:

    import subprocess
    
    try:
        result = subprocess.run(['不存在的命令'], capture_output=True, text=True, check=True)
    except subprocess.CalledProcessError as e:
        print("命令执行失败!")
        print("错误码:", e.returncode)
        print("标准错误:", e.stderr)

    这段代码执行了一个不存在的命令,由于check=True,所以run()会抛出一个CalledProcessError异常,我们可以通过try...except语句来捕获这个异常,并进行处理。

  • 超时控制:

    import subprocess
    
    try:
        result = subprocess.run(['sleep', '5'], timeout=2, capture_output=True, text=True)
    except subprocess.TimeoutExpired as e:
        print("命令执行超时!")
        print("超时时间:", e.timeout)
        print("命令:", e.cmd)

    这段代码执行了一个会休眠5秒的命令,但是设置了超时时间为2秒,所以run()会抛出一个TimeoutExpired异常。

  • 使用管道:

    虽然不推荐使用shell=True,但在某些情况下,使用管道可以简化代码。

    import subprocess
    
    result = subprocess.run('ls -l | grep "myfile.txt"', capture_output=True, text=True, shell=True)
    
    print(result.stdout)

    这段代码使用了管道,将ls -l的输出传递给grep "myfile.txt"命令进行过滤。注意,使用shell=True时,需要格外小心shell注入的风险。

第二道主菜:subprocess.Popen() – 掌控进程的精细大师

Popen()方法是subprocess模块的核心,其他方法都是基于它实现的。它允许你更精细地控制子进程的执行,例如可以实时获取子进程的输出,可以向子进程发送输入,等等。

  • 非阻塞式执行: Popen()启动一个子进程后,会立即返回一个Popen对象,而不会等待命令执行完毕。这意味着你的Python代码可以继续往下执行,而子进程在后台运行。

  • 基本用法:

    import subprocess
    
    process = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    
    stdout, stderr = process.communicate()
    
    print("标准输出:", stdout)
    print("标准错误:", stderr)
    print("返回值:", process.returncode)

    这段代码使用Popen()启动了ls -l命令,并将标准输出和标准错误重定向到管道中。然后,使用process.communicate()方法从管道中读取输出,并获取命令的返回值。

  • 常用参数:

    • args: 要执行的命令,和run()一样,建议使用列表。
    • stdin: 子进程的标准输入。可以是一个文件对象,也可以是一个管道。
    • stdout: 子进程的标准输出。可以是一个文件对象,也可以是一个管道。
    • stderr: 子进程的标准错误。可以是一个文件对象,也可以是一个管道。
    • shell: 是否使用shell执行命令,和run()一样,不建议使用。
    • cwd: 子进程的工作目录。
    • env: 子进程的环境变量。
  • 实时获取输出:

    import subprocess
    import time
    
    process = subprocess.Popen(['ping', 'www.baidu.com'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    
    while True:
        line = process.stdout.readline()
        if line:
            print(line.strip())
        if process.poll() is not None: #检查子进程是否结束
            break
        time.sleep(0.1)
    
    print("返回值:", process.returncode)

    这段代码使用Popen()启动了ping www.baidu.com命令,并实时获取输出。process.stdout.readline()方法会阻塞,直到读取到一行数据或者文件结束。process.poll()方法会检查子进程是否结束,如果结束则返回子进程的返回值,否则返回None

  • 向子进程发送输入:

    import subprocess
    
    process = subprocess.Popen(['python'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    
    process.stdin.write("print('Hello from child process!')n")
    process.stdin.flush()
    process.stdin.close() # 必须关闭输入流,否则子进程会一直等待输入
    
    stdout, stderr = process.communicate()
    
    print("标准输出:", stdout)
    print("标准错误:", stderr)
    print("返回值:", process.returncode)

    这段代码使用Popen()启动了一个Python解释器,并通过process.stdin.write()方法向子进程发送Python代码。注意,必须调用process.stdin.close()方法关闭输入流,否则子进程会一直等待输入。

  • wait()方法:

    process.wait()方法会等待子进程执行完毕,并返回子进程的返回值。它和run()方法一样,是阻塞式的。

    import subprocess
    
    process = subprocess.Popen(['sleep', '5'])
    
    print("开始等待...")
    returncode = process.wait()
    print("等待结束,返回值:", returncode)
  • kill()terminate()方法:

    process.kill()方法会向子进程发送SIGKILL信号,强制终止子进程。process.terminate()方法会向子进程发送SIGTERM信号,请求子进程正常终止。

    import subprocess
    import time
    
    process = subprocess.Popen(['sleep', '10'])
    
    time.sleep(2)
    process.terminate()  # 或者 process.kill()
    
    print("已发送终止信号")
    returncode = process.wait()
    print("返回值:", returncode)

第三道主菜:subprocess.call() – 简单粗暴,但有时也够用

call()方法是subprocess模块中最简单的方法。它会执行命令,并等待命令执行完毕,然后返回命令的返回值。

  • 阻塞式执行: call()也会阻塞程序的执行,直到命令执行完毕。

  • 基本用法:

    import subprocess
    
    returncode = subprocess.call(['ls', '-l'])
    
    print("返回值:", returncode)

    这段代码执行了ls -l命令,并将返回值打印出来。

  • 重定向输出:

    call()方法可以通过stdoutstderr参数将标准输出和标准错误重定向到文件对象。

    import subprocess
    
    with open('output.txt', 'w') as f:
        returncode = subprocess.call(['ls', '-l'], stdout=f)
    
    print("返回值:", returncode)

    这段代码将ls -l命令的标准输出重定向到output.txt文件中。

  • call()的局限性:

    call()方法的功能比较简单,无法捕获标准输出和标准错误,也无法实时获取输出。因此,在大多数情况下,建议使用run()Popen()方法。

餐后甜点:总结与选择

功能 subprocess.run() subprocess.Popen() subprocess.call()
执行方式 阻塞式 非阻塞式(需要配合communicate()wait()等方法使用) 阻塞式
返回值 CompletedProcess对象 (包含返回值, 标准输出, 标准错误等) Popen对象 (可以控制子进程, 获取标准输入/输出/错误) 返回命令的返回值
标准输出/错误捕获 支持 (通过 capture_output=True) 支持 (通过 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 支持重定向到文件 (通过 stdout=file, stderr=file)
实时输出 不支持 支持 (通过循环读取 stdoutstderr) 不支持
超时控制 支持 (通过 timeout 参数) 需要手动实现 (例如使用 process.wait(timeout=...) 并捕获 TimeoutExpired 异常) 不支持
使用场景 简单命令执行, 需要获取完整结果, 并对结果进行处理 需要更精细地控制子进程, 例如实时获取输出, 向子进程发送输入, 或者需要非阻塞式执行命令.需要进行复杂的进程间通信或控制。 简单命令执行, 只需要返回值, 不需要获取输出
推荐程度 强烈推荐 (Python 3.5+) 推荐 (需要更精细的控制时) 不推荐 (功能较弱, 建议使用 run()Popen())

选择建议:

  • 如果只是想简单地执行一个命令,并获取它的返回值和输出,那么run()方法是你的首选。
  • 如果需要更精细地控制子进程的执行,例如实时获取输出,向子进程发送输入,或者需要非阻塞式执行命令,那么Popen()方法是你的不二之选。
  • 如果只是想简单地执行一个命令,并获取它的返回值,那么call()方法也可以凑合用用,但是不推荐。

注意事项:

  • 尽量避免使用shell=True,除非你真的需要shell的特性。
  • 在使用Popen()方法时,一定要记得关闭输入流,否则子进程可能会一直等待输入。
  • 在使用管道时,要注意缓冲区的大小,避免死锁。

好了,今天的讲座就到这里。希望大家能够掌握subprocess模块的这三个方法,并在实际工作中灵活运用。记住,没有最好的方法,只有最适合的方法。根据不同的场景选择不同的方法,才能让你事半功倍。

谢谢大家!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注