讀取 GB 級日誌文件尾部幾行是經典場景。常用 file()、file_get_contents() 會撐爆記憶體(預設 memory_limit=16M)。本文實測四種方案,附執行耗時數據,助你選型。
access.log(800MB,約 500 萬行)| 方案 | 核心思路 | 耗時 | 評價 |
|---|---|---|---|
| 1. file() 全讀入 | 一次性載入記憶體 | 116.96 秒 | 機器假死 20 分鐘,**嚴禁生產使用** |
| 2. shell_exec tail | 調用系統 tail -n 10 | 0.0034 秒 | 最快、最簡單,依賴 Linux 環境、escapeshellarg 防注入 |
| 3. fseek 逐字元反向讀 | 從 EOF 向前逐字元找換行符 | 0.0095 秒 | 純 PHP、跨平台,邏輯簡單但系統調用多 |
| 4. fseek 分塊反向讀 (4KB) | 每次讀 4KB 塊,統計換行符數 | 0.0009 秒 | **推薦純 PHP 方案**,I/O 最少,效能極佳 |
| 5. fseek 指數退避法 | 指數增長跳躍讀取,配合 fgets | 0.0003 秒 | 最快純 PHP 算法,代碼較複雜 |
| 6. stream_get_line | 流式逐行讀取 | ~20 秒 (50萬行) | 適合順序讀取大文件,非尾部查詢最優 |
<?php
$file = 'access.log';
$file = escapeshellarg($file); // 防命令注入
$line = `tail -n 1 $file`; // 反引號執行命令
echo $line;
<?php
$fp = fopen($file, 'r');
$num = 10;
$chunk = 4096;
$fs = sprintf('%u', filesize($file));
$max = ($fs == PHP_INT_MAX) ? PHP_INT_MAX : filesize($file);
$readData = '';
for ($len = 0; $len < $max; $len += $chunk) {
$seekSize = ($max - $len > $chunk) ? $chunk : $max - $len;
fseek($fp, ($len + $seekSize) * -1, 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;
<?php
function tail($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);
}
var_dump(tail(fopen('access.log', 'r+'), 10));
<?php
$fp = fopen('./iis.log', 'r');
$logArray = [];
while (!feof($fp)) {
$logArray[] = stream_get_line($fp, 65535, "\n");
}
// 處理 $logArray...
tail,最簡潔高效。stream_get_line 或 fgets 逐行流式處理,嚴禁一次性載入。