Python
單元:自動化與腳本應用(Automation & CLI)
主題:subprocess 執行外部程式
簡介
在日常的自動化腳本或 CLI 工具開發中,常會需要 呼叫系統指令或其他可執行檔,例如 git、ffmpeg、curl… 直接在 Python 程式內執行這類外部程式,既能利用現有的功能,又能把結果整合回 Python 的資料流程,極大提升開發效率與可維護性。
subprocess 是 Python 標準庫中專門負責 建立子行程、與之通訊、取得輸出或錯誤訊息 的模組。從 Python 2 時代的 subprocess.Popen 到 Python 3.5 起加入的 subprocess.run,它提供了多層次的抽象,讓開發者可以根據需求選擇簡潔或彈性的 API。
本篇文章將從 核心概念、實用範例、常見陷阱與最佳實踐,一步步帶你掌握在 Python 中安全、有效地執行外部程式,並說明在真實專案中如何應用。
核心概念
1. 為什麼不直接使用 os.system?
os.system() 只能傳遞一個字串給 shell,無法取得子行程的輸出,而且在安全性上容易受到 shell injection 攻擊。subprocess 則提供:
- 完整的 輸入/輸出/錯誤管道 控制
- 支援 字串或列表 形式的參數,避免解析錯誤
- 可選擇 是否使用 shell(
shell=True/False)
結論:在需要取得結果、處理錯誤、或避免安全風險時,
subprocess是首選。
2. 主要 API 概覽
| API | 簡介 | 何時使用 |
|---|---|---|
subprocess.run(args, ...) |
Python 3.5+ 的高階封裝,會等子行程結束後回傳 CompletedProcess 物件 |
大多數簡單或一次性的呼叫 |
subprocess.Popen(args, ...) |
低階、可持續互動的介面,返回 Popen 物件 |
需要即時讀寫、長時間執行或多次交互 |
subprocess.check_output(args, ...) |
只回傳標準輸出,失敗時拋出 CalledProcessError |
想快速取得輸出且不在意錯誤代碼 |
subprocess.call(args, ...) |
僅回傳返回碼 | 只關心成功與否,不需要輸出 |
subprocess.check_call(args, ...) |
成功回傳 0,失敗拋例外 | 想要在失敗時立即中斷程式 |
3. 參數說明(最常用)
| 參數 | 功能 | 常見值 |
|---|---|---|
args |
程式名稱與參數(字串或 list) | ["git", "status"] |
shell |
是否交給系統 shell 執行 | True(需小心) |
capture_output |
同時捕獲 stdout、stderr(Python 3.7+) |
True |
text / encoding |
文字模式或指定編碼 | text=True |
cwd |
子行程的工作目錄 | cwd="/tmp" |
env |
傳遞自訂環境變數 | env={"PATH": "..."} |
timeout |
最長執行秒數,逾時會拋 TimeoutExpired |
timeout=10 |
stdin, stdout, stderr |
重新導向管道 | subprocess.PIPE |
程式碼範例
以下範例均以 Python 3.9+ 為基礎,採用 subprocess.run 為主,必要時示範 Popen 的使用方式。每段程式碼都加上註解說明重點。
範例 1:最簡單的指令執行與回傳碼
import subprocess
# 執行 "ls -l"(Linux/macOS)或 "dir"(Windows)並取得返回碼
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
print("返回碼:", result.returncode) # 0 表示成功
print("標準輸出:")
print(result.stdout) # 列出目錄內容
重點:使用 list 形式的
args,shell=False(預設)可避免 shell 注入問題。capture_output=True同時捕獲stdout與stderr,text=True讓結果直接是字串。
範例 2:取得標準錯誤並拋出例外
import subprocess
try:
# 嘗試執行不存在的指令
subprocess.run(
["nonexistent_cmd"],
capture_output=True,
text=True,
check=True # 若返回碼非 0,拋出 CalledProcessError
)
except subprocess.CalledProcessError as e:
print("指令失敗!返回碼:", e.returncode)
print("錯誤訊息:", e.stderr) # 這裡會顯示「command not found」之類的訊息
技巧:
check=True能自動把非 0 返回碼轉成例外,讓錯誤處理更集中。
範例 3:使用 timeout 防止指令卡住
import subprocess
try:
# 假設有一個會卡住的指令,例如 "sleep 30"
subprocess.run(
["sleep", "30"],
timeout=5, # 最多等 5 秒
capture_output=True,
text=True
)
except subprocess.TimeoutExpired as e:
print(f"指令執行逾時!已在 {e.timeout} 秒後終止。")
實務:在自動化腳本或 CI/CD 流程中,逾時控制 能防止單一步驟卡住整個 pipeline。
範例 4:以 Popen 與子行程互動(即時讀寫)
import subprocess
# 啟動一個交互式的 Python 直譯器
proc = subprocess.Popen(
["python", "-i"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 傳送指令給子行程
proc.stdin.write('print("Hello from child")\n')
proc.stdin.flush() # 必須 flush,才能讓子行程收到資料
# 讀取子行程的回應(一次讀取所有輸出)
output = proc.stdout.readline()
print("子行程回傳:", output.strip())
# 結束子行程
proc.stdin.write('exit()\n')
proc.stdin.flush()
proc.wait()
說明:
Popen允許 持續的 I/O,適合需要「即時」回饋的情境,如自動化測試、遠端指令控制等。務必 使用flush()或communicate()以免資料卡在緩衝區。
範例 5:在不同目錄執行指令,並傳遞自訂環境變數
import subprocess, os
env = os.environ.copy()
env["MY_VAR"] = "HelloWorld"
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd="/path/to/your/repo", # 切換工作目錄
env=env, # 帶入自訂環境變數
capture_output=True,
text=True,
check=True
)
print("目前提交的短 SHA:", result.stdout.strip())
print("自訂環境變數 MY_VAR:", env["MY_VAR"])
應用:在自動化部署腳本中,常需要 切換工作目錄 或 注入特定環境變數(例如 API 金鑰),
cwd與env參數提供了乾淨且可重現的執行環境。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 / 最佳實踐 |
|---|---|---|
使用 shell=True 且直接拼接字串 |
易受 shell injection,尤其接受使用者輸入時 | 除非必須(如使用管道 ` |
忘記 communicate() 或 flush() |
子行程的輸出卡在緩衝區,導致程式掛住(deadlock) | 使用 subprocess.run(..., capture_output=True),或在 Popen 後 立即呼叫 communicate() |
| 忽略文字編碼 | 在 Windows、Linux 不同的預設編碼下,中文或特殊字元會變成亂碼 | 明確指定 encoding="utf-8" 或 text=True(Python 3.7+) |
直接使用 stdout=subprocess.PIPE 但不讀取 |
子行程因管道緩衝區滿而被阻塞,導致程式卡住 | 若不需要輸出,改用 stdout=subprocess.DEVNULL;若需要,務必 讀取或丟棄 |
未設定 timeout |
長時間卡住的指令會讓 CI/CD 或服務永遠不回應 | 為所有可能無限等待的指令 設定合理的 timeout,並捕捉 TimeoutExpired 例外 |
| 在 Windows 使用 Unix 路徑 | FileNotFoundError 或錯誤的執行結果 |
使用 os.path.join、pathlib.Path,或在程式碼中做平台判斷 (os.name) |
推薦的寫法範本
import subprocess
def run_cmd(args, cwd=None, env=None, timeout=30):
"""安全、統一的子行程執行函式。"""
try:
result = subprocess.run(
args,
cwd=cwd,
env=env,
capture_output=True,
text=True,
timeout=timeout,
check=True, # 失敗自動拋例外
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
raise RuntimeError(f"指令失敗 (返回碼 {e.returncode}): {e.stderr.strip()}") from e
except subprocess.TimeoutExpired:
raise RuntimeError(f"指令執行逾時(>{timeout}s)")
好處:
- 統一錯誤處理,讓上層程式碼只需捕捉
RuntimeError。- 預設安全:
shell=False、text=True、capture_output=True。- 可重用,適用於所有需要呼叫外部工具的情境。
實際應用場景
CI/CD 流程自動化
- 使用
subprocess.run呼叫git,docker,kubectl等 CLI,取得版本資訊或部署結果。 - 例:在 GitHub Actions 中自動產生變更版本號
git describe --tags,再寫入程式碼。
- 使用
資料轉換或多媒體處理
- 透過
ffmpeg轉檔、壓縮影片或音訊,subprocess.run(..., timeout=300)防止長時間卡住。 - 取得
ffprobe的 JSON 輸出後,直接在 Python 中解析。
- 透過
系統監控與資源管理
- 呼叫
ps,top,netstat等工具,收集即時系統資訊,寫入日誌或觸發告警。 - 使用
Popen持續讀取tail -f /var/log/syslog,即時顯示最新日誌。
- 呼叫
自動化測試
- 在測試框架(pytest、unittest)中使用
subprocess.run執行被測試的 CLI 程式,驗證返回碼與輸出。 - 透過
stdin=subprocess.PIPE傳入測試資料,檢查程式的回應行為。
- 在測試框架(pytest、unittest)中使用
跨平台工具封裝
- 為 Windows、macOS、Linux 寫一個統一的 Python 包裝器,內部使用
subprocess呼叫平台原生指令,對外提供相同的 Python API。
- 為 Windows、macOS、Linux 寫一個統一的 Python 包裝器,內部使用
總結
subprocess 是 Python 自動化與 CLI 開發的核心利器。掌握它的 高階 run 與 低階 Popen,以及正確的參數設定(shell=False、capture_output、text、timeout),能讓你:
- 安全 地執行外部程式,避免注入與資源卡住的風險。
- 有效 取得子行程的輸出與錯誤,並在程式內部做進一步的處理。
- 彈性 地控制工作目錄、環境變數、執行時間,適用於各種真實情境。
只要遵守 最佳實踐(使用 list 參數、統一錯誤處理、設定逾時、注意編碼),就能在 腳本、CI/CD、資料處理、測試、跨平台工具 等多種領域中,發揮 subprocess 的威力,寫出 可靠且易維護 的自動化程式。祝你在 Python 的自動化旅程中,玩得開心、寫得順手!