讀取 400MB+ 日誌檔、甚至 GB 級大檔,file()、file_get_contents() 會撐爆記憶體。本文實測四種方案,從「全載入」到「指針跳躍」,效能差距達四個數量級。
access.log(約 1000 萬行)<?php
ini_set('memory_limit', '-1');
$data = file('testfile.txt');
$line = $data[count($data) - 1000];
?>
結果:7.87 秒,記憶體飆升至 1.2 GB+。生產禁用。
tail 指令(最快,但平台相依)<?php
$file = escapeshellarg('testfile.txt');
$line = `tail -n 100 $file`; // 反引號執行指令
?>
結果:幾毫秒,系統級優化。缺點:Windows 不支援、需 escapeshellarg 防注入、依賴外部程序。
<?php
$fp = fopen('testfile.txt', 'r');
$line = 100;
$pos = -2;
while ($line > 0) {
// 往前找換行符
while (fgetc($fp) !== "\n") {
fseek($fp, $pos--, SEEK_END);
}
$data = fgets($fp); // 讀取整行
$line--;
}
fclose($fp);
?>
結果:0.34 秒。純 PHP、跨平台、記憶體常數級。推薦通用方案。
每次讀 4KB 塊,從檔尾向前,累積直到湊夠行數:
<?php
function tailFile(string $file, int $lines = 100, int $chunk = 4096): string {
$fp = fopen($file, 'r');
$size = (int)filesize($file);
$buffer = '';
for ($offset = 0; $offset < $size; $offset += $chunk) {
$seek = min($chunk, $size - $offset);
fseek($fp, -($offset + $seek), SEEK_END);
$buffer = fread($fp, $seek) . $buffer;
if (substr_count($buffer, "\n") >= $lines + 1) {
// 多讀一行防止截斷,用正則精確截取
$extra = substr_count($buffer, "\n") - $lines + 1;
preg_match('/(.*?\n){' . $extra . '}/', $buffer, $m);
fclose($fp);
return $m[1];
}
}
fclose($fp);
return $buffer;
}
?>
結果:0.002 秒,比逐字元快 170 倍,記憶體極低。大檔尾部讀取的最佳純 PHP 實現。
| 方法 | 耗時 | 記憶體 | 跨平台 | 推薦度 |
|---|---|---|---|---|
| file() | 7.87s | 1.2GB+ | ✅ | ❌ 禁用 |
| tail 指令 | ~0.005s | 極低 | ❌ Linux only | ⚠️ 受限環境 |
| fseek 逐字元 | 0.34s | 極低 | ✅ | ✅ 通用備選 |
| fseek 分塊 | 0.002s | 極低 | ✅ | ⭐ 生產首選 |
若需完整遍歷大檔(如匯入、ETL),用 Generator 避免載入全部:
<?php
function readLines(string $file): Generator {
$fp = fopen($file, 'r');
while (($line = fgets($fp)) !== false) {
yield rtrim($line);
}
fclose($fp);
}
foreach (readLines('huge.log') as $line) {
// 處理單行,記憶體恆定
}
?>
大檔讀取核心原則:拒絕全載入、善用指針跳躍、分塊緩衝。純 PHP 環境下,fseek + fread 分塊 是效能與相容性的最佳平衡;Linux 專用環境可直接呼叫 tail 系統指令極致提速。