Один open() на XLSX, XLS и CSV: что нового в FastExcelReader 4.2
Есть у нас маленькая традиция: не успеваем написать в блог про новую версию, как выходит следующая. Не успели, как говорится, высохнуть чернила в статье про версию 4.0 с долгожданным чтением legacy-XLS, как пишем уже про 4.2. Так что давайте одним махом: коротко — что случилось между релизами, и подробно — про главную новость 4.2.
А главная новость простая и приятная: Excel::open() теперь читает XLSX, XLS и CSV — один метод, один код, без единого if по расширению файла.
Боль: импорт, который ветвится по расширению
Классический приёмник файлов от пользователя выглядит так — и наверняка вы такое писали:
use avadim\FastExcelReader\Excel;
$reader = match (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
'csv', 'txt', 'tsv' => Excel::openCsv($file),
default => Excel::open($file), // xlsx / xls
};
foreach ($reader->withHeader()->nextRow() as $rowNum => $row) {
importRow($row, $rowNum);
}
Работает — пока расширение говорит правду. А оно врёт постоянно:
- менеджер сохранил «CSV» из русского Excel, а внутри — настоящий XLSX с расширением
.csv; - интеграция кладёт выгрузку в файл
report.datили вовсе без расширения; - пользователь переименовал
.xlsв.xlsx, потому что «так система просила», и внутри — старый OLE2-бинарник; - временный файл от загрузчика приходит как
php7A2B.tmp, иpathinfo()разводит руками.
Каждый такой случай — это либо криптическая ошибка где-то в глубине парсера, либо, что хуже, молча испорченный импорт. Расширение — это подсказка от файловой системы, а не факт о содержимом.
Решение: определение формата по сигнатуре
В 4.0 мы научили Excel::open() выбирать ридер по сигнатуре файла, а не по расширению — тогда это касалось пары XLSX/XLS. В 4.2 в ту же точку входа добрали CSV. Теперь open() смотрит на первые байты файла и решает сам:
- сигнатура OLE2 (
D0 CF 11 E0 …) — это legacy-XLS; - сигнатура ZIP (
PK\x03\x04) — это XLSX; - всё остальное — читается как текст с разделителями, то есть CSV.
Расширение при этом не смотрится вообще. Тот же приёмник теперь — одна строка:
use avadim\FastExcelReader\Excel;
$book = Excel::open($file); // xlsx, xls или csv — определится по содержимому
foreach ($book->withHeader()->nextRow() as $rowNum => $row) {
importRow($row, $rowNum);
}
CSV-файл теперь возвращается из open() как Csv\CsvBook — обычная книга с одним листом, у которой тот же самый интерфейс чтения (AbstractSheet), что и у XLSX и XLS: те же withHeader(), nextRow(), readRows(), readColumns(), режимы ключей KEYS_* и области чтения. Импорт «принимаем что дадут» перестаёт быть тремя ветками кода и становится одной.
Для паритета с XLSX ключами колонок через open() по умолчанию становятся буквы — A, B, C — так что код, написанный под Excel, читает CSV без правок.
Когда нужно подсказать явно
Автоопределение надёжно, но иногда формат известен заранее — тогда его можно зафиксировать. Второй аргумент open() (появился в 4.2, обратная совместимость сохранена) принимает опции CSV-ридера, а 'format' => 'csv' принудительно включает CSV-режим:
// заставить читать как CSV и сразу настроить парсер
$book = Excel::open($file, [
'format' => 'csv',
'delimiter' => ';',
'encoding' => 'Windows-1251',
]);
Если нужна не книга, а низкоуровневый движок парсинга (с onError(), толерантным режимом и прочей CSV-спецификой из отдельной статьи про CSV-ридер), он под рукой:
$reader = $book->getReader(); // Csv\CsvReader изнутри CsvBook
// или сразу, как и раньше:
$reader = Excel::openCsv($file);
openCsv() никуда не делся и по-прежнему возвращает движок Csv\CsvReader напрямую — если вам нужен именно он, а не книжный API.
Заодно — внятные ошибки вместо криптических
Раз уж open() теперь распознаёт форматы, он заодно перестал загадочно падать на неподходящих файлах:
- ZIP, но не XLSX (например, DOCX или PPTX, которые тоже ZIP-контейнеры) даёт понятное сообщение «Not an XLSX workbook: the ZIP archive has no xl/workbook.xml» с подсказкой про DOCX/PPTX — вместо прежнего криптического «Internal file not found: xl/_rels/workbook.xml.rels»;
- бинарный мусор (NUL-байт или высокая доля управляющих символов) отклоняется сразу с сообщением «file appears to be binary», а не уходит ломаться вглубь парсера. UTF-16/UTF-32-текст под эту проверку не попадает; при необходимости её можно отключить опцией
allow_binary(илиCsvOptions::setAllowBinary(true)).
А для тех, кому детект нужен отдельно от чтения, появился Excel::isXlsx() — проверка сигнатуры PK\x03\x04, симметричная давнему Excel::isXls().
Что было в 4.1: даты перестали зависеть от локали
Тот самый релиз, что проскочил между 4.0 и 4.2. Встроенные форматы дат (коды numFmtId 14–22, в том числе «короткая дата» под кодом 14) раньше могли рендериться по-разному в зависимости от того, установлено ли ext-intl и какая локаль стоит на сервере — один и тот же файл давал разные строки на разных машинах. В 4.1 эти коды разрешаются в фиксированные шаблоны независимо от окружения.
Если старое поведение (рендер дат по локали) нужно осознанно — его теперь включают явно:
$excel->useLocaleFormats('ru_RU'); // требует ext-intl; без аргумента — локаль процесса
Честные границы
CSV, прочитанный через open(), — это всё же CSV, а не книга Excel:
- в нём нет стилей, типизации чисел и дат, объединённых ячеек и картинок. Общие методы чтения, которые есть у всех форматов, для CSV просто отдают пустоту:
readCellStyles()вернёт значения без оформления,getMergedCells()— пустой массив. Единый API означает единые вызовы, а не волшебное появление данных, которых в формате нет; - библиотека по-прежнему только читает — писать и редактировать файлы — это к FastExcelWriter;
- и по-прежнему не вычисляет формулы — отдаёт текст формулы и последнее сохранённое значение.
Кому стоит обновиться
Всем, у кого на входе — файлы «со стороны»: пользовательские загрузки, интеграции, миграции, выгрузки из 1С и прочих систем, где расширение файла — это пожелание, а не гарантия. В 4.2 приёмник таких файлов сжимается до одного Excel::open($file), который сам разберётся, XLSX перед ним, XLS или CSV, и отдаст всё через один и тот же интерфейс. Апгрейд безопасный: второй аргумент open() опционален, openCsv() работает как прежде.
composer require avadim/fast-excel-reader
Документация и примеры — в репозитории на GitHub. А про 4.3 постараемся написать до выхода 4.4. Постараемся.