處理 GB 級別的大型檔案(日誌、CSV、SQL Dump、影片)時,file_get_contents() 一次讀入記憶體會導致 OOM(Out of Memory)。本文詳細比較分段讀取、串流處理、Generator 迭代器三大策略,並提供生產環境可用的完整程式碼範例。
<?php
// 危險:一次載入整個檔案到記憶體
$content = file_get_contents('huge_10gb.log'); // 記憶體耗盡 Fatal Error
// 雖然支援 offset/maxlen,但仍需人工管理迴圈
$chunk = file_get_contents('huge.log', false, null, 0, 1024*1024); // 讀 1MB
?>
<?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); // 你的處理邏輯
}
?>
<?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");
}
}
?>
<?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);
?>
<?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";
?>
file_get_contents() 最簡單SplFileObject 或 fgets() Generatorfread() 固定大小區塊stream_filter_append()pcntl_fork 或外部 split 指令)| 方法 | 記憶體峰值 | 耗時 | CPU | 適用性 |
|---|---|---|---|---|
| file_get_contents | 1GB+ (OOM 風險) | 最快 | 低 | 小檔案 |
| fread 1MB 區塊 | ~1MB | 快 | 低 | 二進位、固定區塊 |
| fgets 逐行 | ~行長度 | 中 | 低 | 文字、CSV、日誌 |
| SplFileObject CSV | ~行長度 | 中 | 低 | 結構化 CSV |
| 串流過濾器 | ~緩衝區 | 中 | 中 | 即時轉換 |
| 錯誤 | 原因 | 修正 |
|---|---|---|
| Allowed memory size exhausted | 一次讀入過大檔案 | 改用 Generator 串流處理 |
| fread() 回傳空字串非 false | EOF 時 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 束縛。