萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> PHP 程式設計

PHP 讀取大型檔案完全指南:file_get_contents、fread、Generator 串流處理與記憶體優化

前言

處理 GB 級別的大型檔案(日誌、CSV、SQL Dump、影片)時,file_get_contents() 一次讀入記憶體會導致 OOM(Out of Memory)。本文詳細比較分段讀取、串流處理、Generator 迭代器三大策略,並提供生產環境可用的完整程式碼範例。

問題:file_get_contents() 記憶體爆炸

<?php
// 危險:一次載入整個檔案到記憶體
$content = file_get_contents('huge_10gb.log'); // 記憶體耗盡 Fatal Error

// 雖然支援 offset/maxlen,但仍需人工管理迴圈
$chunk = file_get_contents('huge.log', false, null, 0, 1024*1024); // 讀 1MB
?>

方案一:fopen + fread 手動分塊讀取

<?php
function readInChunks(string $filename, int $chunkSize = 1024*1024): Generator {
    $handle = fopen($filename, 'rb');
    if (!$handle) throw new RuntimeException("Cannot open $filename");
    
    try {
        while (!feof($handle)) {
            $chunk = fread($handle, $chunkSize);
            if ($chunk === false) throw new RuntimeException('Read error');
            yield $chunk;
        }
    } finally {
        fclose($handle);
    }
}

// 使用:處理 10GB 檔案,記憶體只佔用 1MB
foreach (readInChunks('huge.log', 1024*1024) as $chunk) {
    processChunk($chunk); // 你的處理邏輯
}
?>

方案二:fgets 逐行讀取(適用行導向檔案)

<?php
function readLines(string $filename): Generator {
    $handle = fopen($filename, 'r');
    if (!$handle) throw new RuntimeException("Cannot open $filename");
    
    try {
        while (($line = fgets($handle)) !== false) {
            yield rtrim($line, "\r\n"); // 移除換行符
        }
    } finally {
        fclose($handle);
    }
}

// 處理大型 CSV/日誌
foreach (readLines('access.log') as $lineNumber => $line) {
    if (str_contains($line, 'ERROR')) {
        error_log("Line $lineNumber: $line");
    }
}
?>

方案三:SplFileObject 物件導向介面

<?php
function processCsv(string $filename): Generator {
    $file = new SplFileObject($filename, 'r');
    $file->setFlags(
        SplFileObject::READ_CSV | 
        SplFileObject::READ_AHEAD | 
        SplFileObject::SKIP_EMPTY
    );
    $file->setCsvControl(','); // 分隔符
    
    $headers = $file->current(); // 第一行為標題
    $file->next();
    
    foreach ($file as $row) {
        yield array_combine($headers, $row);
    }
}

// 使用:記憶體常數,逐行處理
foreach (processCsv('huge.csv') as $record) {
    // $record = ['id'=>'1', 'name'=>'Alice', ...]
    User::create($record);
}
?>

方案四:串流過濾器(即時轉換)

<?php
// 註冊自訂串流過濾器
class StrToUpperFilter extends php_user_filter {
    public function filter($in, $out, &$consumed, $closing): int {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strtoupper($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}
stream_filter_register('str.toupper', StrToUpperFilter::class);

// 使用:讀取時自動轉大寫
$handle = fopen('huge.log', 'r');
stream_filter_append($handle, 'str.toupper');
while (($line = fgets($handle)) !== false) {
    // $line 已是大寫
    process($line);
}
fclose($handle);
?>

大型 SQL 檔案匯入策略

<?php
function importLargeSql(string $sqlFile, PDO $pdo): void {
    $handle = fopen($sqlFile, 'r');
    if (!$handle) throw new RuntimeException("Cannot open $sqlFile");
    
    $buffer = '';
    $stmtCount = 0;
    
    try {
        $pdo->beginTransaction();
        
        while (($line = fgets($handle)) !== false) {
            $buffer .= $line;
            
            // 簡易判斷 SQL 結束(分號在行尾且非字串內)
            if (preg_match('/;\s*$/', $line) && !inString($buffer)) {
                $sql = trim($buffer);
                if ($sql !== '') {
                    $pdo->exec($sql);
                    $stmtCount++;
                    
                    // 定期提交避免交易過大
                    if ($stmtCount % 1000 === 0) {
                        $pdo->commit();
                        $pdo->beginTransaction();
                    }
                }
                $buffer = '';
            }
        }
        
        // 餘下 SQL
        if (trim($buffer) !== '') {
            $pdo->exec(trim($buffer));
        }
        
        $pdo->commit();
    } catch (Exception $e) {
        $pdo->rollBack();
        throw $e;
    } finally {
        fclose($handle);
    }
}

function inString(string $sql): bool {
    $inSingle = $inDouble = false;
    $escaped = false;
    for ($i = 0; $i < strlen($sql); $i++) {
        $c = $sql[$i];
        if ($escaped) { $escaped = false; continue; }
        if ($c === '\\') { $escaped = true; continue; }
        if ($c === "'" && !$inDouble) $inSingle = !$inSingle;
        if ($c === '"' && !$inSingle) $inDouble = !$inDouble;
    }
    return $inSingle || $inDouble;
}
?>

記憶體限制配置

<?php
// 執行期調整(若 php.ini 允許)
ini_set('memory_limit', '512M'); // 或 -1 移除限制(危險)

// 檢查當前限制
echo ini_get('memory_limit'); // 128M

// 監控記憶體使用
echo "Current: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";
echo "Peak: " . memory_get_peak_usage(true) / 1024 / 1024 . " MB\n";
?>

策略選擇決策樹

  1. 檔案 < 記憶體限制/4:file_get_contents() 最簡單
  2. 行導向檔案(CSV、日誌、JSONL):SplFileObject 或 fgets() Generator
  3. 二進位檔案、固定區塊處理:fread() 固定大小區塊
  4. 需即時轉換(壓縮、加密、編碼):串流過濾器 stream_filter_append()
  5. 並行處理:多程序分割檔案(pcntl_fork 或外部 split 指令)

效能比較參考(1GB 檔案,SSD,PHP 8.2)

方法記憶體峰值耗時CPU適用性
file_get_contents1GB+ (OOM 風險)最快低小檔案
fread 1MB 區塊~1MB快低二進位、固定區塊
fgets 逐行~行長度中低文字、CSV、日誌
SplFileObject CSV~行長度中低結構化 CSV
串流過濾器~緩衝區中中即時轉換

常見錯誤與除錯

錯誤原因修正
Allowed memory size exhausted一次讀入過大檔案改用 Generator 串流處理
fread() 回傳空字串非 falseEOF 時 fread 回傳 ''用 feof() 或 === false 判斷
fgets() 截斷長行預設 8192 位元組緩衝指定長度 fgets($h, 1024*1024)
CSV 欄位含換行/引號簡易 explode(',') 失效必須用 SplFileObject::READ_CSV 或 str_getcsv()
Windows 換行符 \r\n殘留 \r 字元rtrim($line, "\r\n")

結語

處理大型檔案的核心原則是串流處理:一次只在記憶體保留一小塊資料,處理完釋放,再讀取下一塊。PHP 的 Generator 配合 fopen()/fread()/fgets()/SplFileObject 提供了優雅的串流抽象。掌握這些技巧,讓你的 PHP 程式能輕鬆處理 TB 級資料,而不受 memory_limit 束縛。

copyright © 萬盛學電腦網 all rights reserved