Представьте ночное задание, которое собирает книгу со счетами в коде и записывает её в CSV для импорта нижестоящей системой. Числа в Excel выглядят правильно. CSV открывается в текстовом редакторе без проблем. Затем импортёр давится на столбце итогов, потому что поле суммы для строки 42 содержит =SUM(D2:D41) — формулу как буквальный текст, а не значение, которое она должна была вычислить. Ничего не сломано. Это документированное поведение, и это первое, что нужно понять об экспорте из HotXLS: писатель сериализует модель ячеек ровно в том виде, в каком она есть, а у ячейки с формулой, значение которой никогда не вычислялось, есть только текст формулы, который можно передать
Почему в вашем CSV формулы вместо чисел
HotXLS хранит текст формулы и вычисленное значение как две отдельные вещи. SaveAsCSV намеренно не запускает вычислительный движок на выходе: экспорт не должен изменять книгу и не должен рисковать зависанием на патологической цепочке формул. Файлы, которые сохранил сам Excel, несут закэшированные результаты рядом с формулами, поэтому их повторный экспорт ведёт себя ожидаемым образом. Ловушка специфична для книг, сгенерированных вашим собственным кодом, где формулы были записаны, но никогда не вычислялись. Решение — обеспечить наличие значений ещё до экспорта, используя тот же движок Calculate, который разрешает межлистовые ссылки и пользовательские функции:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
R: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('invoice-run.xlsx');
Sheet := Book.Sheets[0];
// Материализовать результаты формул, чтобы CSV нёс числа, а не текст '=...'
for R := 2 to 41 do
if Sheet.Cells[R, 4].Formula <> '' then
Sheet.Cells[R, 4].Value := Book.Calculate(Sheet.Cells[R, 4].Formula);
Book.SaveAsCSV('feed.csv', 0, ','); // лист 0, запятая
Book.SaveAsCSV('feed.tsv', 0, #9); // тот же лист в формате TSV
finally
Book.Free;
end;
end;
Обратите внимание, что на самом деле делает этот цикл: он перезаписывает ячейки с формулами их вычисленными значениями. Это абсолютно правильно для одноразового прохода экспорта и неправильно, если вы затем собираетесь снова сохранить книгу как .xlsx, потому что вы только что заменили живые формулы замороженными числами. Экспортируйте из копии или ограничьте область обратной записи так, чтобы она затрагивала только сам прогон экспорта. Движок за Calculate идёт дальше этого, включая регистрацию собственных функций, — тема статьи о движке формул HotXLS и пользовательских функциях
Что гарантирует писатель с разделителями
Путь CSV производит UTF-8 с меткой порядка байтов, окончаниями строк CRLF и кавычками по RFC 4180. Любое поле, содержащее разделитель, кавычку или разрыв строки, заключается в кавычки, а вложенные кавычки удваиваются. Даты отображаются как yyyy-mm-dd hh:nn:ss независимо от формата отображения ячейки. Это правильное решение для машинного потребителя, хотя оно удивляет всех, кто ожидал, что экранное форматирование перенесётся. Ячейки с форматированным текстом сплющиваются путём конкатенации их фрагментов
Эти значения по умолчанию улаживают большинство споров с импортёром ещё до их начала, но два из них всё равно стоит закрепить в контракте интерфейса. Первое — BOM. Именно он позволяет Excel открывать файл с сохранёнными акцентированными символами, однако горстка строгих парсеров воспринимает эти три байта как данные; если ваш парсер из их числа, удаляйте их при передаче. Второе — TSV. Это вообще не отдельная функция, а тот же писатель, вызванный с #9 в качестве разделителя, так что всё сказанное выше применимо к нему без изменений. Экспортируемый лист выбирается по индексу с отсчётом от 0 в перегрузке с несколькими аргументами, тогда как сокращённая форма с одним аргументом SaveAsCSV(FileName) берёт активный лист
Экспорт в HTML — это снимок, а не формат обмена
Там, где CSV отбрасывает всё, кроме значений, SaveAsHTML пытается сохранить внешний вид: одна <table> на лист, объединённые области выражены через colspan и rowspan, базовое оформление ячеек встроено инлайн как CSS. Цвета, привязанные к теме, пропускаются, а не разрешаются, поэтому шаблон, опирающийся на слоты темы, выглядит проще, чем в Excel. Задавайте явные цвета RGB для всего, что должно пережить этот переход. Объект параметров управляет оболочкой:
var
Opts: TXLSXHtmlExportOptions;
begin
Opts := TXLSXHtmlExportOptions.Create;
try
Opts.Title := 'Weekly settlement';
Opts.TableClass := 'report-grid'; // зацепка для таблицы стилей принимающей страницы
Opts.WriteDocument := True; // полная страница, а не фрагмент
if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
raise Exception.Create('Sheet index out of range');
finally
Opts.Free;
end;
end;
Две детали в этом фрагменте заслуживают внимания. Переключите WriteDocument в False, и результатом станет голый фрагмент таблицы вместо полной страницы — именно это нужно, когда предпросмотр встраивается в существующую разметку: задайте TableClass и позвольте таблице стилей принимающей страницы взять оформление на себя. Соглашение о возвращаемом значении здесь тоже обратное по сравнению с большинством вызовов HotXLS. SaveAsHTML возвращает 0 при успехе и -1 при неверном индексе листа, так что привычная проверка на = 1 покажет каждый успешный экспорт как отказ. Когда нужен не весь лист, а отдельная область — например, для отправки по почте или встраивания одного блока, — TXLSXRange.SaveAsHTML экспортирует любой прямоугольный диапазон по тем же правилам рендеринга
Вывод в RTF и где он всё ещё оправдан
Четвёртая цель — запись таблиц RTF 1.6, по одному листу за вызов через SaveAsRTF. Ширины столбцов приближённо переводятся из расчёта примерно 96 твипов на символ ширины столбца. Структурное ограничение, которое стоит знать: объединённые ячейки в результате не растягиваются — только якорная ячейка несёт содержимое, а покрытые ею ячейки выводятся пустыми. Это исключает RTF для шаблонов с насыщенной вёрсткой. Но он всё же оправдывает своё место как путь наименьшего сопротивления для помещения табличных результатов в текстовый процессор или в устаревшую систему управления документами, появившуюся раньше, чем приём HTML
Обратный цикл: импорт CSV разрушителен по замыслу
У чтения CSV обратно свой собственный контракт. OpenCSV очищает всю книгу целиком и перестраивает её как единственный лист с именем Sheet1. По сути это конструктор, а не слияние, поэтому никогда не вызывайте его на книге, которая всё ещё содержит несохранённое содержимое. Передача #0 в качестве разделителя запускает автоматическое определение разделителя. Флаг ADetectTypes управляет продвижением типов: при включении числовые строки становятся числами, строки ISO-8601 становятся датами, а true/false — булевыми значениями. Отключайте его, когда поток данных несёт идентификаторы с ведущими нулями, почтовые индексы или коды товаров — всё это продвижение типов молча портит, превращая в числа (ведущий ноль просто исчезает в момент, когда 00123 становится 123). Оба фасада предоставляют один и тот же импорт. Совместите его с вызовами экспорта выше, и вы получите мост форматов, для которого нигде в конвейере не нужен установленный Excel, — сценарий, рассмотренный в статье о генерации отчётов Excel из базы данных с HotXLS
Экспорт напрямую в поток
У каждого писателя здесь рядом с версией по имени файла есть перегрузка с потоком: у CSV, HTML, RTF и самих форматов книги. В серверном коде именно к этим перегрузкам и нужно обращаться. Веб-обработчик, отдающий CSV на скачивание, может писать в TMemoryStream и передавать его прямо объекту ответа — без временного файла, без задачи очистки и без коллизии между двумя запросами, которым случайно досталось одинаковое сгенерированное имя. То же самое верно для отправки экспорта в объектное хранилище или прикрепления к исходящей почте. Файловая система полностью выпадает из картины
Этот приём усиливается тем, как разворачивается библиотека. Оба фасада — нативные читатели и писатели Object Pascal, поэтому не нужна установка Excel, автоматизация COM и не возникает узкое место на процесс, сериализующее запросы на сервере. Каждый запрос может владеть собственным объектом книги, выполнять обратную запись вычислений из первого раздела и передавать свой экспорт потоком параллельно с соседними. Единственный ресурс, за которым нужно следить, — это память. Модель книги живёт в оперативной памяти на всё время экспорта, поэтому служба, открывающая очень большие файлы только для того, чтобы заново выдать их как CSV, должна ограничивать число одновременных задач или ставить в очередь особо крупные, а не позволять всплеску трафика определять рабочий набор
Ещё одна, менее заметная настройка: включайте IncludeBOM в параметрах HTML, когда фрагмент будет сохранён как отдельный файл, кодировку которого какой-то инструмент дальше по цепочке определяет по содержимому. Когда вы отдаёте HTML напрямую по HTTP, вместо этого оставьте объявление кодировки заголовкам ответа
Когда байты всё равно выходят неправильными
Самый частый вопрос в поддержку об экспорте CSV — это та же проблема, что и в начале статьи, но в другом костюме: Excel показывает кракозябры вместо акцентированных символов. Первый порыв — обвинить писателя, но он как раз по этой причине выдаёт BOM в UTF-8, и файл почти всегда корректен в момент, когда покидает ваш код. Что-то между этим моментом и Excel съело BOM. Передача по FTP в текстовом режиме, копирование потока, пропускающее первые три байта, прокси, перекодирующий данные на лету, — любое из этого сотрёт маркер и заставит Excel гадать о кодировке, а делает он это плохо. Диагностируйте это на границе, а не в самом вызове экспорта. Откройте доставленный файл в hex-редакторе и убедитесь, что EF BB BF всё ещё стоит первым
Это и есть сквозная линия для всех четырёх форматов. Сам вызов экспорта — простая часть, и в каждом решении, с которым сталкивается писатель, HotXLS делает обоснованный выбор. Сбои живут на стыках: там, где текст формулы встречается с парсером, ожидавшим число, там, где BOM встречается с транспортом, который его не сохраняет, там, где объединённая ячейка встречается с плоской табличной моделью RTF. Каждый из этих фактов нужно закрепить в контракте между вашим экспортёром и тем, что его потребляет, потому что потребитель не может прочитать ваши намерения по одним лишь байтам. Полный список методов для обоих фасадов книги приведён на странице продукта HotXLS Delphi Component