文件讀取是 PHP 最基礎的 I/O 操作,但**「沒有最好的方法,只有最適合場景的方法」**。從一次性讀入內存到流式逐塊處理,從二進制安全到文本行處理,五大核心方法各有優勢局限。本文將從**內存佔用、執行速度、適用文件大小、編碼處理、錯誤控制**五個維度深度對比,並給出生產環境選型決策樹。
| 方法 | 核心函數 | 讀取模式 | 內存佔用 | 適用文件大小 | 二進制安全 | 行處理 | 典型場景 |
|---|---|---|---|---|---|---|---|
| 1. fread | fread($fp, $length) | 指定長度/全量 | 高 (全量讀入) | < 可用內存 | ✅ 是 | 手動分割 | 二進制文件、小配置文件、需精確控制字節 |
| 2. file_get_contents | file_get_contents($path) | 全量一次性 | 高 (全量讀入) | < 可用內存 | ✅ 是 | 手動分割 | **小文件首選**、API 響應、模板讀取、靜態資源內聯 |
| 3. 流式 fread 循環 | fread($fp, $buffer) + feof() | 固定緩衝區塊 | **極低 (常數)** | **無上限 (GB/TB)** | ✅ 是 | 手動分割 | 大文件處理、視頻流、日誌分析、加密/壓縮流 |
| 4. file() | file($path) | 全量讀入→按行分割數組 | 高 (全量+數組開銷) | < 內存/10 | ✅ 是 | ✅ **原生逐行** | 中小文本文件、CSV/日誌逐行處理、配置解析 |
| 5. fgets() | fgets($fp, $length) | 逐行流式讀取 | **低 (單行緩衝)** | **大文本文件** | ✅ 是 | ✅ **原生逐行** | 超大日誌、CSV 流式解析、內存受限環境逐行處理 |
<?php
$path = 'data.bin';
$fp = fopen($path, 'rb'); // 'b' 標誌:二進制模式 (跨平台兼容)
if ($fp) {
// 讀取前 1024 字節 (文件頭解析)
$header = fread($fp, 1024);
// 讀取整個文件 (已知大小)
$size = filesize($path);
$content = fread($fp, $size); // 從當前指針位置讀取
// 關鍵:fread 遇到 EOF 返回空字串 "", 非 false
fclose($fp);
}
// 二進制安全讀取圖片並輸出
header('Content-Type: image/jpeg');
echo file_get_contents('photo.jpg'); // 更簡潔
?>
優勢:精確控制讀取位置 (配合 fseek) 和長度;二進制安全。
劣勢:全量讀取大文件會 OOM;需手動管理 fopen/fclose。
<?php
// 1. 讀取本地文件
$content = file_get_contents('config.json');
$config = json_decode($content, true);
// 2. 讀取遠程 HTTP/HTTPS (需 allow_url_fopen=On)
$html = file_get_contents('https://api.example.com/data');
// 3. 上下文控制 (超時、Header、POST、代理)
$ctx = stream_context_create([
'http' => [
'timeout' => 10,
'method' => 'GET',
'header' => "User-Agent: MyApp/1.0\r\nAccept: application/json",
],
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] // 開發環境
]);
$json = file_get_contents('https://api.example.com/data', false, $ctx);
// 4. 部分讀取 (offset, maxlen) - PHP 5.1+
$chunk = file_get_contents('huge.log', false, null, 1000, 500); // 從1000字節讀500字節
?>
優勢:代碼極簡、內部優化 (mmap)、支援流上下文 (HTTP/FTP/SSL/壓縮)、可部分讀取。
劣勢:大文件全量讀入內存 → OOM 風險;無原生逐行迭代。
<?php
function processLargeFile(string $path, callable $chunkHandler, int $bufferSize = 8192): void {
$fp = fopen($path, 'rb');
if (!$fp) throw new RuntimeException("無法打開: $path");
try {
while (!feof($fp)) {
$chunk = fread($fp, $bufferSize);
if ($chunk === '') break; // EOF
$chunkHandler($chunk); // 處理塊 (寫入另文件、發送網絡、哈希、解密)
}
} finally {
fclose($fp);
}
}
// 使用:計算大文件 SHA256 (不載入內存)
$hash = hash_init('sha256');
processLargeFile('video.mp4', function($chunk) use ($hash) {
hash_update($hash, $chunk);
});
echo hash_final($hash);
// 使用:分塊發送大文件下載 (斷點續傳核心)
function sendFileChunked(string $path, int $start, int $length, int $buffer = 8192) {
$fp = fopen($path, 'rb');
fseek($fp, $start);
$remaining = $length;
while ($remaining > 0 && !feof($fp)) {
$read = min($buffer, $remaining);
echo fread($fp, $read);
$remaining -= $read;
@ob_flush(); @flush(); // 刷新輸出緩衝
}
fclose($fp);
}
?>
核心優勢:**內存佔用恆定 ≈ $bufferSize** (默認 8KB),可處理 TB 級文件。
關鍵細節:feof() 在**讀取嘗試後**才為 true;fread 返回 '' (空字串) 表示 EOF,非 false。
<?php
// 1. 直接獲取行數組 (保留換行符)
$lines = file('data.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// FILE_IGNORE_NEW_LINES: 移除每行末尾 \n/\r\n
// FILE_SKIP_EMPTY_LINES: 跳過空行
// 2. 處理 CSV (簡易)
foreach ($lines as $line) {
$fields = str_getcsv($line); // 解析 CSV 行
// 處理...
}
// 3. 反轉文件行序 (需全量內存)
$reversed = array_reverse($lines);
file_put_contents('reversed.txt', implode("\n", $reversed));
?>
優勢:一行代碼獲取行數組、原生支援跳過空行/去換行符。
劣勢:**內存 = 文件大小 + 數組開銷 (約 2-3x)**;大文件極易 OOM;FILE_SKIP_EMPTY_LINES 會跳過包含空白字符的行 (可能非預期)。
<?php
function processLargeTextFile(string $path, callable $lineHandler, int $bufferSize = 0): void {
$fp = fopen($path, 'r'); // 文本模式 (自動處理換行符)
if (!$fp) throw new RuntimeException("無法打開: $path");
try {
// $bufferSize = 0 或省略 = 默認 1024 字節 (舊版) / 8192 (新版)
// 設置較大緩衝 (如 64KB) 可減少系統調用,提升大文件速度
while (($line = fgets($fp, $bufferSize ?: 65536)) !== false) {
// $line 包含換行符 (\n 或 \r\n)
$line = rtrim($line, "\r\n"); // 移除換行
$lineHandler($line);
}
} finally {
fclose($fp);
}
}
// 使用:逐行處理 10GB 訪問日誌 (內存恆定 ~64KB)
$ipCounts = [];
processLargeTextFile('access.log', function($line) use (&$ipCounts) {
// 簡易解析: IP - - [time] "request" ...
if (preg_match('/^(\S+)/', $line, $m)) {
$ipCounts[$m[1]] = ($ipCounts[$m[1]] ?? 0) + 1;
}
}, 65536); // 64KB 緩衝
arsort($ipCounts);
print_r(array_slice($ipCounts, 0, 10));
// 使用:流式 CSV 解析 (配合 fgetcsv 更佳)
$fp = fopen('huge.csv', 'r');
while (($row = fgetcsv($fp)) !== false) {
// $row 為字段數組
}
fclose($fp);
?>
優勢:**真正的流式逐行**,內存恆定;原生處理換行符;配合 fgetcsv 解析 CSV 完美。
劣勢:僅適用文本文件;二進制文件用 fread;極長行 (超過緩衝) 會被截斷 (需增大 $bufferSize)。
| 方法 | 耗時 | 峰值內存 | 備註 |
|---|---|---|---|
| file_get_contents | ~120ms | ~100MB | 最快,但佔內存 |
| file() | ~180ms | ~280MB | 數組開銷大 |
| fread (8KB buffer) | ~250ms | ~2MB | 流式,穩定 |
| fgets (64KB buffer) | ~220ms | ~2MB | 逐行流式最快 |
| fread (1MB buffer) | ~150ms | ~3MB | 大緩衝接近 file_get_contents 速度 |
結論:
• 小文件 (<10MB):**file_get_contents 或 file()** 最快開發體驗。
• 大文件 (>50MB) / 內存受限:**fread 循環 (二進制) 或 fgets/fgetcsv (文本)** 唯一可行。
• 緩衝區大小對流式速度影響大:64KB-256KB 為甜點,平衡系統調用開銷與內存。
<?php
// 1. 統一換行符為 \n (內部處理)
$content = str_replace(["\r\n", "\r"], "\n", $content);
// 2. 輸出時轉為平台格式 (Web 通常用 \n 或
)
// PHP_EOL 為當前系統換行符 (Linux \n, Windows \r\n)
file_put_contents($path, implode(PHP_EOL, $lines));
// 3. 編碼轉換 (讀取非 UTF-8 文件)
$content = file_get_contents('gbk.txt');
$utf8 = iconv('GBK', 'UTF-8//IGNORE', $content); // //IGNORE 忽略非法字符
// 4. BOM 處理 (UTF-8 BOM: EF BB BF)
if (strncmp($content, "\xEF\xBB\xBF", 3) === 0) {
$content = substr($content, 3);
}
?>
<?php
// 現代模式:try/finally 確保關閉
function safeRead(string $path): ?string {
$fp = @fopen($path, 'rb');
if (!$fp) return null; // 或拋異常
try {
// 讀取邏輯...
$content = stream_get_contents($fp); // 從當前指針讀取剩餘 (流式全量)
return $content;
} finally {
fclose($fp); // 異常也會執行
}
}
// 流上下文超時控制 (file_get_contents/fopen 通用)
$ctx = stream_context_create(['http' => ['timeout' => 5]]);
$content = @file_get_contents('https://slow.api', false, $ctx);
if ($content === false) { /* 處理超時 */ }
?>
需讀取文件?
│
├─ 文件 < 10MB 且 內存充裕 ?
│ ├─ 是 → 需逐行處理/CSV? → file() / fgetcsv(fopen)
│ └─ 否 → 整體內容 (JSON/模板/二進制)? → file_get_contents() 【首選】
│
├─ 文件 > 50MB 或 內存受限 / 長時間運行進程 ?
│ ├─ 二進制 (圖片/視頻/壓縮包/加密)? → fread($fp, 64KB-256KB) 循環
│ └─ 文本/日誌/CSV 逐行? → fgets($fp, 64KB) 循環 / fgetcsv($fp)
│
├─ 遠程 HTTP/HTTPS ?
│ └─ file_get_contents + stream_context (超時/Header/代理) 【首選】
│
└─ 需隨機訪問/斷點續傳/部分讀取?
└─ fopen + fseek + fread / file_get_contents($path, false, null, $offset, $maxlen)
PHP 文件讀取無銀彈,**核心取捨在於「內存 vs 功能」**:
fopen 必配 try/finally fclose;大文件**堅決不用**全量讀入函數;編碼統一 UTF-8,換行符內部統一 \n。根據文件類型、大小、內存預算、處理邏輯選擇對應方法,並封裝為統一 FileReader 服務類,即可構建健壯、高效的文件讀取基礎設施。