文件操作是 PHP 後端開發基礎技能:日誌記錄、配置管理、模板渲染、靜態化生成、數據導入導出等場景無處不在。本文系統梳理 fopen 模式、讀取寫入函數、指針控制、鎖機制、編碼陷阱,並提供可直接用於生產的封裝函數。
| 函數 | 作用 | 關鍵參數 |
|---|---|---|
fopen(filename, mode) | 打開文件,返回資源句柄 | 模式詳見下表 |
fwrite(handle, string, length?) | 寫入內容(二進制安全) | 可選長度限制 |
fread(handle, length) | 讀取指定字節數 | 必須指定長度 |
fgets(handle, length) | 逐行讀取(含換行符) | 最大長度,預設 1024 |
fgetcsv(handle, length, separator) | 解析 CSV 行 | 自動處理引號、轉義 |
file_get_contents(path) | 一次性讀取整個文件到字串 | PHP 4.3+,支援 URL |
file_put_contents(path, data, flags) | 一次性寫入(原子性可选) | FILE_APPEND, LOCK_EX |
fclose(handle) | 關閉句柄,釋放鎖 | 必須調用 |
feof(handle) | 判斷是否到達 EOF | 循環讀取終止條件 |
flock(handle, operation) | 文件鎖(共享/獨佔/非阻塞) | LOCK_SH, LOCK_EX, LOCK_NB |
| 模式 | 讀 | 寫 | 指針位置 | 截斷文件 | 文件不存在 | 說明 |
|---|---|---|---|---|---|---|
r | ✅ | ❌ | 開頭 | 否 | 失敗 | 只讀 |
r+ | ✅ | ✅ | 開頭 | 否 | 失敗 | 讀寫 |
w | ❌ | ✅ | 開頭 | ✅ | 創建 | 只寫,清空內容 |
w+ | ✅ | ✅ | 開頭 | ✅ | 創建 | 讀寫,清空內容 |
a | ❌ | ✅ | 結尾 | 否 | 創建 | 追加寫入 |
a+ | ✅ | ✅ | 結尾 | 否 | 創建 | 讀取+追加 |
x | ❌ | ✅ | 開頭 | 否 | 創建/失敗 | 獨佔創建,存在則失敗 |
x+ | ✅ | ✅ | 開頭 | 否 | 創建/失敗 | 獨佔創建讀寫 |
c | ❌ | ✅ | 開頭 | 否 | 創建 | 不截斷,PHP 5.2.6+ |
c+ | ✅ | ✅ | 開頭 | 否 | 創建 | 讀寫不截斷 |
**二進制安全**:Windows 下建議加 b 後綴(如 rb、wb),避免 \r\n 轉換導致位置偏移。
<?php
function safeWrite(string $path, string $data): bool {
$dir = dirname($path);
if (!is_dir($dir)) mkdir($dir, 0755, true);
$tmp = $path . '.tmp.' . random_bytes(8);
$fp = fopen($tmp, 'wb');
if (!$fp) return false;
if (flock($fp, LOCK_EX)) {
$written = fwrite($fp, $data);
flock($fp, LOCK_UN);
fclose($fp);
if ($written === false || $written !== strlen($data)) {
@unlink($tmp);
return false;
}
return rename($tmp, $path); // 原子替換
}
fclose($fp);
@unlink($tmp);
return false;
}
?>
<?php
function replaceInFile(string $path, string $search, string $replace): bool {
$tmp = $path . '.replace.tmp';
$rfp = fopen($path, 'rb');
$wfp = fopen($tmp, 'wb');
if (!$rfp || !$wfp) { @fclose($rfp); @fclose($wfp); @unlink($tmp); return false; }
while (($line = fgets($rfp)) !== false) {
fwrite($wfp, str_replace($search, $replace, $line));
}
fclose($rfp);
fclose($wfp);
return rename($tmp, $path);
}
?>
<?php
function appendLog(string $logDir, string $message, int $maxSize = 10*1024*1024): void {
$date = date('Y-m-d');
$file = $logDir . "/app_{$date}.log";
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
// 日誌輪轉
if (file_exists($file) && filesize($file) > $maxSize) {
$rotate = $logDir . "/app_{$date}_" . time() . ".log";
rename($file, $rotate);
}
$fp = fopen($file, 'ab'); // 追加二進制
if (!$fp) return;
// 非阻塞獨佔鎖,獲取失敗直接丟棄(日誌允許丟失極少量)
if (flock($fp, LOCK_EX | LOCK_NB)) {
fwrite($fp, date('c') . " {$message}\n");
flock($fp, LOCK_UN);
}
fclose($fp);
}
?>
| 坑點 | 後果 | 解決 |
|---|---|---|
fopen('r') 打開不存在文件 | 返回 false,後續 fread 崩潰 | 先 file_exists 或檢查返回值 |
大文件 file_get_contents | 記憶體耗盡 (OOM) | 改用 fgets 逐行/分塊處理 |
Windows \r\n 換行差異 | fgets 讀取含 \r,邏輯判斷失效 | 用 rtrim($line, "\r\n") 或 fopen(..., 'rb') |
| 並發寫入無鎖 | 數據交錯、損壞 | 必須 flock(LOCK_EX) |
| 長時間持有鎖 | 併發阻塞、超時 | 最小化臨界區、用 LOCK_NB 失敗快速返回 |
未 fclose | 句柄洩漏、鎖不釋放、數據未刷盤 | try/finally 或 register_shutdown_function 保證關閉 |
| 編碼不一致(UTF-8 vs GBK) | 中文亂碼、替換失效 | 統一 UTF-8,或用 mb_convert_encoding 處理 |
<?php
// 逐行遍歷
$file = new SplFileObject('data.csv');
$file->setFlags(SplFileObject::READ_CSV);
foreach ($file as $row) {
// $row 已解析為陣列
}
// 寫入
$file = new SplFileObject('output.txt', 'w');
$file->fwrite("第一行\n");
$file->fwrite("第二行\n");
// 自動 fclose
?>
優點:自動資源管理、內建 CSV 解析、可迭代、支援 seek() 隨機訪問。
PHP 文件操作看似基礎,實則蘊含「模式選擇、鎖策略、編碼處理、大文件流式處理、原子性寫入」等工程細節。掌握 fopen 全模式、善用 flock、避開 file_get_contents 大文件陷阱、採用臨時文件+重命名實現原子寫入,是從「能跑通」到「生產可靠」的關鍵躍升。