日誌檔、匯出檔常達百 MB 至數 GB。file()、file_get_contents() 會一次載入整檔到記憶體,觸發 memory_limit 錯誤甚至導致伺服器當機。正解是使用 fseek() 從檔案末尾向前定位,僅讀取必要區塊。
<?php
/**
* 從檔案尾部讀取最後 $n 行
*
* @param resource $fp 檔案指標(fopen 'r' 模式)
* @param int $n 要讀取的行數
* @param int $base 指數基底(預設 5,每次向前跳躍倍數)
* @return array 最後 $n 行陣列(含換行符)
*/
function tail($fp, $n, $base = 5)
{
assert($n > 0);
$pos = $n + 1;
$lines = [];
while (count($lines) <= $n) {
try {
// 從 EOF 向前跳躍 $pos 位元組
fseek($fp, -$pos, SEEK_END);
} catch (Exception $e) {
// 已超過檔案開頭
fseek($fp, 0);
break;
}
$pos *= $base; // 指數增大跳躍距離
// 讀取目前位置到 EOF 的所有內容
while (!feof($fp)) {
array_unshift($lines, fgets($fp));
}
}
return array_slice($lines, 0, $n);
}
// 使用範例
$lines = tail(fopen('access.log', 'r'), 10);
print_r($lines);
?>
<?php
$fp = fopen('access.log', 'r');
$line = 10;
$pos = -2;
$t = ' ';
$data = '';
while ($line > 0) {
// 向前找換行符
while ($t !== "\n") {
fseek($fp, $pos, SEEK_END);
$t = fgetc($fp);
$pos--;
}
$t = ' ';
$data .= fgets($fp); // 讀取該行
$line--;
}
fclose($fp);
echo $data;
?>
<?php
$fp = fopen('access.log', 'r');
$num = 10; // 要讀取的行數
$chunk = 4096; // 每次讀取 4KB
$fs = sprintf('%u', filesize('access.log'));
$max = ($fs == PHP_INT_MAX) ? PHP_INT_MAX : (int)$fs;
$readData = '';
for ($len = 0; $len < $max; $len += $chunk) {
$seekSize = ($max - $len > $chunk) ? $chunk : $max - $len;
// 從 EOF 向前 $seekSize 位置開始讀取
fseek($fp, -($len + $seekSize), SEEK_END);
$readData = fread($fp, $seekSize) . $readData;
// 累積換行符數量超過需求即停止
if (substr_count($readData, "\n") >= $num + 1) {
preg_match("!(.*?\n){{$num}}\$!", $readData, $match);
$data = $match[0];
break;
}
}
fclose($fp);
echo $data;
?>
| 方法 | 時間複雜度 | 適用場景 | 備註 |
|---|---|---|---|
| 指數退避 | O(log n) 跳躍 + O(k) 讀取 | 行長度極不固定、極長行 | 跳躍次數少,但可能重複讀取重疊區塊 |
| 逐字元掃描 | O(m) m=尾部位元組數 | 極小檔、教學示範 | 每字元一個 fseek/fgetc,系統呼叫開銷極大 |
| 區塊讀取(推薦) | O(k) 區塊數 | 通用大檔案、生產環境 | 序列讀取、系統呼叫最少、記憶體固定 |
<?php
$file = 'access.log';
$file = escapeshellarg($file); // 防指令注入
$lastLine = `tail -n 1 $file`; // 反引號執行系統指令
echo $lastLine;
?>
exec()/shell_exec()(共享主機常禁用)、Windows 不支援、安全性須嚴格過濾參數。ini_set('memory_limit', '-1'); // 移除限制,極度危險
$lines = file('huge.log'); // 依然會吃光實體記憶體
僅適用於「確定檔案小於可用記憶體」的一次性腳本,長期服務嚴禁。