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-скрипт для