Технічна стаття

Експорт книг Excel у CSV, TSV, HTML та RTF з Delphi за допомогою HotXLS

Уявіть нічне завдання, що будує книгу рахунків у коді й виводить її як CSV, щоб система нижче за течією її імпортувала. Числа в Excel мають правильний вигляд. CSV чисто відкривається в текстовому редакторі. А потім імпортер давиться стовпцем підсумків, бо поле суми в рядку 42 містить =SUM(D2:D41) — формулу як буквальний текст, а не число, яке вона мала б обчислити. Нічого не зламано. Це задокументована поведінка, і саме її треба зрозуміти першою про експорт із HotXLS: записувач серіалізує модель клітинок рівно такою, як вона є, а клітинці з формулою, значення якої ніколи не обчислювали, нічого передати, крім тексту самої формули

Чому ваш CSV містить формули замість чисел

HotXLS зберігає текст формули та обчислене значення як дві окремі речі. SaveAsCSV навмисно не запускає рушій обчислень по дорозі назовні: експорт не повинен мутувати книгу й не повинен ризикувати застрягти на патологічному ланцюжку формул. Файли, які зберіг сам Excel, несуть кешовані результати поруч із формулами, тож їхній повторний експорт поводиться так, як ви очікуєте. Пастка стосується саме тих книг, які згенерував ваш власний код, де формули записали, але жодного разу не обчислили. Ліки — зробити так, щоб значення існували ще до експорту, тим самим рушієм Calculate, який розв’язує посилання між аркушами та власні функції:

Діаграма: клітинка книги HotXLS у Delphi тримає лише текст формули, доки Book.Calculate не обчислить значення, тож експорт CSV видає число замість тексту =SUM
SaveAsCSV серіалізує модель клітинок як вона є — без 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 незалежно від формату відображення клітинки. Для машинного споживача це правильне рішення, хоча воно дивує всіх, хто чекав, що екранне форматування перенесеться. Клітинки з форматованим текстом сплющуються склеюванням їхніх фрагментів

Діаграма єдиного розділюваного писаря HotXLS у Delphi: CSV з комою і TSV з #9, при цьому обидва виходи мають спільні BOM UTF-8, закінчення CRLF і лапки за RFC 4180
CSV і TSV походять з одного записувача, тож UTF-8 BOM, закінчення CRLF і цитування RFC 4180 застосовуються до обох без змін

Ці усталені налаштування владнують більшість суперечок з імпортером ще до їх початку, але дві з них однаково варто внести до вашого контракту інтерфейсу. Перша — BOM. Саме він дає Excel відкрити файл із неушкодженими діакритичними символами, проте жменька суворих парсерів сприймає ці три байти як дані; якщо ваш саме такий, зрізайте їх під час передачі. Друга — TSV. Це взагалі не окрема можливість, а той самий записувач, викликаний із #9 як роздільником, тож усе сказане вище стосується його без змін. Аркуш для експорту обирається за індексом від нуля в перевантаженні з кількома аргументами, тоді як однопараметрове скорочення 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 показує кракозябри замість діакритичних символів. Інстинкт каже звинуватити записувач, але той видає UTF-8 BOM саме з цієї причини, і файл майже завжди правильний, коли покидає ваш код. Щось між тим місцем і Excel з’їло BOM. Передача FTP у текстовому режимі, копіювання потоку, що пропускає перші три байти, проксі, який перекодовує по дорозі, — будь-що з цього зріже маркер і лишить Excel вгадувати кодування, а вгадує він погано. Діагностуйте це на межі, а не у виклику експорту. Відкрийте доставлений файл у шістнадцятковому переглядачі й переконайтеся, що EF BB BF досі стоїть у ньому першим

Діаграма того, як правильний BOM UTF-8, записаний експортом CSV у HotXLS з Delphi, зрізається передачею FTP у текстовому режимі або проксі з перекодуванням, залишаючи Excel показувати кракозябри
Записувач видає EF BB BF правильно — каша з'являється лише після того, як транспорт здере маркер, тож діагностуйте доставлені байти в hex-переглядачі

Це наскрізна лінія для всіх чотирьох форматів. Виклик експорту — легка частина, і HotXLS робить обґрунтований вибір у кожному рішенні, перед яким постає записувач. Збої живуть на стиках: там, де текст формули зустрічає парсер, який хотів число; там, де BOM зустрічає транспорт, що його не зберігає; там, де об’єднана клітинка зустрічає пласку модель таблиці в RTF. Кожен із цих фактів варто вписати в контракт між вашим експортером і тим, що його споживає, бо споживач не вичитає ваших намірів із байтів. Повний перелік методів обох фасадів книги містить сторінка продукту HotXLS Delphi Component