苦练Python第69天:subprocess模块从入门到上瘾,手把手教你驯服系统命令!

前言

大家好,我是 倔强青铜三 。欢迎关注我,微信公众号: 倔强青铜三。点赞、收藏、关注,一键三连!

Python的subprocess模块是标准库中用于创建和管理子进程的终极武器。

相比老旧的os.systemos.popen,它提供了更强大、更灵活、更安全的进程控制能力。

今天我们就来彻底掌握这个能让你Python脚本调用任何系统命令的模块!

一、模块概述与核心价值

在Python中执行系统命令曾经是个头疼的问题。老方法如os.system("ls -l")虽然简单,但只能获取返回码,无法捕获命令输出;os.popen()能获取输出,但功能有限且不够安全。

subprocess模块解决了所有这些问题,它不仅能执行命令,还能:

  • 捕获命令的输出和错误
  • 控制输入输出流
  • 设置超时限制
  • 管理进程环境
  • 安全地处理命令参数
python 复制代码
import subprocess

# 老方法:os.system只能获取返回码
import os
return_code = os.system("echo 'Hello from os.system'")
print(f"os.system返回码: {return_code}")  # 只能得到执行状态,看不到输出

# 运行结果:
# Hello from os.system
# os.system返回码: 0

二、核心类与函数:Popen与run()

1. 基础类 subprocess.Popen

Popensubprocess模块的底层类,提供了最灵活的进程控制方式。

python 复制代码
import subprocess

# 最基本的Popen用法
process = subprocess.Popen(["echo", "Hello from Popen"])
process.wait()  # 等待进程结束
print(f"进程已结束,返回码: {process.returncode}")

# 运行结果:
# Hello from Popen
# 进程已结束,返回码: 0

Popen构造函数的关键参数:

  • args: 要执行的命令,可以是字符串(不推荐)或列表(推荐)
  • shell: 是否通过系统shell执行命令
  • stdout, stderr, stdin: 控制输入输出流
  • cwd: 设置工作目录
  • env: 设置环境变量

2. 高级快捷函数 subprocess.run()

run()是Python 3.5+引入的高级封装,简化了常见用例。

python 复制代码
import subprocess

# 使用run()执行简单命令
result = subprocess.run(["echo", "Hello from run()"], capture_output=True, text=True)
print(f"返回码: {result.returncode}")
print(f"标准输出: {result.stdout.strip()}")
print(f"标准错误: {result.stderr}")

# 运行结果:
# 返回码: 0
# 标准输出: Hello from run()
# 标准错误: 

run()返回一个CompletedProcess对象,包含:

  • returncode: 命令的退出状态码
  • stdout: 标准输出内容(如果设置了capture_output或stdout=PIPE)
  • stderr: 标准错误内容(如果设置了capture_output或stderr=PIPE)

三、子进程的执行与控制

1. 同步执行与阻塞

默认情况下,run()Popen.wait()都是阻塞的,会等待命令执行完成。

python 复制代码
import subprocess
import time

print("开始执行命令...")
start_time = time.time()

# 执行一个会稍微延迟的命令
result = subprocess.run(["sleep", "2"], capture_output=True, text=True)

end_time = time.time()
print(f"命令执行完成,耗时: {end_time - start_time:.2f}秒")
print(f"返回码: {result.returncode}")

# 运行结果:
# 开始执行命令...
# 命令执行完成,耗时: 2.00秒
# 返回码: 0

2. 异步执行与非阻塞

虽然run()是阻塞的,但我们可以使用Popen实现非阻塞效果。

python 复制代码
import subprocess
import time

# 启动一个长时间运行的进程但不等待
process = subprocess.Popen(["sleep", "3"])

print(f"进程已启动,PID: {process.pid}")
print("主程序继续执行其他任务...")

# 检查进程状态
for i in range(5):
    return_code = process.poll()  # 检查进程是否结束
    if return_code is None:
        print(f"第{i+1}秒检查: 进程仍在运行")
    else:
        print(f"第{i+1}秒检查: 进程已结束,返回码: {return_code}")
        break
    time.sleep(1)

# 确保进程结束
if process.poll() is None:
    process.wait()
print("最终检查: 进程状态已确认")

# 运行结果:
# 进程已启动,PID: 12345  # 实际PID会不同
# 主程序继续执行其他任务...
# 第1秒检查: 进程仍在运行
# 第2秒检查: 进程仍在运行
# 第3秒检查: 进程已结束,返回码: 0
# 最终检查: 进程状态已确认

四、输入输出(I/O)的重定向与捕获

1. 标准输出(stdout)的捕获与处理

捕获命令输出是最常见的需求之一。

python 复制代码
import subprocess

# 捕获命令输出
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)

print("命令输出:")
print(result.stdout[:200] + "...")  # 只打印前200个字符避免输出过长
print(f"\n返回码: {result.returncode}")
print(f"输出字节长度: {len(result.stdout)}")

# 运行结果(示例,实际输出取决于你的目录内容):
# 命令输出:
# total 16
# -rw-r--r--  1 user  group  1234 Aug 19 10:00 example.py
# -rw-r--r--  1 user  group  5678 Aug 19 10:01 test.txt
# ...
# 
# 返回码: 0
# 输出字节长度: 123

2. 标准错误(stderr)的捕获与合并

错误输出同样重要,特别是调试时。

python 复制代码
import subprocess

# 执行一个会出错的命令
result = subprocess.run(["ls", "non_existent_file_12345"], 
                       capture_output=True, text=True)

print("标准输出:", result.stdout)
print("标准错误:", result.stderr.strip())
print("返回码:", result.returncode)

# 运行结果:
# 标准输出: 
# 标准错误: ls: non_existent_file_12345: No such file or directory
# 返回码: 1

3. 标准输入(stdin)的写入与交互

向命令传递输入数据。

python 复制代码
import subprocess

# 向命令传递输入
process = subprocess.Popen(["grep", "hello"], 
                          stdin=subprocess.PIPE, 
                          stdout=subprocess.PIPE, 
                          stderr=subprocess.PIPE,
                          text=True)

# 发送输入并获取输出
input_text = "This is a test\nHello world\nAnother line\n"
stdout, stderr = process.communicate(input=input_text)

print("输入文本:")
print(input_text)
print("\n过滤后的输出(grep 'hello'):")
print(stdout)
print("返回码:", process.returncode)

# 运行结果:
# 输入文本:
# This is a test
# Hello world
# Another line
#
# 过滤后的输出(grep 'hello'):
# Hello world
# 返回码: 0

五、错误处理与退出码管理

1. 子进程退出状态码

理解返回码是处理命令执行结果的关键。

python 复制代码
import subprocess

# 成功的命令
success_result = subprocess.run(["true"])  # true命令总是返回0
print(f"true命令返回码: {success_result.returncode}")

# 失败的命令
try:
    fail_result = subprocess.run(["false"], check=True)  # false命令总是返回1
except subprocess.CalledProcessError as e:
    print(f"false命令执行失败,返回码: {e.returncode}")
    print(f"异常信息: {e}")

# 运行结果:
# true命令返回码: 0
# false命令执行失败,返回码: 1
# 异常信息: Command '['false']' returned non-zero exit status 1.

2. 异常处理

正确处理各种异常情况。

python 复制代码
import subprocess

# 处理命令不存在的情况
try:
    result = subprocess.run(["non_existent_command_123"], check=True)
except FileNotFoundError as e:
    print(f"命令未找到错误: {e}")
except subprocess.CalledProcessError as e:
    print(f"命令执行失败: {e}")
except Exception as e:
    print(f"其他错误: {e}")

# 处理超时情况
try:
    result = subprocess.run(["sleep", "5"], timeout=2)  # 2秒后超时
except subprocess.TimeoutExpired as e:
    print(f"命令执行超时: {e}")
    
# 运行结果(会触发FileNotFoundError或TimeoutExpired,取决于第一个异常):
# 命令未找到错误: [Errno 2] No such file or directory: 'non_existent_command_123'
# 或者
# 命令执行超时: Command '['sleep', '5']' timed out after 2 seconds

六、高级配置与扩展

1. 环境变量与工作目录

自定义命令执行环境。

python 复制代码
import subprocess
import os

# 查看当前工作目录
print("当前Python脚本工作目录:", os.getcwd())

# 在指定目录中执行命令
result = subprocess.run(["pwd"],  # Unix/Linux命令,显示当前目录
                       cwd="/tmp",  # 在/tmp目录中执行
                       capture_output=True, 
                       text=True)
print(f"/tmp目录下的pwd结果: {result.stdout.strip()}")

# 自定义环境变量
custom_env = os.environ.copy()  # 复制当前环境
custom_env["MY_CUSTOM_VAR"] = "Hello from custom env"

result = subprocess.run(["env"],  # 显示所有环境变量
                       env=custom_env,  # 使用自定义环境
                       capture_output=True, 
                       text=True)
print("\n环境变量中包含MY_CUSTOM_VAR:", "MY_CUSTOM_VAR" in result.stdout)
print("MY_CUSTOM_VAR的值:", 
      [line for line in result.stdout.split('\n') if "MY_CUSTOM_VAR" in line][0])

# 运行结果:
# 当前Python脚本工作目录: /your/current/path  # 实际路径会不同
# /tmp目录下的pwd结果: /tmp
# 环境变量中包含MY_CUSTOM_VAR: True
# MY_CUSTOM_VAR的值: MY_CUSTOM_VAR=Hello from custom env

2. 超时控制

防止命令执行时间过长。

python 复制代码
import subprocess

# 设置超时
try:
    print("执行一个会超时的命令...")
    result = subprocess.run(["sleep", "3"], timeout=1.5)  # 1.5秒后超时
except subprocess.TimeoutExpired as e:
    print(f"命令超时被终止: {e}")
    print("超时命令信息:", e.cmd)
    print("超时设置:", e.timeout)

# 运行结果:
# 执行一个会超时的命令...
# 命令超时被终止: Command '['sleep', '3']' timed out after 1.5 seconds
# 超时命令信息: ['sleep', '3']
# 超时设置: 1.5

3. Shell模式的 使用与风险

shell=True的强大与危险。

python 复制代码
import subprocess

# 不使用shell
print("不使用shell执行:")
result = subprocess.run(["echo", "Hello without shell"], 
                       capture_output=True, 
                       text=True)
print(result.stdout.strip())

# 使用shell (有风险!)
print("\n使用shell执行:")
try:
    result = subprocess.run("echo 'Hello with shell'", 
                          shell=True, 
                          capture_output=True, 
                          text=True)
    print(result.stdout.strip())
    
    # 危险示例: 命令注入风险(仅用于演示,实际不要这样做!)
    user_input = "hello; ls -l"  # 恶意用户可能输入这样的内容
    print("\n危险示例 - 命令注入风险演示:")
    dangerous_command = f"echo {user_input}"  # 不安全的命令构造
    result = subprocess.run(dangerous_command, shell=True, capture_output=True, text=True)
    print("危险命令输出:", result.stdout)
except Exception as e:
    print(f"错误: {e}")

# 运行结果:
# 不使用shell执行:
# Hello without shell
#
# 使用shell执行:
# Hello with shell
#
# 危险示例 - 命令注入风险演示:
# 危险命令输出: echo hello; ls -l
# hello; ls -l

⚠️ 重要警告 : shell=True虽然方便(可以直接执行像ls -l这样的字符串命令),但存在严重的安全风险!如果命令中包含用户输入,攻击者可能通过注入额外命令来执行恶意操作。尽可能使用shell=False(默认)并通过列表传递参数。

七、最佳实践与常见问题

  1. 优先使用列表形式传递参数["ls", "-l"]"ls -l"更安全
  2. 避免不必要的shell=True:除非需要shell特性(如管道、通配符)
  3. 总是处理返回码和异常:检查命令是否成功执行
  4. 合理使用超时:防止命令挂起导致程序卡死
  5. 及时释放资源:确保子进程正确终止,避免僵尸进程
python 复制代码
import subprocess

# 最佳实践示例: 安全地执行命令并处理结果
def safe_execute(command, timeout=30):
    try:
        # 使用列表形式,避免shell=True
        result = subprocess.run(command, 
                              stdout=subprocess.PIPE, 
                              stderr=subprocess.PIPE,
                              text=True,
                              timeout=timeout,
                              check=True)
        return {
            "success": True,
            "returncode": result.returncode,
            "stdout": result.stdout,
            "stderr": result.stderr
        }
    except subprocess.CalledProcessError as e:
        return {
            "success": False,
            "error": "命令执行失败",
            "returncode": e.returncode,
            "stdout": e.stdout,
            "stderr": e.stderr
        }
    except subprocess.TimeoutExpired:
        return {
            "success": False,
            "error": f"命令执行超时(超过{timeout}秒)",
            "returncode": None,
            "stdout": None,
            "stderr": None
        }
    except FileNotFoundError:
        return {
            "success": False,
            "error": "命令不存在",
            "returncode": None,
            "stdout": None,
            "stderr": None
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"未知错误: {str(e)}",
            "returncode": None,
            "stdout": None,
            "stderr": None
        }

# 测试最佳实践函数
print("测试成功命令:")
print(safe_execute(["echo", "This is a safe command"]))

print("\n测试失败命令:")
print(safe_execute(["false"]))

print("\n测试不存在的命令:")
print(safe_execute(["non_existent_command_123"]))

# 运行结果:
# 测试成功命令:
# {'success': True, 'returncode': 0, 'stdout': 'This is a safe command\n', 'stderr': ''}
#
# 测试失败命令:
# {'success': False, 'error': '命令执行失败', 'returncode': 1, 'stdout': '', 'stderr': ''}
#
# 测试不存在的命令:
# {'success': False, 'error': '命令不存在', 'returncode': None, 'stdout': None, 'stderr': None}

八、实战场景与应用

subprocess模块在实际开发中有着广泛的应用:

  1. 自动化运维脚本:执行系统命令监控服务器状态
  2. 与命令行工具交互:调用git、docker、ffmpeg等工具
  3. 数据处理管道:将多个命令组合成处理流程
  4. 替代旧方法 :用更安全的subprocess替换os.systemos.popen
python 复制代码
import subprocess

# 实战示例: 获取系统信息并处理
def get_system_info():
    try:
        # 获取磁盘使用情况
        df_result = subprocess.run(["df", "-h"], 
                                 capture_output=True, 
                                 text=True, 
                                 check=True)
        
        # 获取内存使用情况 (Unix/Linux)
        free_result = subprocess.run(["free", "-h"], 
                                   capture_output=True, 
                                   text=True, 
                                   check=True)
        
        return {
            "disk_usage": df_result.stdout,
            "memory_usage": free_result.stdout,
            "status": "success"
        }
    except Exception as e:
        return {
            "status": "error",
            "error": str(e)
        }

# 获取并打印系统信息
system_info = get_system_info()
if system_info["status"] == "success":
    print("=== 磁盘使用情况 ===")
    print(system_info["disk_usage"])
    print("\n=== 内存使用情况 ===")
    print(system_info["memory_usage"])
else:
    print("获取系统信息失败:", system_info["error"])

# 运行结果(示例,实际输出取决于你的系统):
# === 磁盘使用情况 ===
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/disk1s1    500G  100G  400G  20% /
# ...
#
# === 内存使用情况 ===
#               total        used        free      shared  buff/cache   available
# Mem:           8G        2G        4G        200M        2G        5G
# Swap:          2G        0B        2G

通过掌握subprocess模块,你的Python脚本将获得强大的系统交互能力,能够轻松调用任何命令行工具,实现各种自动化任务。

记住遵循最佳实践,特别是安全方面的考虑,你就能成为Python系统编程的高手!

最后感谢阅读!欢迎关注我,微信公众号: 倔强青铜三
点赞、收藏、关注 ,一键三连!!

欢迎 点击 【👍喜欢作者】 按钮进行 打赏💰💰,请我喝一杯咖啡☕️!

相关推荐
Antonio9154 小时前
【图像处理】rgb和srgb
图像处理·人工智能·数码相机
倔强青铜三4 小时前
苦练 Python 第 68 天:并发狂飙!concurrent 模块让你 CPU 原地起飞
人工智能·python·面试
星期天要睡觉4 小时前
深度学习——循环神经网络(RNN)实战项目:基于PyTorch的文本情感分析
人工智能·python·rnn·深度学习·神经网络
2401_858869804 小时前
目标检测2
人工智能·目标检测·计算机视觉
ARM+FPGA+AI工业主板定制专家4 小时前
基于ZYNQ的目标检测算法硬件加速器优化设计
人工智能·目标检测·计算机视觉·fpga开发·自动驾驶
koo3644 小时前
李宏毅机器学习笔记21-26周汇总
人工智能·笔记·机器学习
ERROR_LESS5 小时前
【ADS-1】【python基础-2】基本语法与数据结构(列表、字典、集合)
python
2401_841495645 小时前
【数据结构】基于Floyd算法的最短路径求解
java·数据结构·c++·python·算法··floyd