subprocess:调用外部命令
Python 很强大,但有些事情用现成的命令行工具更方便,pandoc 转文档、ffmpeg 处理视频、git 管理版本。subprocess 就是从 Python 里调这些工具的桥梁。
1. 为什么需要 subprocess
| 场景 | 现成工具 | Python 原生实现 |
|---|---|---|
| 文档转换 | pandoc | 没有现成的 |
| 图片压缩 | imagemagick | Pillow 可以但更慢 |
| 视频处理 | ffmpeg | 无 |
| 版本控制 | git | 没有原生 git 库 |
| 系统信息 | df、ls、find | os / 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.system | subprocess.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 篇串起来。