讀取大文件 (GB 級日誌、導出數據、視頻切片) 是 PHP 易發生 memory_limit 耗盡、執行超時、阻塞 I/O 的高危操作。本文系統梳理流式逐行讀取、尾部高效讀取、隨機訪問、異步處理四大核心模式,提供生產級代碼模板與性能基準。
file_get_contents()、file()、readfile() 讀取大文件 (全量載入內存)fopen() + fgets()/fread() 流式處理 (恆定內存)fseek(SEEK_END) 反向掃描 (日誌分析核心需求)<?php
/**
* 生成器逐行讀取大文件
* 內存恆定 ~幾 KB,適合任意大小文件
*/
function readLargeFileLineByLine(string $filePath, int $bufferSize = 8192): Generator
{
$handle = fopen($filePath, 'rb');
if (!$handle) {
throw new RuntimeException("無法打開文件: $filePath");
}
try {
while (!feof($handle)) {
$line = fgets($handle, $bufferSize);
if ($line !== false) {
yield rtrim($line, "\r\n");
}
}
} finally {
fclose($handle);
}
}
// 使用示例:處理 10GB 日誌
$lineCount = 0;
$errorCount = 0;
foreach (readLargeFileLineByLine('/var/log/nginx/access.log') as $line) {
$lineCount++;
if (str_contains($line, ' 500 ')) {
$errorCount++;
// 異步推送告警...
}
// 進度報告
if ($lineCount % 100000 === 0) {
echo "已處理: $lineCount 行, 500錯誤: $errorCount\n";
}
}
echo "總計: $lineCount 行, 500錯誤: $errorCount\n";
<?php
function parseCsvStream(string $filePath, callable $processor, int $chunkSize = 1000): void
{
$handle = fopen($filePath, 'rb');
if (!$handle) throw new RuntimeException("打開失敗: $filePath");
$headers = null;
$buffer = [];
try {
while (($row = fgetcsv($handle)) !== false) {
if ($headers === null) {
$headers = $row;
continue;
}
$record = array_combine($headers, $row);
$buffer[] = $record;
if (count($buffer) >= $chunkSize) {
$processor($buffer);
$buffer = [];
}
}
if (!empty($buffer)) {
$processor($buffer);
}
} finally {
fclose($handle);
}
}
// 使用:批量入庫
parseCsvStream('/data/users_100w.csv', function(array $batch) {
User::insert($batch); // 批量插入
echo "入庫 " . count($batch) . " 條\n";
});
文件末尾
↑
┌─────────────────────┐
│ 塊 N (最後) │ ← 讀取、按 \n 分割、收集行
├─────────────────────┤
│ 塊 N-1 │ ← 繼續讀取,直到收集足夠行數
├─────────────────────┤
│ ... │
└─────────────────────┘
文件開頭
<?php
/**
* 讀取文件最後 N 行 (高效、大文件友好)
* @param string $filePath
* @param int $lineCount 讀取行數
* @param int $blockSize 讀取塊大小 (默認 4KB)
* @return string[] 行數組 (按文件順序:舊→新)
*/
function tailFile(string $filePath, int $lineCount = 10, int $blockSize = 4096): array
{
if ($lineCount <= 0) return [];
$handle = fopen($filePath, 'rb');
if (!$handle) {
throw new RuntimeException("無法打開文件: $filePath");
}
try {
$fileSize = fstat($handle)['size'];
if ($fileSize === 0) return [];
$lines = [];
$position = $fileSize;
$buffer = '';
$newline = "\n";
while (count($lines) < $lineCount && $position > 0) {
// 計算本次讀取位置和大小
$readSize = min($blockSize, $position);
$position -= $readSize;
fseek($handle, $position);
$chunk = fread($handle, $readSize);
if ($chunk === false) break;
$buffer = $chunk . $buffer;
// 按換行符分割
$parts = explode($newline, $buffer);
// 保留第一個可能不完整的片段作為下次緩衝
$buffer = array_shift($parts);
// 其餘為完整行 (逆序)
$lines = array_merge($parts, $lines);
}
// 處理最後剩餘的緩衝
if ($buffer !== '' && count($lines) < $lineCount) {
$lines = array_merge([$buffer], $lines);
}
// 只返回請求的行數 (最新的在後)
return array_slice($lines, -$lineCount);
} finally {
fclose($handle);
}
}
// 使用
$last100 = tailFile('/var/log/app/error.log', 100);
foreach ($last100 as $line) {
echo $line . PHP_EOL;
}
<?php
/**
* 實時跟蹤文件新增內容 (生成器,長時間運行)
*/
function followFile(string $filePath, int $intervalMs = 500): Generator
{
$handle = fopen($filePath, 'rb');
if (!$handle) throw new RuntimeException("打開失敗: $filePath");
// 移至文件末尾
fseek($handle, 0, SEEK_END);
try {
while (true) {
$line = fgets($handle);
if ($line !== false && $line !== '') {
yield rtrim($line, "\r\n");
} else {
// 無新數據,休眠
usleep($intervalMs * 1000);
clearstatcache(true, $filePath); // 刷新文件狀態
}
}
} finally {
fclose($handle);
}
}
// 使用:實時日誌監控
foreach (followFile('/var/log/nginx/access.log') as $line) {
if (str_contains($line, ' 404 ')) {
// 發送告警
$alertService->send("404 錯誤: $line");
}
}
<?php
/**
* 讀取文件指定位置、指定長度的內容
*/
function readFileChunk(string $filePath, int $offset, int $length): string
{
$handle = fopen($filePath, 'rb');
if (!$handle) throw new RuntimeException("打開失敗: $filePath");
try {
if (fseek($handle, $offset) === -1) {
throw new RuntimeException("定位失敗: offset=$offset");
}
$data = fread($handle, $length);
if ($data === false) {
throw new RuntimeException("讀取失敗");
}
return $data;
} finally {
fclose($handle);
}
}
/**
* 大文件分片上傳/下載支持
*/
class FileChunker
{
public function __construct(
private string $filePath,
private int $chunkSize = 1024 * 1024 // 1MB
) {}
public function getTotalChunks(): int
{
$size = filesize($this->filePath);
return (int)ceil($size / $this->chunkSize);
}
public function getChunk(int $index): string
{
$offset = $index * $this->chunkSize;
return readFileChunk($this->filePath, $offset, $this->chunkSize);
}
public function streamChunk(int $index): void
{
$handle = fopen($this->filePath, 'rb');
fseek($handle, $index * $this->chunkSize);
$remaining = $this->chunkSize;
while ($remaining > 0 && !feof($handle)) {
$readSize = min(8192, $remaining);
$data = fread($handle, $readSize);
if ($data === false) break;
echo $data;
$remaining -= strlen($data);
@ob_flush(); @flush();
}
fclose($handle);
}
}
<?php
// 生產者:文件上傳後推送任務
// 消費者:後台處理
// Job 類
class ProcessLargeFileJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public string $filePath,
public int $chunkSize = 10000
) {}
public function handle(): void
{
$processed = 0;
foreach (readLargeFileLineByLine($this->filePath) as $line) {
// 處理邏輯
$this->processLine($line);
$processed++;
// 定期釋放內存、更新進度
if ($processed % $this->chunkSize === 0) {
gc_collect_cycles();
$this->updateProgress($processed);
}
}
// 完成
$this->markCompleted($processed);
}
private function processLine(string $line): void { /* ... */ }
private function updateProgress(int $count): void { /* Redis/DB */ }
private function markCompleted(int $total): void { /* ... */ }
}
// 分發
ProcessLargeFileJob::dispatch('/uploads/huge_data.csv')->onQueue('large_files');
file_get_contents() | **OOM 失敗** | - | ❌ 禁用 |
| file() | **OOM 失敗** | - | ❌ 禁用 |
| fgets() 生成器 | **~2 MB** | ~45s | ✅ 推薦通用 |
| fgetcsv() 批量 | **~10 MB** | ~60s | ✅ 結構化數據 |
| tailFile() (最後 1000 行) | **~1 MB** | **~0.05s** | ✅ 日誌分析 |
| followFile() 實時 | **~1 MB** | 持續 | ✅ 監控告警 |
yield 避免數組累積unset($batch) + gc_collect_cycles()implode)memory_limit (如 256M) 防止單腳本拖垮服務器fgets() 返回空字符串非 false | 讀到空行 | while (($line = fgets($h)) !== false) 正確判斷 |
| Windows 換行符 \r\n 殘留 | fgets 保留 \r | rtrim($line, "\r\n") |
| 大文件 fseek 失敗 | 32位 PHP 整數溢出 | **必須用 64位 PHP**,PHP_INT_MAX > 文件大小 |
| fopen "Too many open files" | 文件描述符洩漏 | 嚴格 finally { fclose() }、檢查 ulimit -n |
| 讀取中斷無法恢復 | 無斷點續傳機制 | 記錄已處理行號/位移,支持斷點續傳 |
fopen + fgets + yield,內存恆定,通用首選fseek(SEEK_END) + 反向塊讀,毫秒級讀取最後 N 行,日誌分析利器fseek(END) + 循環 fgets + usleep,實現 tail -ffseek + fread,支持斷點續傳、視頻流、大文件並行處理大文件處理的核心是空間換時間:用恆定的小內存緩衝區,換取處理任意大小文件的能力。掌握這四大模式,GB/TB 級文件在 PHP 中也能游刃有餘。