CSDN 274 MB SQL 洩露檔案、百萬行日誌、GB 級匯出檔 —— 直接用 file_get_contents()、file()、fopen() + fread() 全讀入記憶體,輕易觸發 memory_limit 或 OOM Killer。正確做法是流式分塊讀取,只在記憶體保留當前處理的片段。
<?php
/**
* 讀取大檔案指定行數
* @param string $filename 檔案路徑
* @param int $count 讀取行數
* @param string $tag 行分隔符(Windows: \r\n, Linux: \n, Mac: \r)
* @return string HTML 格式內容(含 <br />)
*/
function readBigFile(string $filename, int $count = 20, string $tag = "\n"): string {
$content = '';
$step = 1; // 每次讀 1 位元組(可調大如 4096 提升效能)
$tagLen = strlen($tag);
$start = 0;
$lineCount = 0;
$handle = fopen($filename, 'rb'); // 二進位模式避免轉換
if (!$handle) {
throw new RuntimeException("無法開啟檔案: $filename");
}
while ($lineCount < $count && !feof($handle)) {
fseek($handle, $start, SEEK_SET);
$chunk = fread($handle, $step);
if ($chunk === false || $chunk === '') break;
$content .= $chunk;
$start += $step;
// 檢查尾部是否為分隔符
if (substr($content, -$tagLen) === $tag) {
$lineCount++;
$content .= '<br />';
}
}
fclose($handle);
return $content;
}
// 使用範例
$filename = 'csdn.sql';
$tag = "\n"; // Linux 換行
$count = 100;
echo readBigFile($filename, $count, $tag);
| 細節 | 說明 |
|---|---|
二進位模式 rb | 避免 Windows 自動將 \r\n 轉 \n,導致位移計算錯誤 |
分隔符 $tag | 跨平台必須正確指定:Windows \r\n、Linux/Unix \n、Mac \r |
$step 調整 | 預設 1 位元組教學用;生產環境建議 4096/8192,配合緩衝區搜尋分隔符 |
| 記憶體佔用 | 僅佔用 $content 累積的當前區塊 + 緩衝區,不隨檔案大小增長 |
| 效能 | 頻繁 fseek + 小 fread 較慢;大檔案建議改用 fread(8192) 讀大塊,再在記憶體中 strpos 切行 |
class BigFileReader {
private $handle;
private $filename;
private $bufferSize = 8192;
private $position = 0; // 記錄上次讀取位置
public function __construct(string $filename) {
$this->filename = $filename;
$this->handle = fopen($filename, 'rb');
if (!$this->handle) throw new RuntimeException("Open failed: $filename");
}
// 讀取下一塊(預設 8KB),回傳字串或 false(EOF)
public function readChunk(): string|false {
if (feof($this->handle)) return false;
$chunk = fread($this->handle, $this->bufferSize);
$this->position = ftell($this->handle);
return $chunk;
}
// 依行讀取(生成器,記憶體極低)
public function readLines(): Generator {
$buffer = '';
while (!feof($this->handle)) {
$buffer .= fread($this->handle, $this->bufferSize);
while (($pos = strpos($buffer, "\n")) !== false) {
yield substr($buffer, 0, $pos + 1);
$buffer = substr($buffer, $pos + 1);
}
}
if ($buffer !== '') yield $buffer; // 最後一行無換行
}
// 儲存/恢復位置(斷點續讀)
public function savePosition(): int { return $this->position; }
public function seekPosition(int $pos): void {
fseek($this->handle, $pos, SEEK_SET);
$this->position = $pos;
}
public function close(): void { fclose($this->handle); }
}
// 使用範例
$reader = new BigFileReader('huge.log');
foreach ($reader->readLines() as $i => $line) {
if ($i >= 1000) break; // 只處理前 1000 行
processLine($line);
}
$reader->close();
mb_convert_encoding() 統一轉 UTF-8。set_time_limit(0) 或分批次處理(配合 Queue/Worker)。flock($handle, LOCK_SH) 共享鎖。fopen、fread、fseek 皆需檢查回傳值,拋出異常或記錄日誌。| 方案 | 記憶體 | 速度 | 適用場景 |
|---|---|---|---|
| file_get_contents() | 高(全檔) | 快 | < 50MB 文字檔 |
| file() 陣列 | 極高 | 中 | 小檔、需隨機存取行 |
| fread 固定緩衝區 | 低(緩衝區) | 快 | 大檔案順序處理、串流輸出 |
| fseek + fread 小步長 | 極低 | 慢 | 隨機存取特定位移、教學示範 |
| Generator 分行讀取 | 極低 | 中 | 逐行邏輯處理、記憶體敏感環境 |
| SplFileObject | 低 | 中 | 物件導向、內建 CSV/行迭代 |
讀取超大檔案的核心原則是「串流處理、不全量載入」。根據業務需求選擇:
fseek + freadSplFileObject搭配位置持久化(ftell/fseek),即可實現斷點續讀、分散式處理,徹底解決記憶體爆滿與執行逾時問題。