處理 GB 級日誌、CSV、資料匯出檔時,file_get_contents()、file() 會將整個檔案載入記憶體導致 OOM。正解是流式讀取:逐行、分塊、游標定位。本文提供兩個完整類別 Lib_File1(fopen/fgets)與 Lib_File2(SplFileObject),支援分批讀取、條件過濾、編碼偵測、記憶體/耗時統計、命令列行數統計,可直接落地生產環境。
class Lib_File2 {
private static $init = null;
private static $startMemory = 0;
public static function instance() {
self::$startMemory = memory_get_usage(true);
if (self::$init instanceof self) return self::$init;
return self::$init = new self();
}
private function __construct() {}
// ... 設定 root、suffix、limit、length 等 setter
}
| 方法 | 用途 | 回傳結構 |
|---|---|---|
readFile($date,$appid,$op,$startNum,$length,$jsonFlag,$condition) | 分批讀取單一檔案 | ['count'=>N,'diffTime'=>秒,'memory'=>'MB','data'=>[行號=>解析後陣列]] |
total_lineFile($date,$appid,$op) | CLI wc -l 极速取得總行數 | ['count'=>總行數,...] |
countFile($date,$appid,$op,$condition) | 全檔掃描計數(含過濾) | 同上 |
countFileMID(...) | 統計唯一用戶數(去重 mid) | ['mid'=>[...],'count'=>N,...] |
getReturnUser($date,$appid,$op,$day,$midflag) | 跨多天統計留存 | ['20240101'=>[...],'20240102'=>[...]] |
public function readFile($date,$appid,$op,$startNum=0,$length=0,$jsonFlag=false,$condition=[]) {
$data = ['data'=>[], 'count'=>0];
$handle = $this->_openFile($date,$appid,$op); // 取得 SplFileObject
$handle->seek($startNum); // 直接跳轉到起始行(O(1) 定位)
$handle->setMaxLineLen($this->length); // 限制單行最大位元組,防記憶體噴漲
$line = $startNum;
while ($line - $startNum < $length - 1) {
$raw = $handle->current();
if (empty($raw)) { $handle->next(); continue; }
$parsed = $this->_parseData($raw, $jsonFlag); // 解析 timestamp|mid|json
if (!$parsed) { $handle->next(); continue; }
// 條件過濾
if ($condition && isset($parsed['data'][$key = array_keys($condition)[0]])) {
if (!in_array($parsed['data'][$key], $condition[$key])) {
$handle->next(); continue;
}
}
$data['data'][$line] = $parsed;
$data['count']++;
if ($line - $startNum >= $length - 1) break;
$handle->next();
$line = $handle->key();
}
$data['diffTime'] = microtime(true) - $this->startTime;
$data['memory'] = round((memory_get_usage(true) - self::$startMemory)/1024/1024, 2) . ' MB';
return $data;
}
每行格式:timestamp|mid|json_string
1704067200|10001|{"action":"login","ip":"1.2.3.4"}
1704067205|10002|{"action":"view","page":"/home"}
_parseData() 回傳:
[
'timestamp' => 1704067200,
'mid' => 10001,
'data' => ['action'=>'login','ip'=>'1.2.3.4'] // json_decode(true)
]
fgets($handle, $length) 逐行讀取,需手動 fseek 定位(不支援隨機跳行,只能循環略過)。seek 請用 Lib_File2。<?php
require_once 'Lib_File2.php';
$lf = Lib_File2::instance();
$lf->setRoot('/data/logs/');
$lf->setLimit(50000); // 每批 5 萬行
// 讀取 2024-01-01 appid=1000 login 日誌第 0-49999 行
$result = $lf->readFile('20240101', 1000, 'login', 0, 50000, true);
echo "讀取 {$result['count']} 筆,耗時 {$result['diffTime']} 秒,記憶體 {$result['memory']}\n";
// 統計全檔總行數(極速)
$total = $lf->total_lineFile('20240101', 1000, 'login');
echo "總行數:{$total['count']}\n";
?>
| 操作 | Lib_File1 (fgets) | Lib_File2 (SplFileObject) |
|---|---|---|
| 全檔掃描計數 | 1.8 秒 | 1.6 秒 |
| 隨機分頁讀取 (seek 50萬行) | 3.2 秒 (需循環略過) | 0.02 秒 (直接 seek) |
| 峰值記憶體 | 4 MB | 5 MB |
Lib_File2 (SplFileObject)Lib_File1 (fgets) 略快、依賴較少pcntl_fork 多進程分段平行處理,或改用 awk/sed 前置過濾再入 PHP。