文件操作基礎流程
PHP 文件操作遵循「打開 → 讀寫 → 關閉」三步驟,核心是資源句柄管理。本文系統梳理 8 大類常用操作與最佳實踐。
1. 打開文件:fopen()
<?php
// 模式說明
$modes = [
'r' => '只讀,指針開頭',
'r+' => '讀寫,指針開頭',
'w' => '只寫,截斷/創建,指針開頭',
'w+' => '讀寫,截斷/創建,指針開頭',
'a' => '只寫,指針結尾(追加),創建',
'a+' => '讀寫,指針結尾(追加),創建',
'x' => '只寫,僅創建(存在失敗)',
'x+' => '讀寫,僅創建(存在失敗)',
'b' => '二進制模式(推薦始終加上)',
't' => '文本模式(Windows換行轉換)'
];
// 實際使用
$fp = fopen('log.txt', 'rb'); // 相對路徑
$fp = fopen('../data/config.ini', 'rb'); // 相對路徑(上級)
$fp = fopen('https://example.com/api', 'rb'); // URL(需 allow_url_fopen=On)
$fp = fopen('ftp://user:pass@ftp.example.com/file', 'rb'); // FTP
// 安全打開模式
$filename = 'user_data.json';
$fp = @fopen($filename, 'rb');
if ($fp === false) {
throw new RuntimeException("無法打開文件: $filename");
}
?>
2. 關閉文件:fclose()
<?php
// 必須顯式關閉,確保緩衝區刷新、釋放鎖、避免資源洩漏
fclose($fp);
// 最佳實踐:使用 try-finally 或註冊關閉函數
try {
$fp = fopen('data.txt', 'wb');
// ... 操作
} finally {
if (isset($fp)) fclose($fp);
}
// 或使用自動資源管理 (PHP 8.0+)
$fp = fopen('data.txt', 'wb');
register_shutdown_function('fclose', $fp);
?>
3. 讀取文件:多種方式對比
| 函數 | 用途 | 特點 | 適用場景 |
| fgets() | 逐行讀取 | 內存效率高、保留換行符 | 大文本文件、日志分析 |
| fgetss() | 逐行讀取+去標籤 | 已棄用 PHP 8.0 | 不推薦使用 |
| fgetc() | 逐字符讀取 | 極慢、精確控制 | 特殊解析需求 |
| fread() | 二進制塊讀取 | 高性能、需指定長度 | 圖片、大文件、網絡流 |
| file_get_contents() | 一次性讀全文 | 簡潔、大文件內存風險 | 小配置文件、API 響應 |
| file() | 讀入數組(每行一元素) | 自動去換行(可選)、內存佔用大 | 行處理邏輯 |
| SplFileObject | 面向對象迭代器 | 功能豐富、可 Seek、CSV 解析 | 現代 PHP 項目首選 |
大文件高效讀取模式
<?php
// 1. 塊讀取 (推薦 8KB-1MB)
$handle = fopen('large.log', 'rb');
while (!feof($handle)) {
$chunk = fread($handle, 8192); // 8KB
processChunk($chunk);
}
fclose($handle);
// 2. 行迭代 (內存友好)
$handle = fopen('large.csv', 'rb');
while (($line = fgets($handle)) !== false) {
processLine(rtrim($line));
}
fclose($handle);
// 3. SplFileObject (現代最佳)
$file = new SplFileObject('large.csv', 'r');
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD);
foreach ($file as $fields) {
processCsvRow($fields);
}
?>
4. 文件指針狀態判斷
<?php
$fp = fopen('data.bin', 'rb');
// 當前位置
$pos = ftell($fp); // 字節偏移
// 文件大小
$size = fstat($fp)['size']; // 或 filesize('data.bin')
// 是否到達末尾
if (feof($fp)) { /* EOF */ }
// 移動指針
fseek($fp, 0, SEEK_SET); // 開頭
fseek($fp, -100, SEEK_END); // 倒數 100 字節
fseek($fp, 50, SEEK_CUR); // 當前位置 +50
// 重置指針
rewind($fp); // 等同 fseek($fp, 0, SEEK_SET)
?>
5. 寫入文件
<?php
$filename = 'output.txt';
$content = "數據內容\n";
// 文本追加寫入
if (is_writable($filename)) {
$fp = fopen($filename, 'ab'); // 追加二進制
if ($fp) {
$written = fwrite($fp, $content);
if ($written === false) {
throw new RuntimeException("寫入失敗");
}
fclose($fp);
}
}
// 二進制寫入 (必須指定長度)
$binaryData = random_bytes(1024);
$fp = fopen('binary.dat', 'wb');
fwrite($fp, $binaryData, strlen($binaryData)); // 或 mb_strlen($data, '8bit')
fclose($fp);
// 原子寫入 (防止部分寫入被讀取)
$tmpFile = tempnam(sys_get_temp_dir(), 'atomic_');
file_put_contents($tmpFile, $content);
rename($tmpFile, $filename); // 同文件系統下原子操作
?>
6. 文件屬性與權限檢查
| 函數 | 返回值 | 說明 |
| file_exists() | bool | 文件/目錄是否存在 |
| is_file() | bool | 是否普通文件 |
| is_dir() | bool | 是否目錄 |
| is_readable() | bool | 當前進程可讀 |
| is_writable() | bool | 當前進程可寫 |
| is_executable() | bool | 可執行 |
| filesize() | int|false | 文件大小(字節) |
| filemtime() | int|false | 最後修改時間戳 |
| filectime() | int|false | inode 變更時間 |
| fileatime() | int|false | 最後訪問時間 |
| fileperms() | int|false | 權限位掩碼 |
| fileowner() | int|false | 擁有者 UID |
| filegroup() | int|false | 所屬組 GID |
| filetype() | string|false | file/dir/link/char/block/fifo/socket |
權限掩碼解析
<?php
$perms = fileperms('script.php');
if ($perms !== false) {
// 類型
$type = ($perms & 0xF000) >> 12;
// 擁有者權限
$owner = ($perms & 0x01C0) >> 6;
// 組權限
$group = ($perms & 0x0038) >> 3;
// 其他權限
$other = $perms & 0x0007;
printf("類型: %o, 權限: %o%o%o\n", $type, $owner, $group, $other);
// 輸出如: 類型: 10, 權限: 644 (普通文件 rw-r--r--)
}
?>
7. 刪除與重命名
<?php
// 刪除文件
unlink('temp.txt'); // 失敗返回 false,不拋異常
// 刪除目錄 (必須為空)
rmdir('empty_dir');
// 遞刪除目錄樹
function rrmdir($dir) {
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = "$dir/$file";
is_dir($path) ? rrmdir($path) : unlink($path);
}
rmdir($dir);
}
// 重命名/移動 (跨文件系統可能失敗)
rename('old_name.txt', 'new_name.txt');
rename('dir/file.txt', 'other_dir/file.txt'); // 移動
?>
8. 目錄操作
路徑處理函數
<?php
$path = '/var/www/html/index.php';
echo basename($path); // index.php
echo basename($path, '.php'); // index
echo dirname($path); // /var/www/html
echo realpath('../config'); // /var/www/config (解析絕對路徑)
$info = pathinfo($path);
/*
[
'dirname' => '/var/www/html',
'basename' => 'index.php',
'extension' => 'php',
'filename' => 'index'
]
*/
?>
目錄遍歷對比
| 方法 | 風格 | 特點 | 推薦度 |
| opendir/readdir | 過程式 | 底層、需手動管理句柄 | ⭐⭐ |
| scandir() | 函數式 | 返回數組、含 . 和 ..、大目錄內存壓力 | ⭐⭐⭐ |
| glob() | 模式匹配 | 支持通配符、直接過濾 | ⭐⭐⭐⭐ |
| DirectoryIterator | OO 迭代器 | 內存友好、豐富屬性方法 | ⭐⭐⭐⭐⭐ |
| RecursiveDirectoryIterator | 遞歸 OO | 遞歸遍歷、配合 FilterIterator | ⭐⭐⭐⭐⭐ |
| FilesystemIterator | 增強迭代器 | 跳過 . ..、路徑對象、可鏈式 | ⭐⭐⭐⭐⭐ |
現代遍歷最佳實踐
<?php
// 1. 非遞歸:FilesystemIterator
$dir = new FilesystemIterator(__DIR__, FilesystemIterator::SKIP_DOTS);
foreach ($dir as $fileinfo) {
echo $fileinfo->getFilename() . ' (' . $fileinfo->getSize() . " bytes)\n";
}
// 2. 遞歸 + 過濾:僅獲取 .php 文件
$phpFiles = new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(__DIR__, RecursiveDirectoryIterator::SKIP_DOTS)
),
'/\.php$/i'
);
foreach ($phpFiles as $file) {
echo $file->getPathname() . "\n";
}
// 3. 目錄操作
mkdir('new_dir', 0755, true); // 遞歸創建
chdir('new_dir'); // 改變工作目錄
echo getcwd(); // 獲取當前工作目錄
?>
錯誤處理與性能優化
統一異常處理
<?php
function safeFileOperation(callable $operation, string $context = '') {
try {
return $operation();
} catch (Throwable $e) {
error_log("文件操作失敗 [$context]: " . $e->getMessage());
throw new RuntimeException("文件操作失敗: $context", 0, $e);
}
}
// 使用
$content = safeFileOperation(function() {
return file_get_contents('config.json');
}, '讀取配置文件');
?>
性能優化清單
- 大文件流式處理:避免
file_get_contents 讀入內存
- 減少 stat 系統調用:緩存
file_exists/filesize 結果
- 使用 opcache:PHP 文件本身受益於字節碼緩存
- 批量操作:合併多次寫入、使用
flock 適當鎖定
- SSD/NFS 調優:根據存儲介質調整塊大小、並發數
總結
PHP 文件系統 API 豐富但易混淆,現代開發建議:
- 優先使用 SplFileObject / FilesystemIterator 面向對象接口
- 大文件採用 流式讀寫,小文件用
file_get_contents/file_put_contents
- 路徑操作統一用
pathinfo/realpath,避免手動拼接
- 關鍵操作添加 原子寫入、異常處理、權限檢查
- 目錄遍歷根據深度選擇
glob 或 RecursiveDirectoryIterator