需從幾百 MB 的日誌/資料檔讀取第 X 行到第 Y 行,file() 會載入整檔導致記憶體爆炸,fgets 逐行跳過又太慢。PHP 5.1+ 引入 SplFileObject::seek() 可直接跳轉至指定行號,大幅提升效能。
getFileLines()<?php
/**
* 讀取檔案指定行範圍(支援大檔案)
*
* @param string $filename 檔案路徑
* @param int $startLine 起始行號(1-based)
* @param int $endLine 結束行號(含)
* @param string $mode fopen 模式,預設 'rb' 二進位讀取
* @return array|string 行內容陣列,或錯誤訊息
*/
function getFileLines(string $filename, int $startLine = 1, int $endLine = 50, string $mode = 'rb') {
if (!is_file($filename) || !is_readable($filename)) {
return 'error: file not found or not readable';
}
if ($startLine < 1 || $endLine < $startLine) {
return 'error: invalid line range';
}
$content = [];
$count = $endLine - $startLine;
// PHP 5.1+ 使用 SplFileObject::seek() 直接跳轉
if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
try {
$fp = new SplFileObject($filename, $mode);
// seek 參數為 0-based 索引
$fp->seek($startLine - 1);
for ($i = 0; $i <= $count; $i++) {
if ($fp->eof()) break;
$content[] = $fp->current();
$fp->next();
}
} catch (RuntimeException $e) {
return 'error: ' . $e->getMessage();
}
}
// 舊版 PHP 退回 fgets 跳過法
else {
$fp = fopen($filename, $mode);
if (!$fp) return 'error: cannot open file';
// 跳過前 $startLine - 1 行
for ($i = 1; $i < $startLine; $i++) {
if (fgets($fp) === false) {
fclose($fp);
return $content; // 檔案行數不足
}
}
// 讀取目標行
for ($i = $startLine; $i <= $endLine; $i++) {
$line = fgets($fp);
if ($line === false) break;
$content[] = $line;
}
fclose($fp);
}
// 過濾空值(false, null, '')
return array_filter($content, function($v) { return $v !== '' && $v !== null && $v !== false; });
}
?>
| 方法 | 耗時 | 記憶體峰值 | 備註 |
|---|---|---|---|
| SplFileObject::seek() | ~0.0012 秒 | ~49 KB | 直接跳轉,極快 |
| fgets 迴圈跳過 | ~0.018 秒 | ~29 KB | 逐行讀取,稍慢但記憶體更低 |
| file() 全載入 | ~0.045 秒 | ~18 MB | 記憶體隨檔案大小線性增長,大檔案不可用 |
<?php
// 讀取 access.log 第 10000-10050 行
$lines = getFileLines('/var/log/nginx/access.log', 10000, 10050);
if (is_array($lines)) {
foreach ($lines as $lineNum => $line) {
$actualLine = 10000 + $lineNum;
echo "[$actualLine] $line";
}
} else {
echo "錯誤: $lines";
}
// 取得檔案總行數(不載入內容)
function countFileLines(string $filename): int {
$count = 0;
$fp = fopen($filename, 'rb');
while (fgets($fp) !== false) $count++;
fclose($fp);
return $count;
}
?>
<?php
/**
* 類似 tail -f:即時讀取檔案新增內容
*/
function tailFile(string $filename, int $initialLines = 10, callable $callback) {
$fp = fopen($filename, 'rb');
if (!$fp) return;
// 先讀最後 N 行
$fsize = filesize($filename);
$buffer = 4096;
$pos = max(0, $fsize - $buffer);
fseek($fp, $pos);
$data = fread($fp, $buffer);
$lines = array_slice(explode("\n", $data), -$initialLines);
foreach ($lines as $line) $callback($line);
// 持續監聽
while (true) {
$line = fgets($fp);
if ($line !== false) {
$callback(rtrim($line));
} else {
usleep(100000); // 100ms
clearstatcache(true, $filename);
}
}
}
// 使用
tailFile('/var/log/app.log', 20, function($line) {
echo "[" . date('H:i:s') . "] $line\n";
});
?>
SplFileObject: PHP 5.1+ 環境下 seek() 為 O(1) 跳轉,效能最佳。fgets 迴圈(記憶體最穩)或系統指令 sed -n '100,200p' file(最快)。mb_convert_encoding($line, 'UTF-8', 'auto') 或指定 fopen($file, 'rb') 避免換行符轉換。