rzaripov.kz

Разработка парсера Excel-файлов на PHP с PhpSpreadsheet

Обработка электронных таблиц часто требуется интернет-магазинам, корпоративным системам, CRM и внутренним сервисам. Прайсы поставщиков, каталоги товаров, отчёты и списки сотрудников обычно хранятся в форматах XLSX или XLS, поэтому веб-приложению нужен надёжный механизм чтения, проверки и преобразования таких данных.

В PHP для этой задачи удобно использовать библиотеку PhpSpreadsheet. Она поддерживает основные форматы Excel, позволяет обращаться к ячейкам и строкам, работать с листами, формулами, стилями и датами. При этом парсер можно встроить почти в любой проект: от небольшого административного раздела до фонового сервиса массового импорта.

Установка И Настройка Библиотеки

PhpSpreadsheet распространяется через Composer. В корневой папке проекта достаточно выполнить команду:

composer require phpoffice/phpspreadsheet

После установки подключается автозагрузчик Composer, а затем классы, необходимые для чтения таблицы:

require __DIR__ . '/vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\IOFactory;

Файл можно загрузить из формы, локального каталога или удалённого хранилища. Для пользовательской загрузки важно проверять размер, расширение и фактический MIME-тип. Одного расширения недостаточно: файл с именем catalog.xlsx может содержать неподходящие или потенциально опасные данные.

$path = $_FILES['document']['tmp_name'];

$spreadsheet = IOFactory::load($path);
$sheet = $spreadsheet->getActiveSheet();

Метод load() автоматически определяет формат во многих типичных случаях. Если формат известен заранее, лучше использовать специализированный ридер. Это немного ускоряет обработку и помогает избежать ошибок при работе с повреждёнными или нестандартными документами.

Чтение Строк И Ячеек

Лист Excel можно обойти по строкам, начиная с первой строки с заголовками. Метод toArray() преобразует диапазон в обычный массив PHP, что удобно для небольших файлов:

$rows = $sheet->toArray(null, true, true, true);

foreach ($rows as $number => $row) {
    $name = trim((string) ($row['A'] ?? ''));
    $price = $row['B'] ?? null;
    $stock = $row['C'] ?? null;

    if ($number === 1) {
        continue;
    }

    if ($name === '') {
        continue;
    }

    // Сохранение проверенной записи
}

Параметры toArray() определяют значение для пустых ячеек, обработку формул и индексацию колонок. Последний аргумент со значением true сохраняет буквенные ключи A, B, C. При импорте сложных таблиц полезнее заранее сопоставить заголовки с внутренними полями, а не полагаться на позиции столбцов.

$header = array_map(
    static fn ($value) => mb_strtolower(trim((string) $value)),
    $rows[1]
);

$columns = array_flip($header);
$productName = $row[$columns['название']] ?? '';

Такой подход делает обработчик устойчивее к перестановке колонок. Перед записью в базу данных следует нормализовать пробелы, привести числовые значения к нужному типу и проверить обязательные поля. Ошибочные строки можно складывать в отдельный массив с номером строки и описанием проблемы.

Выбор Подхода Для Разных Форматов

PhpSpreadsheet подходит для чтения XLSX и XLS, а также умеет работать с CSV и рядом других форматов. Однако универсальность библиотеки не означает, что для любого файла нужно применять один и тот же сценарий. Способ чтения выбирают с учётом размера документа, наличия формул и требований к скорости.

Формат или подход Подходящий инструмент Особенности Когда использовать
XLSX Xlsx или IOFactory Поддержка листов, формул и форматирования Современные таблицы Excel
XLS Xls или IOFactory Старый бинарный формат Обмен с устаревшими системами
CSV Csv или стандартный fgetcsv() Нет листов и стилей Большие плоские выгрузки
Только нужный лист setLoadSheetsOnly() Снижает объём чтения Книги с большим числом вкладок
Только значения setReadDataOnly(true) Формулы читаются без вычисления Импорт готовых результатов

Для управления чтением можно создать ридер вручную:

$reader = IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(true);
$reader->setLoadSheetsOnly(['Товары']);

$spreadsheet = $reader->load($path);

Опция setReadDataOnly(true) уменьшает объём служебной информации, если стили и формулы не нужны. Следует учитывать, что PhpSpreadsheet не всегда вычисляет формулы так, как это делает Microsoft Excel. Для импорта обычно используют сохранённые результаты формул либо заранее подготавливают файл.

CSV-файлы при большом размере часто выгоднее обрабатывать потоково через fgetcsv(). В этом случае в памяти находится одна строка, а не весь документ. Для XLSX можно использовать фильтры чтения или ограничивать диапазон, если структура файла позволяет это сделать.

Проверка Данных Перед Импортом

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

$errors = [];
$products = [];

foreach ($rows as $line => $row) {
    if ($line === 1) {
        continue;
    }

    $sku = trim((string) ($row['A'] ?? ''));
    $title = trim((string) ($row['B'] ?? ''));
    $price = filter_var($row['C'] ?? null, FILTER_VALIDATE_FLOAT);

    if ($sku === '') {
        $errors[] = "Строка {$line}: отсутствует артикул";
        continue;
    }

    if ($title === '') {
        $errors[] = "Строка {$line}: отсутствует название";
        continue;
    }

    if ($price === false || $price < 0) {
        $errors[] = "Строка {$line}: некорректная цена";
        continue;
    }

    $products[] = [
        'sku' => $sku,
        'title' => $title,
        'price' => $price,
    ];
}

Проверка должна учитывать локальные особенности данных. Цена может содержать запятую вместо точки, артикул — ведущие нули, а дата — быть числом Excel, текстом или объектом даты. Преждевременное приведение артикула к целому числу способно уничтожить значимые нули.

Повторный импорт также требует отдельной стратегии. Для уникальных записей можно использовать артикул или внешний идентификатор и выполнять обновление через INSERT ... ON DUPLICATE KEY UPDATE. Перед массовой записью удобно сохранить исходный файл, сформировать журнал обработки и выполнить импорт в транзакции.

Даты Производительность И Безопасность

Excel хранит даты как числовые значения, отсчитываемые от специальной начальной даты. Для преобразования дат PhpSpreadsheet предоставляет класс Shared\Date:

use PhpOffice\PhpSpreadsheet\Shared\Date;

$value = $sheet->getCell('D2')->getValue();

if (is_numeric($value)) {
    $date = Date::excelToDateTimeObject($value);
    $formatted = $date->format('Y-m-d');
} else {
    $formatted = date('Y-m-d', strtotime((string) $value));
}

Проверка результата обязательна: strtotime() может вернуть некорректное значение, если в таблице находится произвольный текст. Для денежных сумм желательно применять точные правила округления и хранить значения в базе данных в подходящем числовом формате, а не в виде строки.

На производительность влияют размер книги, количество листов, стили и объём пустых областей. Не следует загружать всю рабочую книгу, если нужен один лист. Для крупных файлов импорт лучше запускать в очереди или консольной команде, показывая пользователю статус выполнения.

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

Пример Готового Сценария

Ниже приведён упрощённый вариант обработчика, который выбирает лист, пропускает заголовок и формирует нормализованный массив товаров:

require __DIR__ . '/vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\IOFactory;

$reader = IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);

$book = $reader->load($path);
$sheet = $book->getSheetByName('Товары');

if ($sheet === null) {
    throw new RuntimeException('Лист "Товары" не найден');
}

foreach ($sheet->getRowIterator(2) as $row) {
    $cells = $row->getCellIterator();
    $cells->setIterateOnlyExistingCells(false);

    $values = [];
    foreach ($cells as $cell) {
        $values[] = $cell->getValue();
    }

    $sku = trim((string) ($values[0] ?? ''));
    $name = trim((string) ($values[1] ?? ''));

    if ($sku === '' || $name === '') {
        continue;
    }

    // Передача записи в слой базы данных
}

Итераторы полезны, когда таблица содержит много строк и не хочется заранее создавать полный массив через toArray(). При этом границы используемого диапазона желательно контролировать, потому что форматирование далёких ячеек иногда заставляет библиотеку считать лист значительно больше фактических данных.

Надёжный парсер Excel на PHP обычно состоит из нескольких уровней: загрузчик файла, выбор формата и листа, чтение строк, нормализация, валидация, журнал ошибок и сохранение в базу данных. PhpSpreadsheet закрывает задачу доступа к структуре документа, а качество импорта определяется тем, насколько аккуратно приложение обрабатывает реальные данные и исключительные ситуации.