Работа с большими наборами данных в 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) {
// обрабатываем строки из всех файлов подряд
}