Как эффективно читать большой файл построчно в PHP без перегрузки памяти?
Использование fopen() и fgets() для построчного чтения
Основная проблема вашего подхода в том, что file_get_contents() загружает весь файл в оперативную память. Для больших файлов это неприемлемо. Решение — использовать файловые указатели с функциями fopen() и fgets(), которые читают файл порциями (обычно по одной строке).
<?php
$handle = fopen('big_file.csv', 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// Обработка строки
echo $line;
}
fclose($handle);
} else {
echo "Ошибка открытия файла";
}
?>
Как это работает:
fopen('big_file.csv', 'r')— открывает файл для чтения и возвращает указатель на поток. Сам файл в память не загружается.fgets($handle)— читает одну строку из файла (до символа новой строки или до конца буфера). Возвращает строку илиfalse, если достигнут конец файла.fclose($handle)— закрывает поток и освобождает ресурсы.
Такой подход использует память только для текущей строки, а не для всего файла. Даже файл размером 10 ГБ будет обработан без проблем, если каждая строка не слишком длинная.
Дополнительные оптимизации для очень больших файлов
- Увеличьте буфер чтения: По умолчанию
fgets()использует буфер 8192 байт. Вы можете задать больший размер черезstream_set_chunk_size():
$handle = fopen('big_file.csv', 'r');
stream_set_chunk_size($handle, 65536); // 64 КБ
while (($line = fgets($handle)) !== false) {
// ...
}
fclose($handle);
- Используйте SplFileObject: Это объектно-ориентированный способ, который удобнее для работы с CSV:
$file = new SplFileObject('big_file.csv');
$file->setFlags(SplFileObject::READ_CSV); // Для CSV-файлов
foreach ($file as $line) {
// $line — массив полей CSV
print_r($line);
}
- Обрабатывайте ошибки: Всегда проверяйте возвращаемое значение
fopen()— файл может не существовать или не быть доступным для чтения. - Закрывайте файл: Не забывайте про
fclose(), иначе могут возникнуть утечки дескрипторов.
Что делать, если строки очень длинные?
Если в файле есть строки длиной в несколько мегабайт (например, дампы данных), fgets() всё равно прочитает их целиком. В таком случае используйте fread() с фиксированным размером буфера и обрабатывайте данные частями:
$handle = fopen('big_file.log', 'r');
$buffer = '';
while (!feof($handle)) {
$chunk = fread($handle, 8192);
$buffer .= $chunk;
// Разделяем по символам новой строки
$lines = explode("\n", $buffer);
$buffer = array_pop($lines); // Сохраняем неполную строку
foreach ($lines as $line) {
processLine($line);
}
}
fclose($handle);
Этот метод гарантирует, что вы никогда не загрузите в память больше, чем размер буфера + одна строка.
<<Использование генераторов для ленивого чтения
Альтернативный элегантный подход — обернуть чтение файла в генератор. Это позволит абстрагировать логику чтения и использовать её повторно в разных частях кода.
<?php
function readLines($filename) {
$handle = fopen($filename, 'r');
if (!$handle) {
throw new Exception("Не удалось открыть файл: $filename");
}
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\r\n"); // Убираем символы новой строки
}
fclose($handle);
}
// Использование:
foreach (readLines('big_file.csv') as $lineNumber => $line) {
echo "Строка $lineNumber: $line\n";
}
?>
Генераторы в PHP (начиная с версии 5.5) позволяют создавать итераторы без написания целого класса. Функция readLines() возвращает объект Generator, который при каждой итерации выполняет код до следующего yield. Это даёт те же преимущества по памяти, что и прямой вызов fgets(), но делает код чище и переиспользуемым.
Дополнительный совет: Если вы обрабатываете CSV с BOM (Byte Order Mark) или разными кодировками, добавьте в генератор проверку первой строки и конвертацию через mb_convert_encoding().
Такой подход особенно удобен, когда нужно обработать несколько файлов последовательно или комбинировать чтение с другими операциями (фильтрация, трансформация) без создания промежуточных массивов.