首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 高性能統計大文件行數:分塊讀取優化與實測數據對比

     編輯:PHP 相關知識

PHP 高性能統計大文件行數:分塊讀取優化與實測數據對比

統計大文件行數是日誌分析、數據遷移、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;
}

實測數據(3.14 GB 文件,1321 萬行)

測試環境: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

關鍵優化細節解析

  1. 二進制模式打開:'rb' 避免 Windows 下 \r\n 自動轉換干擾計數
  2. 固定塊大小:2MB 平衡系統調用開銷與內存佔用,可根據 SSD/HDD 調整(1-8MB)
  3. 跨塊換行符處理:\n 或 \r\n 可能被切分在兩塊邊界,需用 $leftover 變量拼接
  4. substr_count 高效計數:C 底層實現,比 PHP 循環快數量級
  5. 避免遺留累積:僅保留最後不完整行片段,防止極長行導致內存漲
  6. 文件結尾判斷:無結尾換行符的文件最後一行需手動 +1

進階場景擴展

統計非 \n 換行符(Windows \r\n、Mac 老式 \r)

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 MBfile() / count(file())代碼最簡潔,內存可接受
100 MB - 10 GBfread() 分塊 + substr_count()純 PHP 實現,無外部依賴,跨平台
> 10 GBwc -l / find 系統命令極致性能,需注意命令注入防護
需併發/分布式分片並行 + 聚合配合 Swoole/Workerman/RoadRunner 進程池

分塊讀取 + substr_count() 是純 PHP 環境下統計大文件行數的最優解,兼顧性能、內存、跨平台與代碼簡潔性。實際專案中建議封裝為工具類,根據文件大小自動選擇策略。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved