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

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

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

Начать курс

PHP и Big Data: миф или реальность?

Когда заходит речь об обработке больших данных (Big Data), в голову обычно приходят Java, Python или Scala с их могучими фреймворками вроде Hadoop и Spark. PHP же часто воспринимают как «язык для сайтов» или «движок для WordPress». Но давайте посмотрим правде в глаза: огромное количество бизнес-задач решается именно на PHP. И да, он умеет работать с объёмами данных, от которых у новичка может закружиться голова.

Представьте: вам нужно обработать 50-гигабайтный лог-файл веб-сервера, собрать оттуда все уникальные IP-адреса и сформировать отчёт. Или выгрузить из CRM миллион записей в Excel. Или спарсить прайс-листы поставщиков в реальном времени. PHP справится со всем этим, если подойти к делу с умом.

В этой статье мы разберём, как превратить PHP из «скриптового мальчика» в мощный инструмент для обработки больших данных. Никакой магии — только код и проверенные техники.

Почему PHP не любят за Big Data (и зря)

Главная претензия к PHP — это модель выполнения. По умолчанию PHP-скрипт:

  • Работает последовательно (один поток);
  • Имеет ограничение по времени выполнения (обычно 30 секунд);
  • Съедает всю память, если загрузить большой файл целиком.

Но это всё настраивается! Более того, PHP 7 и 8 совершили настоящий прорыв в производительности. А с появлением JIT-компиляции в PHP 8.0 скорость выполнения некоторых задач приблизилась к C-расширениям.

Да, PHP не заменит Hadoop для распределённых кластеров. Но для однопоточных задач на одном сервере (а таких большинство в малом и среднем бизнесе) PHP — отличный выбор.

Правильная настройка окружения

Прежде чем нырять в код, убедитесь, что ваш PHP настроен на «долгие заплывы». Откройте php.ini и установите:

max_execution_time = 0
memory_limit = 2048M

max_execution_time = 0 — снимает ограничение по времени. memory_limit — выделяем достаточно памяти, но не перебарщиваем (2 ГБ — хороший старт).

Но помните: выделять память — это не значит её использовать. Настоящие профи не загружают гигабайты в оперативку. Они читают данные потоково.

Техника №1: Потоковая обработка через генераторы

Генераторы — это киллер-фича PHP для работы с большими данными. Они позволяют обрабатывать файлы любого размера, не загружая их в память целиком.

Вот как выглядит классический антипаттерн (так делать НЕ надо):

// Плохо: загружаем весь файл в память
$data = file('huge_log.txt');
foreach ($data as $line) {
    // что-то делаем
}

Если файл весит 10 ГБ — скрипт упадёт с ошибкой памяти. Правильный подход — использовать генератор:

// Хорошо: построчное чтение без загрузки в память
function readLargeFile(string $filename): Generator
{
    $handle = fopen($filename, 'r');
    if ($handle === false) {
        throw new RuntimeException("Не удалось открыть файл: $filename");
    }
    
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    
    fclose($handle);
}

// Использование
foreach (readLargeFile('huge_log.txt') as $lineNumber => $line) {
    // Обрабатываем каждую строку по отдельности
    if (preg_match('/^192\.168\./', $line)) {
        echo "Локальный IP найден: $line\n";
    }
}

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

Техника №2: Парсинг CSV и TSV без боли

CSV-файлы — это классика «больших данных» в PHP-мире. Клиенты любят присылать таблицы на 500 000 строк. Стандартная функция fgetcsv() работает отлично, но только если вы не пытаетесь загрузить всё сразу.

Вот промышленный пример обработки CSV с фильтрацией:

function processLargeCSV(string $filename, callable $rowCallback): int
{
    $handle = fopen($filename, 'r');
    if ($handle === false) {
        throw new RuntimeException("Файл не найден: $filename");
    }
    
    $header = fgetcsv($handle); // Читаем заголовки
    $processed = 0;
    
    while (($row = fgetcsv($handle)) !== false) {
        // Пропускаем пустые строки
        if (count($row) === 1 && empty($row[0])) {
            continue;
        }
        
        // Создаём ассоциативный массив
        $data = array_combine($header, $row);
        
        // Вызываем callback для каждой строки
        $rowCallback($data);
        $processed++;
        
        // Прогресс-бар (каждые 10 000 строк)
        if ($processed % 10000 === 0) {
            echo "Обработано: $processed строк\n";
        }
    }
    
    fclose($handle);
    return $processed;
}

// Использование
$total = processLargeCSV('products_1m.csv', function($product) {
    // Фильтруем только активные товары
    if ($product['status'] === 'active' && $product['price'] > 0) {
        // Сохраняем в БД или пишем в другой файл
        file_put_contents(
            'active_products.csv',
            implode(',', $product) . "\n",
            FILE_APPEND
        );
    }
});

echo "Всего обработано: $total товаров";

Техника №3: Агрегация данных без БД

Иногда нужно быстро собрать статистику из огромного лога, не поднимая базу данных. PHP с этим справляется на ура. Пример: подсчёт количества запросов по каждому HTTP-статусу из access.log.

function analyzeAccessLog(string $logFile): array
{
    $stats = [];
    $handle = fopen($logFile, 'r');
    
    while (($line = fgets($handle)) !== false) {
        // Типичный формат: IP - - [дата] "GET /url HTTP/1.1" 200 1234
        if (preg_match('/"([A-Z]+) ([^"]+)" (\d+)/', $line, $matches)) {
            $method = $matches[1];  // GET, POST, etc.
            $url    = $matches[2];  // /page.php
            $status = $matches[3];  // 200, 404, 500
            
            // Агрегируем
            $key = "$method|$status";
            if (!isset($stats[$key])) {
                $stats[$key] = ['method' => $method, 'status' => $status, 'count' => 0];
            }
            $stats[$key]['count']++;
        }
    }
    
    fclose($handle);
    
    // Сортируем по убыванию
    usort($stats, fn($a, $b) => $b['count'] - $a['count']);
    
    return $stats;
}

$results = analyzeAccessLog('/var/log/nginx/access.log');                

Блоги

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