統計大文件行數是日誌分析、數據遷移、ETL 處理的常見需求。傳統 file() 一次性讀入內存會導致 OOM,fgets() 逐行讀取則極其緩慢。本文提供分塊讀取優化方案,附 3.14GB 文件實測數據。
| 方案 | 核心 API | 內存佔用 | 3.14GB 耗時 | 適用場景 |
|---|---|---|---|---|
| file() 讀入數組 | file($path) | O(N) 文件大小 | OOM 崩潰 | 小文件(<100MB) |
| 逐行讀取 | fgets() 循環 | O(1) 常數 | 極慢(小時級) | 極小文件、需逐行處理 |
| 分塊讀取(推薦) | fread() + substr_count() | O(1) 固定塊大小 | ~50 秒 | 大文件行數統計 |
/**
* 高效率統計文件行數
* @param string $file 文件路徑
* @param int $chunkSize 讀取塊大小(字節),默認 2MB
* @return int 行數
*/
function countLines(string $file, int $chunkSize = 2 * 1024 * 1024): int {
$handle = fopen($file, 'rb');
if (!$handle) {
throw new \RuntimeException("無法打開文件: $file");
}
$lineCount = 0;
$leftover = ''; // 處理跨塊換行符
while (!feof($handle)) {
$chunk = fread($handle, $chunkSize);
if ($chunk === false) {
throw new \RuntimeException("文件讀取失敗: $file");
}
// 處理上一塊遺留的不完整行
$chunk = $leftover . $chunk;
// 統計完整換行符數量
$lineCount += substr_count($chunk, "\n");
// 檢查最後一個字符是否為換行符,決定是否有遺留
$lastChar = $chunk[-1] ?? '';
$leftover = ($lastChar !== "\n" && $lastChar !== "\r") ? $chunk : '';
// 進一步優化:僅保留最後不完整行部分(避免累積過大)
if ($leftover) {
$lastNewline = max(strrpos($leftover, "\n"), strrpos($leftover, "\r"));
if ($lastNewline !== false) {
$leftover = substr($leftover, $lastNewline + 1);
}
}
}
fclose($handle);
// 文件不以換行符結尾時,最後一行也要計數
if ($leftover !== '') {
$lineCount++;
}
return $lineCount;
}
測試環境:Intel i7-8700 / 32GB RAM / SSD / PHP 8.1 CLI 第 1 次:13,214,810 行,耗時 56.28 秒 第 2 次:13,214,810 行,耗時 49.67 秒(OS 文件緩存生效) 平均:~53 秒,吞吐量 ~60 MB/s
'rb' 避免 Windows 下 \r\n 自動轉換干擾計數$leftover 變量拼接function countLinesUniversal(string $file, int $chunkSize = 2 * 1024 * 1024): int {
$handle = fopen($file, 'rb');
$count = 0;
$buffer = '';
while (!feof($handle)) {
$chunk = fread($handle, $chunkSize);
$buffer .= $chunk;
// 統計所有類型換行符
$count += substr_count($buffer, "\n");
$count += substr_count($buffer, "\r");
// 扣除 \r\n 重複計數
$count -= substr_count($buffer, "\r\n");
// 保留最後 2 字符用於跨塊檢查
$buffer = substr($buffer, -2);
}
fclose($handle);
return $count;
}
function countLinesParallel(string $file, int $processes = 4): int {
$size = filesize($file);
$chunkSize = (int)($size / $processes);
$handles = [];
for ($i = 0; $i < $processes; $i++) {
$start = $i * $chunkSize;
$end = ($i === $processes - 1) ? $size : ($start + $chunkSize);
$handles[$i] = fopen($file, 'rb');
fseek($handles[$i], $start);
// 調整起始位置到行首(避免切行)
if ($i > 0) {
while (!feof($handles[$i])) {
$char = fgetc($handles[$i]);
if ($char === "\n" || $char === "\r" || $char === false) break;
}
}
}
// 這裡需配合 pcntl_fork 或進程池實現真正並發
// 簡化版:串行調用各分片統計函數
$total = 0;
foreach ($handles as $i => $h) {
$total += countLinesFromHandle($h, $i === $processes - 1 ? $size : ($chunkSize * ($i + 1)));
fclose($h);
}
return $total;
}
function countLinesSys(string $file): int {
// Linux/Unix: wc -l 最快(C 實現,利用 mmap + SIMD)
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$cmd = escapeshellcmd("wc -l " . escapeshellarg($file));
$output = shell_exec($cmd);
return (int)trim(explode(' ', $output)[0]);
}
// Windows: find /c /v ""
$cmd = 'find /c /v "" ' . escapeshellarg($file);
$output = shell_exec($cmd);
return (int)trim(explode(':', $output)[1]);
}
注意:shell_exec 需禁用 disable_functions 限制,涉及用戶輸入路徑時必須 escapeshellarg 防注入。
| 文件大小 | 推薦方案 | 備註 |
|---|---|---|
| < 100 MB | file() / count(file()) | 代碼最簡潔,內存可接受 |
| 100 MB - 10 GB | fread() 分塊 + substr_count() | 純 PHP 實現,無外部依賴,跨平台 |
| > 10 GB | wc -l / find 系統命令 | 極致性能,需注意命令注入防護 |
| 需併發/分布式 | 分片並行 + 聚合 | 配合 Swoole/Workerman/RoadRunner 進程池 |
分塊讀取 + substr_count() 是純 PHP 環境下統計大文件行數的最優解,兼顧性能、內存、跨平台與代碼簡潔性。實際專案中建議封裝為工具類,根據文件大小自動選擇策略。