首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 高效讀取大檔案指定行範圍:SplFileObject vs fgets 效能實測

     編輯:PHP 綜合知識

問題背景

需從幾百 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; });
}
?>

效能實測對比(8 MB 測試檔,讀取最後 10 行)

方法耗時記憶體峰值備註
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";
});
?>

最佳實踐建議

  1. 優先使用 SplFileObject: PHP 5.1+ 環境下 seek() 為 O(1) 跳轉,效能最佳。
  2. 超大檔案(>1GB): 考慮 fgets 迴圈(記憶體最穩)或系統指令 sed -n '100,200p' file(最快)。
  3. 編碼處理: 讀取後若有亂碼,用 mb_convert_encoding($line, 'UTF-8', 'auto') 或指定 fopen($file, 'rb') 避免換行符轉換。
  4. 生產環境: 避免在 Web 請求中直接讀取大檔,改用背景任務預處理或資料庫/Elasticsearch 索引。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved