文章
合集Python 并发编程第 5 / 7 篇

subprocess:调用外部命令

Python 很强大,但有些事情用现成的命令行工具更方便,pandoc 转文档、ffmpeg 处理视频、git 管理版本。subprocess 就是从 Python 里调这些工具的桥梁。

1. 为什么需要 subprocess

场景现成工具Python 原生实现
文档转换pandoc没有现成的
图片压缩imagemagickPillow 可以但更慢
视频处理ffmpeg无
版本控制git没有原生 git 库
系统信息df、ls、findos / pathlib 有部分替代

2. subprocess.run 核心参数详解

2.1 基本用法

import subprocess

result = subprocess.run(
    ["ls", "-la", "/tmp"],     # 命令是 list,不是字符串
    capture_output=True,        # 捕获 stdout 和 stderr
    text=True,                  # 输出当文本而不是字节
    timeout=10,                 # 超时(秒),超了抛 TimeoutExpired
    check=True,                 # 退出码非 0 时抛异常
)

print(result.stdout)            # 标准输出(文本)
print(result.stderr)            # 错误输出
print(result.returncode)        # 退出码

2.2 list 形式 vs shell=True(安全问题)

2.3 其他参数速查

参数作用建议
capture_output=True捕获 stdout + stderr生产代码推荐
text=True输出自动 decode 为 str用 encoding="utf-8" 更明确
check=True退出码非 0 抛 CalledProcessError强烈推荐加
timeout=N超时抛 TimeoutExpired生产环境标配
cwd="/path"设置工作目录比 cd && cmd 更干净
env={}设置环境变量可以继承或覆盖当前环境

3. 工业级模板

3.1 标准调用模板

import subprocess
import logging

logger = logging.getLogger(__name__)

def run_command(cmd: list[str], timeout: int = 30) -> str | None:
    """
    调用外部命令,返回 stdout 字符串。失败返回 None。
    """
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            check=True,
        )
        return result.stdout.strip()
    except subprocess.CalledProcessError as e:
        # 命令执行了但退出码非 0
        logger.error("命令失败 [%s]: %s", cmd[0], e.stderr.strip())
        return None
    except subprocess.TimeoutExpired:
        # 命令超时
        logger.error("命令超时 [%s],限时 %d 秒", cmd[0], timeout)
        return None
    except FileNotFoundError:
        # 命令不存在
        logger.error("命令不存在: %s", cmd[0])
        return None

三种异常都要捕获:CalledProcessError(执行失败)、TimeoutExpired(超时)、FileNotFoundError(命令不存在)。

3.2 实战:Markdown 转 PDF

def md_to_pdf(md_path: str, pdf_path: str) -> bool:
    """用 pandoc 把 markdown 转 pdf。"""
    try:
        subprocess.run(
            ["pandoc", md_path, "-o", pdf_path],
            check=True,
            timeout=30,
            capture_output=True,
            text=True,
        )
        return True
    except subprocess.CalledProcessError as e:
        logger.error("pandoc 失败: %s", e.stderr)
        return False
    except subprocess.TimeoutExpired:
        logger.error("pandoc 超时")
        return False
    except FileNotFoundError:
        logger.error("pandoc 未安装")
        return False

4. 超时控制与输出截断

4.1 超时处理

try:
    result = subprocess.run(
        ["long_running_command"],
        timeout=5,   # 5 秒不返回就放弃
        capture_output=True,
    )
except subprocess.TimeoutExpired as e:
    # e.stdout 和 e.stderr 在超时时也可能有内容(部分输出)
    print("超时前的输出:", e.stdout)
    # 超时后进程可能还在运行,需要手动终止

4.2 输出截断

对于可能产生大量输出的命令,可以限制输出量:

result = subprocess.run(
    ["large_output_command"],
    capture_output=True,
    text=True,
)

# 只取前 1000 个字符
output = result.stdout[:1000] if result.stdout else ""

或者用 subprocess.Popen 实现流式读取(更高级的用法)。

5. subprocess.run vs os.system

对比os.systemsubprocess.run
返回值退出码(int)CompletedProcess 对象
输出获取不能直接获取capture_output=True
安全性默认走 shell(不安全)默认不走 shell(安全)
超时不支持timeout=N
错误处理无check=True 抛异常
推荐不要用唯一推荐
# os.system 的问题
exit_code = os.system("ls -la")   # 打印到 stdout,拿不到变量里
# 不能捕获输出、不能设超时、默认走 shell

# subprocess.run 的优势
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)   # 输出在变量里,可以处理

6. 小结

知识点要记住的
subprocess.run唯一推荐的 API,替代 os.system
list 形式永远用 ["cmd", "arg1", "arg2"],不要用字符串
check=True生产代码强烈推荐,自动抛 CalledProcessError
timeout=N生产环境标配,防止外部命令卡死
三种异常CalledProcessError + TimeoutExpired + FileNotFoundError

上一篇:python-asyncio,高并发 I/O 的终极方案。 下一篇:python-concurrent-llm-calls,综合案例,把前 5 篇串起来。