Artykuł techniczny

Generowanie plików Excela w Delphi bez automatyzacji Office

Jeśli jedynym zadaniem serwera jest emitowanie plików Excela, nie ma on żadnego interesu w uruchamianiu Excela. Instalowanie Office na agencie kompilacji albo w usłudze raportowej po to, by sterować nim przez automatyzację COM, jest złym projektem i jest złym projektem od tak dawna, jak długo ta praktyka istnieje. Mówi to sam Microsoft, w wytycznych, które nie złagodniały przez dwadzieścia lat: Office nie jest ani zbudowany, ani licencjonowany do automatyzacji z bezobsługowego procesu po stronie serwera. Właściwą odpowiedzią jest zapisywanie bajtów BIFF i OOXML wprost, bez Excela na obrazku w ogóle. To cała przesłanka HotXLS, natywnej biblioteki Object Pascal, która sama czyta i zapisuje formaty arkuszowe, więc nie ma aplikacji desktopowej, która mogłaby się zawiesić, przeciekać albo kosztować za stanowisko

Dlaczego sterowanie EXCEL.EXE z usługi zawodzi

Automatyzacja COM zdalnie steruje programem desktopowym, a program desktopowy po cichu zakłada trzy rzeczy, których usługa Windows nie może mu podać: wczytany profil użytkownika, interaktywną stację okien i człowieka patrzącego na ekran. Odbierz to, a awarie przychodzą w postaci, której żadna maszyna deweloperska nigdy nie odtwarza. Monit odzyskiwania pliku, błąd dodatku albo okno aktywacji licencji otwiera się na pulpicie, którego nikt nie widzi, a wywołanie automatyzacji, które je wyzwoliło, nigdy nie wraca. Wołający w końcu przekracza limit czasu i umiera; instancja Excela często nie, przeżywając jako sierota, która trzyma blokady plików i zatruwa następny przebieg. Każdy, kto patrzył, jak jedenaście zbłąkanych procesów EXCEL.EXE piętrzy się na koncie usługi, zna resztę tej opowieści

Diagram zestawiający usługę Delphi sterującą EXCEL.EXE przez automatyzację COM, gdzie ukryte okna dialogowe i osierocone procesy blokują wywołania, z HotXLS zapisującym bajty skoroszytu BIFF8 i OOXML wprost w procesie
Automatyzacja COM dziedziczy brakujące założenia programu desktopowego, podczas gdy HotXLS zapisuje bajty BIFF8 i OOXML wprost, bez niczego do zainstalowania na serwerze

Opowieść o skalowaniu nie jest lepsza nawet wtedy, gdy nic się nie wywala. Instancja Excela to potok na jeden skoroszyt, każdy dostęp do właściwości płaci koszt międzyprocesowego marshalingu COM, a maszyna uruchamiająca ten kod niesie licencję Office, której warunki wykluczają dokładnie to zastosowanie. Większość zespołów spotyka te granice po jednej awarii naraz, i mniej więcej tak "wycofanie warstwy COM" ląduje w planie prac

Zanim ten przepis się zacznie, rozstrzygnij jedno pytanie o zakres, bo ono decyduje, ile z tej pracy jest realne. Kod COM prawie nigdy nie ustawia tylko wartości komórek. Woła Workbook.SaveAs ze stałymi formatu, wymusza przeliczenie, wypycha ustawienia wydruku, czasem sięga po schowek. Przejdź stary kod i spisz, które z tych zachowań naprawdę trafiają do wyjścia, bo każde ląduje w innym rogu natywnej biblioteki, a kilka z nich (interop ze schowkiem jest tu oczywisty) nie ma po stronie serwera żadnego sensu i powinno zostać porzucone, a nie przeniesione

Dwa natywne silniki, dwa modele własności

HotXLS wymienia proces Excela na dwie bezpośrednie implementacje formatów. Silnik strumienia rekordów BIFF8 (TXLSWorkbook, jednostka lxHandle) obsługuje .xls. Zapisujący pakiety OOXML (TXLSXWorkbook, jednostka lxHandleX) produkuje .xlsx zgodne z ECMA-376 / ISO/IEC 29500. Nie ma nic do zarejestrowania ani nic do zainstalowania na serwerze, a otwartych skoroszytów naraz możesz trzymać tyle, ile pozwoli pamięć

Diagram porównujący dwie fasady HotXLS w Delphi: TXLSWorkbook zwalniany automatycznie przez zliczanie referencji interfejsu IXLSWorkbook i TXLSXWorkbook jako zwykły obiekt wymagający jawnego Free w bloku try..finally
Fasada XLS jest zwalniana przez zliczanie referencji interfejsu, a fasada XLSX potrzebuje jawnego Free, przy czym kolekcje arkuszy różnią się między Entries liczonym od jedynki a Items liczonym od zera

To, co wcześnie podkłada ludziom nogę, to fakt, że obie fasady inaczej władają swoją pamięcią, a różnica milczy aż do awarii:

var
  Book: IXLSWorkbook;          // referencja interfejsowa: zwalniana automatycznie
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // zwykły obiekt: zwalniasz go sam
  SheetX: TXLSXWorksheet;
begin
  // wyjście BIFF8 .xls - bez Free; włada nim licznik referencji interfejsu
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Report';
  Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
  Book.SaveAs('report.xls');

  // wyjście OOXML .xlsx - jawny czas życia
  BookX := TXLSXWorkbook.Create;
  try
    SheetX := BookX.Sheets.Add('Report');
    SheetX.Cells[1, 1].Value := 'Generated without Excel';
    BookX.SaveAs('report.xlsx');
  finally
    BookX.Free;
  end;
end;

Fasada XLS jest zliczana referencyjnie przez interfejs IXLSWorkbook. Zadeklaruj zmienną jako typ interfejsowy i nigdy nie wołaj na niej Free; przetrzymaj ten sam obiekt w zwykłej zmiennej obiektowej i zwolnij go sam, a licznik referencji zwolni go po raz drugi. Fasada XLSX to zwyczajny obiekt, który chce zwyczajnego try..finally. Adresowanie komórek liczy się od jedynki po obu stronach, i to jedyne miejsce, w którym obie się zgadzają. Kolekcje arkuszy już nie: Entries po stronie XLS liczy od jedynki, indekser Items w XLSX liczy od zera, a ten błąd o jeden kompiluje się czysto, którąkolwiek stronę pomylisz, i pokazuje się dopiero w czasie działania

Zapis skoroszytu prosto do odpowiedzi HTTP

Eksport po stronie serwera zwykle nie ma powodu, by dotykać dysku. Pliki tymczasowe wymagają polityki sprzątania, kolidują przy równoczesnych żądaniach i zostawiają dane klientów na wolumenach, których nikt nie pomyślał audytować. Obie fasady biorą TStream przez swoje przeciążenia SaveAs, więc skoroszyt może pójść prosto do odpowiedzi:

Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // zapisuje od BIEŻĄCEJ pozycji strumienia
  Mem.Position := 0;         // przewiń przed przekazaniem strumienia
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // od teraz Mem należy do szkieletu
finally
  Book.Free;
end;

Przewinięcie to linia, która zarabia na swój komentarz. SaveAs(Stream) zapisuje od bieżącej pozycji strumienia i nigdy potem nie wraca do zera. Zapomnij o Mem.Position := 0, a klient dostanie pobranie o zerowej długości albo Excel nazwie plik uszkodzonym. To najczęstszy błąd w kodzie skoroszytowym zwróconym ku sieci i najokrutniejszy, bo przemyka obok każdego testu jednostkowego, który sprawdza tylko, że strumień ma niezerową długość

Jedna procedura budująca skoroszyt sięga do każdego innego formatu dostawy bez restrukturyzacji. SaveAsCSV odpowiada na prośbę "daj mi po prostu surowe dane", SaveAsHTML obsługuje "wrzuć to na stronę portalu", SaveAsRTF zasila potoki dokumentowe, a SaveAsODS pokrywa nakaz OpenDocument, wszystkie z przeciążeniami plikowymi i strumieniowymi. Jedna procedura eksportu plus parametr formatu zastępuje to, co bywało czterema osobnymi makrami COM. TXLSXHtmlExportOptions eksportera HTML niesie tytuł, klasę CSS i przełącznik fragment-albo-pełny-dokument, co trzyma przypadek portalu z dala od edytowania wyeksportowanego znacznika wyrażeniami regularnymi

Diagram obsługi żądania w Delphi zapisującej skoroszyt HotXLS do TMemoryStream, przewijającej Mem.Position do zera i podającej strumień odpowiedzi HTTP, z eksporterami CSV, HTML, RTF i ODS obok
Zapis do TMemoryStream i przewinięcie go przed przekazaniem wysyła bajty skoroszytu prosto do klienta, a jedna procedura eksportu pokrywa zapisujące CSV, HTML, RTF i ODS

Wartości formuł bez procesu Excela, który by je policzył

Pod automatyzacją COM Excel przeliczał wszystko za darmo, a porzucenie COM po cichu to cofa. SaveAs przechowuje formuły jako tekst, nie wyliczając ich; liczby pojawiają się dopiero, gdy Excel otworzy plik i przeliczy, a fasada XLS pozwala nastroić to zachowanie przez RecalcOnSave i CalculationMode. Dla pliku idącego do człowieka to dokładnie właściwe. Jest złe dla usługi, która musi potwierdzić sumę, zanim ją wyśle, i złe dla eksportu CSV, który zapisuje tekst formuły zamiast jej wyniku. Każdy z tych przypadków musi wyliczyć na serwerze wbudowanym silnikiem:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // fasada XLSX: bez prefiksu '='
Total := BookX.Calculate('SUM(A1:A2)');       // wylicz na serwerze, teraz
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

Konwencja fasad gryzie tu ponownie. Strona XLSX przypisuje wyrażenia przez Cell.Formula bez znaku równości; strona XLS zapisuje je przez Cell.Value z wiodącym '='. Przenieś kod z jednej do drugiej bez zmian, a niewłaściwa konwencja zapisze ciąg tekstowy jedynie przypominający formułę, bez żadnego błędu, który by to zgłosił. Kiedy formuły skoroszytu muszą sięgnąć w twoją własną logikę biznesową, wywołanie zwrotne OnUserFunction pozwala silnikowi oddać nieznane nazwy funkcji kodowi Delphi w czasie wyliczania. To natywny zamiennik dodatków UDF, które lubią chować się w tych właśnie arkuszach, wokół których wyrósł system z automatyzacją COM

Krawędzie wdrożeniowe, które wychodzą dopiero na serwerze

O tym, czy wdrożenie będzie czyste czy zagadkowe, decyduje kilka szczegółów, a pierwszym z nich jest graf jednostek. Eksporter zbiorów danych typu przeciągnij i upuść, TDataToXLS, wciąga z VCL Forms, Controls i Dialogs. Nieszkodliwe w narzędziu desktopowym; w usłudze konsolowej ciągnie za sobą całe VCL. Rdzeniowe jednostki lxHandle i lxHandleX sięgają tylko po Windows, Classes, SysUtils i Variants, więc czystej usłudze lepiej wyjdzie napisanie własnej pętli po zbiorze danych na rdzeniowym API niż zaimportowanie komponentu dla wygody

Dalej jest wielowątkowość. Instancje skoroszytu nie są bezpieczne wątkowo, ale nie dzielą też żadnego stanu globalnego, więc wzorcem, który się skaluje, jest ten najprostszy: jeden obiekt skoroszytu na zadanie albo na wątek roboczy. To kupuje równoległe generowanie raportów, czego pojedyncza współdzielona instancja Excela nigdy nie potrafi. Obsługa żądania, która tworzy, wypełnia, zapisuje i zwalnia własny skoroszyt, nie potrzebuje żadnych blokad, a promień rażenia awarii kurczy się z "współdzielona instancja Excela zaklinowała się dla wszystkich" do "to jedno żądanie podniosło wyjątek", z czym twoja istniejąca obsługa błędów już wie, co zrobić

Celowanie w format jest ostatnim z nich. TXLSWorkbook.SaveAs zapisuje domyślnie BIFF (xlExcel97), a wpychanie treści XLS do .xlsx idzie przez pomost SaveXLSWorkbookAsXLSX o obniżonej wierności. Wybieraj fasadę według formatu, który zamierzasz wysyłać, na etapie projektowania, zamiast budować w jednym i konwertować na końcu potoku

Dla tej połowy typowego projektu wymiany, która dotyczy wczytywania danych, wzorce eksportu z bazy danych do skoroszytu omawiają zarówno komponent, jak i ręcznie pisaną pętlę, a gdy liczby wierszy sięgną sześciu cyfr, techniki wydajnościowe dla dużych skoroszytów stają się różnicą między minutami a sekundami. Raporty budowane z układów utrzymywanych przez projektanta omawia przewodnik po generowaniu raportów z szablonów

HotXLS jest dostarczany jako kod źródłowy w Object Pascalu dla Delphi i C++Buildera; edycje, licencjonowanie i pełna referencja API są na stronie produktu HotXLS Delphi Component