PHP 執行外部程式的常用函式
在 PHP 裡想執行系統指令,最常見的函式有 exec(), shell_exec(), system() 和 passthru()。雖然它們都能達成目的,但回傳的結果和處理方式不太一樣。
exec() 通常用來獲取指令執行後的最後一行輸出。如果你需要完整的輸出內容,可以傳入一個陣列變數來接收。它不會直接把結果印在網頁上,適合需要處理資料的內容。
shell_exec() 會執行指令並將完整的輸出內容轉成字串回傳。這對於想一次看完整結果的人很方便。另外,使用反引號也能達到同樣的效果。
system() 和 passthru() 則會直接把執行結果輸出到瀏覽器。system() 會回傳最後一行的內容,而 passthru() 則適合用在處理二進位資料,像是直接產生圖片或檔案流。
安全性建議
執行系統指令是非常危險的操作。如果指令中包含使用者輸入的資料,務必使用 escapeshellarg() 或 escapeshellcmd() 進行過濾。這可以防止惡意使用者透過特殊字元注入額外的系統指令。
很多主機商為了安全會預設停用這些函式。你可以在 php.ini 檔案裡的 disable_functions 看到限制清單。
程式碼範例
這裡示範如何安全地執行一個簡單的 ls 指令:
PHP
<?php
$dir = "/tmp";
$safe_dir = escapeshellarg($dir);
$command = "ls -l " . $safe_dir;
// 使用 exec 獲取詳細列表
exec($command, $output, $return_var);
if ($return_var === 0) {
foreach ($output as $line) {
echo $line . "\n";
}
} else {
echo "執行失敗";
}
?>
進階處理方式
如果你需要更細緻的控制,像是同時處理標準輸出 (stdout) 和錯誤輸出 (stderr),建議使用 proc_open()。它能開啟一個管道跟進程溝通,就像在寫進階的系統程式一樣。
這對於執行時間較長或需要互動的程式比較有效。雖然寫法複雜一點,但比起前面幾個函式,它能提供的資訊更完整。
使用 proc_open 處理進階流程
如果你需要更精密的控制,proc_open 是最強大的工具。它能讓你同時開啟多個管道,分別處理指令的輸入、輸出與錯誤訊息。這就像是你在 PHP 程式裡開了一個小視窗,可以直接跟作業系統對話。
透過這個函式,你可以把資料傳進程式,同時即時讀取程式回傳的錯誤。這在執行像編譯程式或處理大型影音轉換時非常有用。
實作範例
這是一個簡單的範例,示範如何開啟進程並讀取結果:
PHP
<?php
$descriptorspec = [
0 => ["pipe", "r"], // 標準輸入
1 => ["pipe", "w"], // 標準輸出
2 => ["pipe", "w"] // 標準錯誤
];
$process = proc_open('ls -l', $descriptorspec, $pipes);
if (is_resource($process)) {
// 讀取標準輸出
echo stream_get_contents($pipes[1]);
fclose($pipes[1]);
// 讀取錯誤輸出
echo stream_get_contents($pipes[2]);
fclose($pipes[2]);
$return_value = proc_close($process);
echo "程式結束碼:" . $return_value;
}
?>
注意事項
使用這些函式時,記得檢查主機權限。如果 PHP 執行的權限不夠,很多系統指令會無法運作。另外,執行時間太長可能會觸發 PHP 的 max_execution_time 限制,這點要特別小心。
背景執行指令
如果你只需要啟動一個指令,但不打算等它執行完才回傳 API 結果,這就是所謂的背景執行。在伺服器端開發這很常見,像是發送大量郵件或處理圖檔,如果不非同步處理,使用者會感覺網頁卡住很久。
最簡單的做法是把輸出導向到黑洞。在 Linux 系統中,這代表要把標準輸出和錯誤輸出都導向到 /dev/null。
常見的快速解法
在指令最後面加上 & 符號,可以讓指令在背景執行。配合輸出重定向,PHP 就不會卡在該行程式。
PHP
<?php
$command = "php long_task.php";
// 導向輸出並在背景啟動
exec($command . " > /dev/null 2>&1 &");
echo "任務已啟動";
?>
這裡的 > /dev/null 代表把一般輸出丟棄,2>&1 則是把錯誤訊息也一起丟棄。最後的 & 讓這條命令直接進到背景跑。
使用 nohup 確保穩定
如果你的外部指令執行時間非常長,有時候 PHP 進程結束時會連帶影響到子進程。這時候可以用 nohup 指令。它會讓指令忽略掛斷訊號,即便 PHP 執行完畢,系統指令也會繼續跑完。
PHP
<?php
$command = "nohup python3 process_data.py";
exec($command . " > /dev/null 2>&1 &");
?>
更穩定的架構建議
雖然 exec 配合 & 很方便,但這對伺服器資源管理不太友善。如果 API 瞬間被呼叫一千次,伺服器就會同時噴出一千個背景進程,可能會導致當機。
對於正式的大型專案,通常建議改用隊列系統。API 收到請求後,只把任務寫進資料庫或 Redis 裡,再由另外跑的 Worker 程式去處理。這樣不但能控制同時執行的數量,執行失敗了還能重新嘗試。
這個錯誤訊息代表 PHP 在嘗試將 UTF-8 字串傳遞給 Windows 的 COM 元件時,轉換過程失敗了。這通常發生在 PHP 內部的編碼與 Windows 系統預設的 ANSI 字碼頁(例如 CP950)不一致。
要在 Windows 上徹底解決這個編碼衝突,有幾個調整方向可以嘗試。
調整 PHP 的內部編碼設定
你可以嘗試在程式碼最開頭,強迫 PHP 處理 COM 物件時使用 UTF-8。這通常能解決轉換 Unicode 失敗的問題。
PHP
<?php
// 設定 COM 使用 UTF-8 編碼
ini_set('com.code_page', 65001);
$shell = new COM("WScript.Shell");
$param = "中文內容";
// 使用 /u 參數叫 cmd 用 Unicode 處理
$shell->Run("cmd /u /c my_command.exe " . $param, 0, false);
?>
設定 com.code_page 為 65001 是關鍵,這會告訴 PHP 引擎在呼叫 COM 介面時,直接使用 UTF-8 進行溝通。
使用 PowerShell 作為中繼
如果 cmd.exe 依然對編碼很挑剔,改用 PowerShell 執行通常會更穩定。PowerShell 對 Unicode 的支援比傳統的 cmd 好很多。
PHP
<?php
$param = "測試中文";
$command = "powershell -WindowStyle Hidden -Command \"& {my_script.exe '$param'}\"";
exec($command . " > NUL 2>&1");
?>
這種做法一樣符合你不需要回傳值的需求。透過 -WindowStyle Hidden 可以確保執行時不會彈出黑色視窗,並把輸出導向到 NUL(Windows 的黑洞)。
檢查檔案編碼與路徑
請確保你的 PHP 原始碼檔案本身是 UTF-8(不帶 BOM)格式。如果檔案存成 ANSI,你寫在程式碼裡的變數從一開始就不是 UTF-8 了。
另外,如果路徑中包含特殊字元,建議先用 realpath() 轉換成絕對路徑。有時候相對路徑在背景執行時會因為工作目錄不同而找不到檔案。
改用 powershell 就解決了, 詳細用法參考:
https://stackoverflow.max-everyday.com/2026/01/cmd-powershell/