日志分析、數據遷移、備份還原等場景常需處理超過 PHP memory_limit 的大文件。核心原則:**絕不可一次性讀入內存**(file_get_contents, file()),必須采用流式讀取或指針跳轉逐塊處理。
利用 fseek($fp, $offset, SEEK_END) 從文件末尾向前掃描換行符。
<?php
/**
* 獲取文件最後 $n 行
* @param string $file 文件路徑
* @param int $n 行數
* @return string|false 內容或 false
*/
function tailFile($file, $n = 10) {
if (!is_file($file) || !($fp = fopen($file, 'rb'))) return false;
$lineCount = 0;
$pos = -1; // 從 EOF 前 1 字節開始
$buffer = 4096; // 每次回讀緩衝區大小
$data = '';
while ($lineCount < $n && ftell($fp) > 0) {
// 向前移動緩衝區大小,但不超過文件開頭
$seekPos = max(0, ftell($fp) - $buffer);
fseek($fp, $seekPos);
$chunk = fread($fp, ftell($fp) - $seekPos + 1); // 讀取回退區塊
// 從塊末尾向前數換行符
$lines = explode("\n", rtrim($chunk));
// 處理跨塊行:第一塊的最後一行可能不完整,需拼接下一塊開頭
// 簡化版:直接累加行數,最後取後 n 行
$data = $chunk . $data;
$lineCount = substr_count($data, "\n");
if ($seekPos === 0) break; // 已到文件開頭
fseek($fp, $seekPos); // 重新定位繼續向前
}
fclose($fp);
$allLines = explode("\n", rtrim($data));
return implode("\n", array_slice($allLines, -$n));
}
// 更簡潔健壯的版本(推薦生產使用)
function tailFileV2($file, $n = 10) {
$fp = fopen($file, 'rb');
if (!$fp) return false;
fseek($fp, 0, SEEK_END);
$pos = ftell($fp);
$lines = [];
$buf = '';
while ($pos > 0 && count($lines) < $n) {
$readSize = min(8192, $pos);
$pos -= $readSize;
fseek($fp, $pos);
$buf = fread($fp, $readSize) . $buf;
$lines = explode("\n", $buf);
// 如果第一行不完整(開頭無換行),保留等待下一塊
if ($pos > 0 && strpos($buf, "\n") !== 0) {
array_shift($lines); // 丟棄不完整行
}
}
fclose($fp);
return implode("\n", array_slice($lines, -$n));
}
?>
<?php
function processLargeFile($file, callable $handler) {
if (!($fp = fopen($file, 'rb'))) return false;
// 可選:設置流過濾器自動轉碼(如 UTF-16 → UTF-8)
// stream_filter_append($fp, 'convert.iconv.utf-16/utf-8');
while (($line = fgets($fp)) !== false) {
// 去除行尾換行符
$line = rtrim($line, "\r\n");
if ($line === '') continue; // 跳過空行
// 業務處理,返回 false 可中斷
if ($handler($line) === false) break;
}
fclose($fp);
return true;
}
// 使用示例:CSV 逐行入庫
processLargeFile('data.csv', function($line) {
$fields = str_getcsv($line);
// $db->insert('table', ['col1'=>$fields[0], ...]);
return true; // 繼續
});
?>
<?php
function downloadLargeFile($filePath, $fileName = null) {
if (!is_file($filePath)) { http_response_code(404); exit; }
$fileName = $fileName ?? basename($filePath);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.rawurlencode($fileName).'"');
header('Content-Length: '.filesize($filePath));
header('Accept-Ranges: bytes'); // 支持斷點續傳
// 處理 Range 請求(斷點續傳核心)
if (isset($_SERVER['HTTP_RANGE'])) {
list($unit, $range) = explode('=', $_SERVER['HTTP_RANGE']);
if ($unit === 'bytes') {
list($start, $end) = explode('-', $range);
$start = (int)$start;
$end = $end ? (int)$end : filesize($filePath) - 1;
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/".filesize($filePath));
header("Content-Length: ".($end - $start + 1));
}
}
$fp = fopen($filePath, 'rb');
if (isset($start)) fseek($fp, $start);
$buffer = 1024 * 1024; // 1MB 輸出緩衝
while (!feof($fp) && (connection_status() === CONNECTION_NORMAL)) {
$chunk = fread($fp, $buffer);
if (isset($end) && ftell($fp) > $end) {
$chunk = substr($chunk, 0, $end - ftell($fp) + strlen($chunk));
}
echo $chunk;
@flush(); // 強制發送到客戶端
}
fclose($fp);
}
?>
memory_get_usage(),超閾值主動 gc_collect_cycles() 或分批處理。