Обработка больших данных на PHP: от мифа к реальному проекту

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

PHP и Big Data: не оксюморон, а реальность

Когда слышишь "обработка больших данных", первое, что приходит на ум — это Spark, Hadoop или Python с Pandas. PHP в этом списке обычно воспринимается как "язык для лендингов и форумов". Но давайте посмотрим правде в глаза: если ваш проект уже живёт на PHP, переписывать его целиком на Go ради одной задачи по обработке гигабайтного CSV — это стрельба из пушки по воробьям.

PHP отлично справляется с Big Data, если подойти к вопросу с умом. В этой статье мы разберём реальные техники: как не убить сервер, работая с файлами на 10+ ГБ, как обрабатывать миллионы строк из базы данных без out of memory, и как превратить PHP в настоящий конвейер данных. Без воды, только код и архитектурные решения.

Почему PHP "не любит" большие данные (и как это исправить)

Главная проблема PHP в контексте Big Data — это его модель выполнения. Скрипт выполняется последовательно, загружая всё в память. Попробуйте сделать file_get_contents() на файл размером 5 ГБ — сервер просто упадёт.

Но есть и хорошая новость: в PHP есть встроенные механизмы для потоковой обработки. Нужно лишь забыть о "варварских" методах вроде file() или file_get_contents() и перейти на генераторы, потоки и chunk-запросы.

Генераторы: ваш главный инструмент

Генераторы (ключевое слово yield) позволяют возвращать данные по одному элементу, не загружая весь набор в память. Это идеально для чтения больших файлов или выборок из БД.

Пример: читаем CSV-файл на 5 ГБ построчно, не убивая память:

<?php
function readLargeCSV(string $filePath): Generator
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new RuntimeException("Не удалось открыть файл: $filePath");
    }

    // Пропускаем BOM, если есть
    $bom = fread($handle, 3);
    if ($bom !== "\xEF\xBB\xBF") {
        rewind($handle); // Если BOM нет, возвращаемся в начало
    }

    while (($row = fgetcsv($handle, 0, ',')) !== false) {
        yield $row;
    }

    fclose($handle);
}

// Использование
foreach (readLargeCSV('/data/orders_2024.csv') as $index => $row) {
    // Обрабатываем каждую строку по одной
    if ($index === 0) {
        continue; // пропускаем заголовок
    }
    processOrder($row);
}
?>

Почему это работает? Генератор не загружает весь файл в массив. Он читает одну строку — отдаёт её — читает следующую. Потребление памяти остаётся на уровне нескольких килобайт, независимо от размера файла.

Работа с большими базами данных: Chunk-запросы

Если вам нужно обработать 5 миллионов записей из MySQL, не пытайтесь сделать SELECT * FROM table — это убьёт и БД, и PHP. Вместо этого используйте курсоры или chunk-запросы.

Вот правильный подход с использованием PDO и chunk-итерации:

<?php
function processLargeTable(PDO $pdo, int $chunkSize = 1000, callable $callback): void
{
    $offset = 0;
    $lastId = 0;

    // Используем ORDER BY id для стабильности
    $stmt = $pdo->prepare("SELECT * FROM orders WHERE id > :lastId ORDER BY id LIMIT :limit");

    do {
        $stmt->execute(['lastId' => $lastId, 'limit' => $chunkSize]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (empty($rows)) {
            break;
        }

        foreach ($rows as $row) {
            $callback($row);
            $lastId = $row['id']; // Запоминаем последний обработанный ID
        }

        // Освобождаем память
        unset($rows);
        $stmt->closeCursor();

    } while (true);
}

// Использование
$pdo = new PDO('mysql:host=localhost;dbname=big_data', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

processLargeTable($pdo, 500, function (array $row) {
    // Тут ваша логика — отправка в API, запись в другой файл и т.д.
    echo "Обработан заказ #{$row['id']}\n";
});
?>

Этот паттерн гарантирует, что вы никогда не загрузите в память больше, чем один чанк (обычно 500-1000 строк). А ключевой момент — мы используем id > :lastId, что позволяет избежать дубликатов и не терять прогресс при падении скрипта.

Парсинг гигантских JSON и XML: потоковый подход

JSON и XML — ещё одна боль для PHP-разработчиков. Стандартный json_decode() загружает весь JSON в память. Если ваш API-ответ весит 2 ГБ — вы в беде.

Потоковый JSON с JSON Machine

Библиотека JSON Machine позволяет читать JSON как поток, не загружая весь документ. Идеально для больших JSON-массивов.

<?php
require 'vendor/autoload.php';

use JsonMachine\Items;

$file = fopen('/data/users.json', 'r');
$users = Items::fromStream($file, ['pointer' => '/users']);

foreach ($users as $user) {
    // $user — это объект, но каждый раз только один
    processUser($user);
}

fclose($file);
?>

Важно: JSON Machine поддерживает только массивы объектов на верхнем уровне или по указанному JSON Pointer. Для вложенных структур нужно настроить pointer правильно.

Потоковый XML с XMLReader

PHP имеет встроенный XMLReader, который работает как курсор по XML-документу. Это не DOM, который грузит всё в память, а именно потоковый парсер.

<?php
function parseLargeXML(string $filePath): void
{
    $reader = new XMLReader();
    $reader->open($filePath);

    while ($reader->read()) {
        if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'order') {
            // Читаем текущий элемент целиком
            $orderXml = $reader->readOuterXml();

            // Парсим его в SimpleXML (он маленький — только один узел)
            $order = simplexml_load_string($orderXml);
            processOrderFromXml($order);
        }
    }

    $reader->close();
}

parseLargeXML('/data/orders_2024.xml');
?>

Этот подход позволяет обрабатывать XML-файлы любого размера — вы всегда работаете только с одним узлом за раз.

Оптимизация памяти и производительности: чек-лист

Чтобы ваш PHP-скрипт для

Блоги

Рекомендаций по книгам