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

PHP 讀取超大文件(GB 級)的高效內存安全方案

     編輯:PHP 相關知識

PHP 讀取超大文件(GB 級)的高效內存安全方案

日志分析、數據遷移、備份還原等場景常需處理超過 PHP memory_limit 的大文件。核心原則:**絕不可一次性讀入內存**(file_get_contents, file()),必須采用流式讀取或指針跳轉逐塊處理。

場景一:讀取文件最後 N 行(尾部日志查看)

利用 fseek($fp, $offset, SEEK_END) 從文件末尾向前掃描換行符。

<?php
/**
 * 獲取文件最後 $n 行
 * @param string $file 文件路徑
 * @param int $n 行數
 * @return string|false 內容或 false
 */
function tailFile($file, $n = 10) {
    if (!is_file($file) || !($fp = fopen($file, 'rb'))) return false;
    
    $lineCount = 0;
    $pos = -1; // 從 EOF 前 1 字節開始
    $buffer = 4096; // 每次回讀緩衝區大小
    $data = '';
    
    while ($lineCount < $n && ftell($fp) > 0) {
        // 向前移動緩衝區大小,但不超過文件開頭
        $seekPos = max(0, ftell($fp) - $buffer);
        fseek($fp, $seekPos);
        $chunk = fread($fp, ftell($fp) - $seekPos + 1); // 讀取回退區塊
        // 從塊末尾向前數換行符
        $lines = explode("\n", rtrim($chunk));
        // 處理跨塊行:第一塊的最後一行可能不完整,需拼接下一塊開頭
        // 簡化版:直接累加行數,最後取後 n 行
        $data = $chunk . $data;
        $lineCount = substr_count($data, "\n");
        if ($seekPos === 0) break; // 已到文件開頭
        fseek($fp, $seekPos); // 重新定位繼續向前
    }
    fclose($fp);
    $allLines = explode("\n", rtrim($data));
    return implode("\n", array_slice($allLines, -$n));
}

// 更簡潔健壯的版本(推薦生產使用)
function tailFileV2($file, $n = 10) {
    $fp = fopen($file, 'rb');
    if (!$fp) return false;
    fseek($fp, 0, SEEK_END);
    $pos = ftell($fp);
    $lines = [];
    $buf = '';
    while ($pos > 0 && count($lines) < $n) {
        $readSize = min(8192, $pos);
        $pos -= $readSize;
        fseek($fp, $pos);
        $buf = fread($fp, $readSize) . $buf;
        $lines = explode("\n", $buf);
        // 如果第一行不完整(開頭無換行),保留等待下一塊
        if ($pos > 0 && strpos($buf, "\n") !== 0) {
            array_shift($lines); // 丟棄不完整行
        }
    }
    fclose($fp);
    return implode("\n", array_slice($lines, -$n));
}
?>

場景二:逐行處理大文件(如 CSV 導入、日志解析)

<?php
function processLargeFile($file, callable $handler) {
    if (!($fp = fopen($file, 'rb'))) return false;
    // 可選:設置流過濾器自動轉碼(如 UTF-16 → UTF-8)
    // stream_filter_append($fp, 'convert.iconv.utf-16/utf-8');
    
    while (($line = fgets($fp)) !== false) {
        // 去除行尾換行符
        $line = rtrim($line, "\r\n");
        if ($line === '') continue; // 跳過空行
        // 業務處理,返回 false 可中斷
        if ($handler($line) === false) break;
    }
    fclose($fp);
    return true;
}

// 使用示例:CSV 逐行入庫
processLargeFile('data.csv', function($line) {
    $fields = str_getcsv($line);
    // $db->insert('table', ['col1'=>$fields[0], ...]);
    return true; // 繼續
});
?>

場景三:大文件下載輸出(不佔用 PHP 內存)

<?php
function downloadLargeFile($filePath, $fileName = null) {
    if (!is_file($filePath)) { http_response_code(404); exit; }
    $fileName = $fileName ?? basename($filePath);
    
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.rawurlencode($fileName).'"');
    header('Content-Length: '.filesize($filePath));
    header('Accept-Ranges: bytes'); // 支持斷點續傳
    
    // 處理 Range 請求(斷點續傳核心)
    if (isset($_SERVER['HTTP_RANGE'])) {
        list($unit, $range) = explode('=', $_SERVER['HTTP_RANGE']);
        if ($unit === 'bytes') {
            list($start, $end) = explode('-', $range);
            $start = (int)$start;
            $end = $end ? (int)$end : filesize($filePath) - 1;
            header('HTTP/1.1 206 Partial Content');
            header("Content-Range: bytes $start-$end/".filesize($filePath));
            header("Content-Length: ".($end - $start + 1));
        }
    }
    
    $fp = fopen($filePath, 'rb');
    if (isset($start)) fseek($fp, $start);
    $buffer = 1024 * 1024; // 1MB 輸出緩衝
    while (!feof($fp) && (connection_status() === CONNECTION_NORMAL)) {
        $chunk = fread($fp, $buffer);
        if (isset($end) && ftell($fp) > $end) {
            $chunk = substr($chunk, 0, $end - ftell($fp) + strlen($chunk));
        }
        echo $chunk;
        @flush(); // 強制發送到客戶端
    }
    fclose($fp);
}
?>

關鍵最佳實踐總結

  1. 只用 'rb' 模式:二進制讀取避免 Windows 換行符轉換導致位置偏移。
  2. 及時 fclose():釋放文件句柄與系統資源。
  3. 設置 set_time_limit(0):長耗時任務防止超時中斷。
  4. 錯誤處理:所有文件操作檢查返回值,拋出異常或記錄日志。
  5. 內存監控:循環中定期 memory_get_usage(),超閾值主動 gc_collect_cycles() 或分批處理。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved