Один open() на XLSX, XLS и CSV: что нового в FastExcelReader 4.2

aVadim 26.07.2026 13:24

Есть у нас маленькая традиция: не успеваем написать в блог про новую версию, как выходит следующая. Не успели, как говорится, высохнуть чернила в статье про версию 4.0 с долгожданным чтением legacy-XLS, как пишем уже про 4.2. Так что давайте одним махом: коротко — что случилось между релизами, и подробно — про главную новость 4.2.

А главная новость простая и приятная: Excel::open() теперь читает XLSX, XLS и CSV — один метод, один код, без единого if по расширению файла.

Боль: импорт, который ветвится по расширению

Классический приёмник файлов от пользователя выглядит так — и наверняка вы такое писали:

php
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.

Расширение при этом не смотрится вообще. Тот же приёмник теперь — одна строка:

php
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-режим:

php
// заставить читать как CSV и сразу настроить парсер
$book = Excel::open($file, [
    'format'    => 'csv',
    'delimiter' => ';',
    'encoding'  => 'Windows-1251',
]);

Если нужна не книга, а низкоуровневый движок парсинга (с onError(), толерантным режимом и прочей CSV-спецификой из отдельной статьи про CSV-ридер), он под рукой:

php
$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 эти коды разрешаются в фиксированные шаблоны независимо от окружения.

Если старое поведение (рендер дат по локали) нужно осознанно — его теперь включают явно:

php
$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() работает как прежде.

bash
composer require avadim/fast-excel-reader

Документация и примеры — в репозитории на GitHub. А про 4.3 постараемся написать до выхода 4.4. Постараемся.

Комментарии (0)