本文 AI 產出,尚未審核

Python

單元:自動化與腳本應用(Automation & CLI)

主題:subprocess 執行外部程式


簡介

在日常的自動化腳本或 CLI 工具開發中,常會需要 呼叫系統指令或其他可執行檔,例如 gitffmpegcurl… 直接在 Python 程式內執行這類外部程式,既能利用現有的功能,又能把結果整合回 Python 的資料流程,極大提升開發效率與可維護性。

subprocess 是 Python 標準庫中專門負責 建立子行程、與之通訊、取得輸出或錯誤訊息 的模組。從 Python 2 時代的 subprocess.Popen 到 Python 3.5 起加入的 subprocess.run,它提供了多層次的抽象,讓開發者可以根據需求選擇簡潔或彈性的 API。

本篇文章將從 核心概念實用範例常見陷阱與最佳實踐,一步步帶你掌握在 Python 中安全、有效地執行外部程式,並說明在真實專案中如何應用。


核心概念

1. 為什麼不直接使用 os.system

os.system() 只能傳遞一個字串給 shell,無法取得子行程的輸出,而且在安全性上容易受到 shell injection 攻擊。subprocess 則提供:

  • 完整的 輸入/輸出/錯誤管道 控制
  • 支援 字串或列表 形式的參數,避免解析錯誤
  • 可選擇 是否使用 shellshell=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 同時捕獲 stdoutstderr(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 形式的 argsshell=False(預設)可避免 shell 注入問題。capture_output=True 同時捕獲 stdoutstderrtext=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 金鑰),cwdenv 參數提供了乾淨且可重現的執行環境。


常見陷阱與最佳實踐

陷阱 可能的後果 解決方案 / 最佳實踐
使用 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.joinpathlib.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)")

好處

  1. 統一錯誤處理,讓上層程式碼只需捕捉 RuntimeError
  2. 預設安全shell=Falsetext=Truecapture_output=True
  3. 可重用,適用於所有需要呼叫外部工具的情境。

實際應用場景

  1. CI/CD 流程自動化

    • 使用 subprocess.run 呼叫 git, docker, kubectl 等 CLI,取得版本資訊或部署結果。
    • 例:在 GitHub Actions 中自動產生變更版本號 git describe --tags,再寫入程式碼。
  2. 資料轉換或多媒體處理

    • 透過 ffmpeg 轉檔、壓縮影片或音訊,subprocess.run(..., timeout=300) 防止長時間卡住。
    • 取得 ffprobe 的 JSON 輸出後,直接在 Python 中解析。
  3. 系統監控與資源管理

    • 呼叫 ps, top, netstat 等工具,收集即時系統資訊,寫入日誌或觸發告警。
    • 使用 Popen 持續讀取 tail -f /var/log/syslog,即時顯示最新日誌。
  4. 自動化測試

    • 在測試框架(pytest、unittest)中使用 subprocess.run 執行被測試的 CLI 程式,驗證返回碼與輸出。
    • 透過 stdin=subprocess.PIPE 傳入測試資料,檢查程式的回應行為。
  5. 跨平台工具封裝

    • 為 Windows、macOS、Linux 寫一個統一的 Python 包裝器,內部使用 subprocess 呼叫平台原生指令,對外提供相同的 Python API。

總結

subprocessPython 自動化與 CLI 開發的核心利器。掌握它的 高階 run低階 Popen,以及正確的參數設定(shell=Falsecapture_outputtexttimeout),能讓你:

  • 安全 地執行外部程式,避免注入與資源卡住的風險。
  • 有效 取得子行程的輸出與錯誤,並在程式內部做進一步的處理。
  • 彈性 地控制工作目錄、環境變數、執行時間,適用於各種真實情境。

只要遵守 最佳實踐(使用 list 參數、統一錯誤處理、設定逾時、注意編碼),就能在 腳本、CI/CD、資料處理、測試、跨平台工具 等多種領域中,發揮 subprocess 的威力,寫出 可靠且易維護 的自動化程式。祝你在 Python 的自動化旅程中,玩得開心、寫得順手!