Работа с большими наборами данных в PHP

Раздел: Веб-разработка на PHP -> Производительность и масштабирование

Основные подходы к обработке больших данных в PHP

Как обработать большой файл, не загружая его целиком в память?

Генераторы (yield)

function readCsvLines(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new RuntimeException('Не удалось открыть файл');
    }
    while (($line = fgets($handle)) !== false) {
        yield str_getcsv($line);
    }
    fclose($handle);
}
foreach (readCsvLines('huge.csv') as $row) {
    process($row);
}

большие данные php (работа с большими данными в php)

Генератор yield приостанавливает выполнение после каждой строки, возвращая её. Память расходуется только на текущую строку. Файл закрывается после цикла. Для больших файлов это наилучший вариант.

Возможные проблемы:

  • Необходимость явно закрывать файл (в примере закрывается в конце).
  • Ошибка открытия файла требует обработки исключений.
  • Время выполнения может превысить лимит; рекомендуется set_time_limit(0) для длительных операций.

Как прочитать файл построчно с использованием встроенного SPL-итератора?

SplFileObject

$file = new SplFileObject('large.log');
$file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
foreach ($file as $line) {
    processLine($line);
}

SplFileObject реализует интерфейс SeekableIterator и автоматически управляет указателем файла. Чтение происходит по одной строке за итерацию.

Особенности:

  • Производительность может быть чуть ниже ручного fopen/fgets из-за накладных расходов на объект.
  • Методы fgetcsv() доступны через setFlags(SplFileObject::READ_CSV).

Как обработать миллионы записей из базы данных без перегрузки памяти?

Небуферизованные запросы к MySQL (PDO)

$pdo = new PDO('mysql:host=...;dbname=...', 'user', 'pass', [
    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false
]);
$stmt = $pdo->query('SELECT * FROM large_table');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    processRow($row);
}

Отключая буферизацию, PDO не сохраняет все строки на стороне клиента. Каждая строка извлекается по мере необходимости из сетевого буфера.

Ограничения:

  • Во время итерации нельзя выполнять другие запросы к тому же соединению (в MySQL).
  • Необходимо следить за временем выполнения и блокировками, если используется транзакция.

Как обработать таблицу большими пачками, если курсор недоступен?

Чанкинг с использованием LIMIT и ключевого поля

function yieldRowsByChunks(PDO $pdo, string $table, int $chunkSize): Generator
{
    $lastId = 0;
    while (true) {
        $rows = $pdo->query("SELECT * FROM $table WHERE id > $lastId ORDER BY id LIMIT $chunkSize")
                     ->fetchAll(PDO::FETCH_ASSOC);
        if (empty($rows)) break;
        foreach ($rows as $row) {
            yield $row;
            $lastId = $row['id'];
        }
    }
}
foreach (yieldRowsByChunks($pdo, 'large_table', 1000) as $row) {
    process($row);
}

Этот вариант использует автоинкрементный идентификатор для обхода проблем со смещением при изменениях данных. Генератор скрывает пачки, возвращая строки по одной.

Недостатки:

  • Требуется уникальное сортируемое поле.
  • Первый запрос может быть медленным при большом смещении, но здесь смещения нет, работает от последнего id.

Как упростить работу с большими CSV файлами с помощью готовой библиотеки?

Библиотека League\Csv

require 'vendor/autoload.php';
use League\Csv\Reader;

$reader = Reader::createFromPath('sales.csv');
$reader->setHeaderOffset(0);
foreach ($reader->getRecords() as $record) {
    processSale($record);
}

Библиотека сама использует генераторы и итераторы, обеспечивая потоковую обработку. Поддерживает фильтры, преобразования и запись без утечек памяти.

Замечания:

  • Необходимо установка через Composer (league/csv).
  • Вызов getRecords() возвращает генератор; не следует вызывать fetchAll() на больших файлах.

Расширенные примеры обработки больших данных

Пример 1: Чтение CSV и вычисление агрегата

Пример
function readCsvLines(string $file): Generator
{
    $handle = fopen($file, 'r');
    while (($line = fgets($handle)) !== false) {
        yield str_getcsv($line);
    }
    fclose($handle);
}

$totalAmount = 0.0;
$lineCount = 0;
foreach (readCsvLines('orders.csv') as $row) {
    // Предположим, что в строках: id, amount, date
    $totalAmount += (float)$row[1];
    $lineCount++;
}
echo 'Сумма заказов: ' . $totalAmount;
echo 'Обработано строк: ' . $lineCount;
echo 'Пик памяти: ' . memory_get_peak_usage(true) . ' байт';
Сумма заказов: 9876543210.12
Обработано строк: 15000000
Пик памяти: 2097152 байт

Пример 2: Фильтрация гигантского лог-файла по регулярному выражению

Пример
function grepGenerator(string $file, string $pattern): Generator
{
    $handle = fopen($file, 'r');
    if (!$handle) {
        throw new RuntimeException('Нельзя открыть файл');
    }
    while (($line = fgets($handle)) !== false) {
        if (preg_match($pattern, $line)) {
            yield $line;
        }
    }
    fclose($handle);
}

$count = 0;
foreach (grepGenerator('/^error|warning/', 'application.log') as $line) {
    $count++;
    // Здесь можно записать отфильтрованные строки в другой файл
}
echo 'Найдено совпадений: ' . $count;

Пример 3: Использование SplFileObject с CSV и заголовками

Пример
$file = new SplFileObject('employees.csv');
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$headers = $file->fgetcsv(); // первая строка - заголовки
$file->next(); // перейти ко второй строке
while ($row = $file->fgetcsv()) {
    if ($row === null) break;
    $record = array_combine($headers, $row);
    processEmployee($record);
}

Пример 4: Небуферизованный запрос с отправкой данных на REST API

Пример
$pdo = new PDO('mysql:host=db.example.com;dbname=analytics', 'user', 'pass', [
    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false
]);
$stmt = $pdo->query('SELECT id, name, email FROM subscribers WHERE active=1');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/subscribers/batch');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$batch = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $batch[] = $row;
    if (count($batch) >= 100) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($batch));
        $response = curl_exec($ch);
        if (curl_errno($ch)) {
            // обработка ошибки
        }
        $batch = [];
    }
}
if (!empty($batch)) {
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($batch));
    curl_exec($ch);
}
curl_close($ch);

Пример 5: Чтение нескольких файлов последовательно с помощью генератора

Пример
function readMultipleFiles(array $filePaths): Generator
{
    foreach ($filePaths as $path) {
        $handle = fopen($path, 'r') ?: throw new RuntimeException('Не удалось открыть ' . $path);
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
        fclose($handle);
    }
}
foreach (readMultipleFiles(['part1.csv', 'part2.csv', 'part3.csv']) as $line) {
    // обрабатываем строки из всех файлов подряд
}

Работа с большими данными в PHP - comments

En
большие данные php (php)