讀取超大日志文件(GB 級)尾部數據時,file()/file_get_contents() 會撐爆內存。fseek() 配合文件指針操作,只讀取必要區塊,是標準解法。本文對比「逐字節回溯」與「塊讀取+換行符計數」兩種方法的性能差異。
function tail_seek($fp, $n, $base = 5) {
assert($n > 0);
$pos = $n + 1;
$lines = [];
while (count($lines) <= $n) {
try {
fseek($fp, -$pos, SEEK_END);
} catch (Exception $e) {
fseek($fp, 0);
break;
}
$pos *= $base; // 指數增大回溯範圍
while (!feof($fp)) {
array_unshift($lines, fgets($fp));
}
}
return array_slice($lines, 0, $n);
}
// 使用
$fp = fopen('access.log', 'r');
var_dump(tail_seek($fp, 10));
fclose($fp);
耗時:~0.0095 秒(100MB 文件測試)
function tail_block($file, $lineNum = 10) {
$fp = fopen($file, 'r');
$pos = -2;
$t = ' ';
$data = '';
while ($lineNum > 0) {
// 向前逐字節找換行符
while ($t !== "\n") {
fseek($fp, $pos, SEEK_END);
$t = fgetc($fp);
$pos--;
}
$t = ' ';
$data .= fgets($fp); // 讀取整行
$lineNum--;
}
fclose($fp);
return $data;
}
// 更優化:按塊讀取(如 4KB/8KB)
function tail_block_optimized($file, $lines = 10, $blockSize = 8192) {
$fp = fopen($file, 'r');
$fileSize = fstat($fp)['size'];
$pos = $fileSize;
$buffer = '';
$found = 0;
while ($pos > 0 && $found < $lines) {
$readSize = min($blockSize, $pos);
$pos -= $readSize;
fseek($fp, $pos);
$chunk = fread($fp, $readSize);
$buffer = $chunk . $buffer;
// 統計換行符
$count = substr_count($buffer, "\n");
if ($count >= $lines) {
$parts = explode("\n", $buffer);
// 取最後 $lines 行
return implode("\n", array_slice($parts, -$lines));
}
$found = $count;
}
fclose($fp);
return $buffer;
}
耗時:~0.0009 秒(同測試環境,快 10 倍)
| 指標 | 方法一(指數回溯) | 方法二(塊讀取) |
|---|---|---|
| 系統調用次數 | 多次 fseek + 多次 fgets | 少量 fseek + 少量 fread |
| 內存佔用 | 僅存目標行 | 緩衝區 ~塊大小 |
| 適合場景 | 極小行數、簡單腳本 | 生產環境、高頻調用、大文件 |
| 邊界處理 | 需 try-catch 防越界 | 自然處理文件頭 |
class LogTailer {
private $file;
private $encoding = 'UTF-8';
public function __construct(string $file) {
$this->file = $file;
}
public function lastLines(int $n = 50, int $blockSize = 8192): array {
$fp = fopen($this->file, 'rb');
if (!$fp) throw new RuntimeException("Cannot open {$this->file}");
$size = fstat($fp)['size'];
$pos = $size;
$buf = '';
$lines = [];
while ($pos > 0 && count($lines) < $n) {
$read = min($blockSize, $pos);
$pos -= $read;
fseek($fp, $pos);
$chunk = fread($fp, $read);
$buf = $chunk . $buf;
$parts = explode("\n", $buf);
// 保留不完整的首行在 buf 中
$buf = array_shift($parts);
$lines = array_merge($parts, $lines);
}
fclose($fp);
// 編碼轉換
if ($this->encoding !== 'UTF-8') {
$lines = array_map(fn($l) => mb_convert_encoding($l, 'UTF-8', $this->encoding), $lines);
}
return array_slice($lines, -$n);
}
// 實時尾部跟蹤(類似 tail -f)
public function follow(callable $callback, int $interval = 100000) {
$fp = fopen($this->file, 'r');
fseek($fp, 0, SEEK_END);
while (true) {
$line = fgets($fp);
if ($line) $callback(rtrim($line));
else usleep($interval);
}
}
}
mb_convert_encoding\r\n、Linux \n、老 Mac \r,建議統一按 \n 分割再 rtrimfollow() 需監控 inode 重新打開exec('tail -n 100 ' . escapeshellarg($file)):系統命令最高效,但依賴環境、有注入風險SplFileObject + fseek:面向對象封裝,行為一致掌握 fseek 塊讀取模式,是 PHP 開發者處理大文件、日志分析、實時監控的必備技能。