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

PHP 高效讀取大文件四大方案性能實測

     編輯:PHP 相關知識

面對 800MB、500 萬行日志文件,file()、file_get_contents() 全量讀入記憶體會導致 OOM。本文實測四種流式/隨機讀取方案,從 tail 系統命令到 fseek 反向塊讀,附帶執行耗時數據與適用場景建議。

測試環境與基準

  • 文件:access.log,800 MB,~500 萬行。
  • 目標:讀取最後 10 行。
  • PHP 記憶體限制:memory_limit = -1(方案 1 需求)。

方案 1:file() 全量讀入 ❌ 不推薦

<?php
ini_set('memory_limit', '-1');
$file = 'access.log';
$data = file($file); // 一次性載入全部行到陣列
$line = $data[count($data) - 1];
echo $line;
// 耗時:116.96 秒,記憶體峰值 > 800 MB,系統近乎凍結
?>

方案 2:系統 tail 命令 ✅ 最快最簡

<?php
$file = 'access.log';
$file = escapeshellarg($file); // 防命令注入
$line = `tail -n 10 $file`;    // 反引號執行命令並捕獲輸出
echo $line;
// 耗時:0.0034 秒
// 優點:極快、零 PHP 記憶體、支援超大文件
// 缺點:依賴 Linux 環境、需 escapeshellarg、Windows 不通用
?>

方案 3:fseek 逐字節反向讀取 ⚠️ 可用但慢

<?php
$fp = fopen($file, 'r');
$lineCount = 10;
$pos = -2; // 從 EOF 前一字節開始(跳過末尾換行)
$t = ' ';
$data = '';

while ($lineCount > 0) {
    while ($t !== "\n") {
        fseek($fp, $pos, SEEK_END);
        $t = fgetc($fp);
        $pos--;
    }
    $t = ' ';
    $data .= fgets($fp); // 讀取整行
    $lineCount--;
}
fclose($fp);
echo $data;
// 耗時:0.0095 秒
// 缺點:大量系統調用、極長行時性能劣化
?>

方案 4:fseek 分塊反向讀取 ✅ 純 PHP 最佳

<?php
$fp = fopen($file, 'r');
$num = 10;
$chunk = 4096; // 4KB 塊
$fs = sprintf('%u', filesize($file)); // 避免 32 位溢位
$max = ($fs == PHP_INT_MAX) ? PHP_INT_MAX : (int)$fs;

$readData = '';
for ($len = 0; $len < $max; $len += $chunk) {
    $seekSize = min($chunk, $max - $len);
    fseek($fp, -($len + $seekSize), SEEK_END);
    $readData = fread($fp, $seekSize) . $readData; // 前置拼接保持順序
    
    if (substr_count($readData, "\n") >= $num + 1) {
        // 正則提取最後 N 行
        preg_match("!(.*?\n){$num}$!", $readData, $match);
        $data = $match[0];
        break;
    }
}
fclose($fp);
echo $data;
// 耗時:0.0009 秒
// 優點:系統調用少、記憶體恆定、純 PHP 跨平台
?>

方案 5:指數退避反向讀取 🏆 極致優化

<?php
function tail($fp, int $n, int $base = 5): array {
    $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));
// 耗時:0.0003 秒
// 原理:每次跳躍距離指數增長 (5, 25, 125, 625...),極快定位到包含 N 行的區塊
?>

性能總結與選型建議

方案耗時記憶體跨平台適用場景
file()116.96s>800MB✅小文件 (<10MB)、需全量處理
tail 命令0.0034s~0❌ (Linux/Unix)日志分析、Linux 部署、極大文件
fseek 逐字節0.0095s~0✅教學演示、極短行文件
fseek 分塊 (4KB)0.0009s~4KB×N✅**通用生產首選**、跨平台、無外部依賴
指數退避0.0003s~0✅極大文件、追求極致性能、行長度不可預測

生產級封裝建議

<?php
class BigFileReader {
    public static function tail(string $path, int $lines = 10, int $chunk = 8192): array {
        if (!is_readable($path)) throw new \InvalidArgumentException("File not readable: $path");
        
        $fp = fopen($path, 'rb');
        if (!$fp) throw new \RuntimeException("Cannot open file: $path");
        
        try {
            $size = filesize($path);
            if ($size === false || $size === 0) return [];
            
            $data = '';
            $read = 0;
            while ($read < $size && substr_count($data, "\n") < $lines + 1) {
                $seekSize = min($chunk, $size - $read);
                fseek($fp, -($read + $seekSize), SEEK_END);
                $data = fread($fp, $seekSize) . $data;
                $read += $seekSize;
            }
            
            $matches = [];
            preg_match("!(.*?\n){$lines}$!", $data, $matches);
            return $matches ? explode("\n", trim($matches[0])) : explode("\n", trim($data));
        } finally {
            fclose($fp);
        }
    }
}
?>
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved