Odborný článok

Exportovanie zošitov Excelu do CSV, TSV, HTML a RTF z Delphi cez HotXLS

Predstavte si nočnú dávkovú úlohu, ktorá v kóde vygeneruje zošit s faktúrami a zapíše ho ako CSV pre nadväzujúci systém. Čísla vyzerajú v Exceli správne. CSV sa v textovom editore otvorí bez problémov. Potom sa však importér zasekne na stĺpci s celkovými súčtami, pretože pole sumy pre riadok 42 obsahuje hodnotu =SUM(D2:D41), čiže text vzorca, a nie číslo, ktoré by sa malo vypočítať. Nič nie je pokazené. Ide o zdokumentované správanie a je to prvá vec, ktorú treba pochopiť o exportovaní z HotXLS: zapisovač serializuje model bunky presne tak, ako existuje, a bunka so vzorcom, ktorej hodnota nebola nikdy vypočítaná, má k dispozícii iba text vzorca

Prečo vaše CSV obsahuje vzorce namiesto čísel

HotXLS ukladá text vzorca a vypočítanú hodnotu ako dve samostatné veci. Metóda SaveAsCSV zámerne nespúšťa výpočtový engine pri exporte: export by nemal meniť zošit a nemal by riskovať zaseknutie na zložitej reťazi vzorcov. Súbory, ktoré uložil samotný Excel, nesú vedľa vzorcov nacacheované výsledky, takže ich opätovný export sa správa podľa očakávania. Pasca sa týka konkrétne zošitov vygenerovaných vaším vlastným kódom, kde boli vzorce zapísané, ale nikdy sa nevyhodnotili. Riešením je zabezpečiť existenciu hodnôt pred exportom pomocou rovnakého enginu Calculate, ktorý rieši odkazy medzi hárkami a vlastné funkcie:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('invoice-run.xlsx');
    Sheet := Book.Sheets[0];

    // Materialize formula results so the CSV carries numbers, not '=...' text
    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, ',');    // sheet 0, comma
    Book.SaveAsCSV('feed.tsv', 0, #9);     // same sheet as TSV
  finally
    Book.Free;
  end;
end;

Sledujte, čo tento cyklus v skutočnosti robí: prepisuje bunky so vzorcami ich vypočítanými hodnotami. To je úplne v poriadku pre jednorazový export, ale nesprávne, ak chcete zošit neskôr znova uložiť ako .xlsx, pretože ste práve nahradili živé vzorce statickými číslami. Exportujte z kópie zošita alebo obmedzte zápis tak, aby sa dotkol iba exportnej fázy. Engine na pozadí metódy Calculate dokáže viac, vrátane registrácie vašich vlastných funkcií, čo je témou článku o engine vzorcov HotXLS a vlastných funkciách

Čo garantuje zapisovač s oddelenými hodnotami

Export do CSV produkuje kódovanie UTF-8 so značkou poradia bajtov BOM, koncami riadkov CRLF a úvodzovkami podľa štandardu RFC 4180. Akékoľvek pole obsahujúce oddeľovač, úvodzovku alebo zlom riadka je zabelené do úvodzoviek a vnútorné úvodzovky sú zdvojené. Dátumy sa zobrazujú vo formáte yyyy-mm-dd hh:nn:ss bez ohľadu na formát zobrazenia bunky. To je správne rozhodnutie pre strojové spracovanie, hoci to môže prekvapiť každého, kto očakával prenos formátovania z obrazovky. Bunky s formátovaným textom (rich text) sa sploštia spojením ich jednotlivých častí

Tieto predvolené hodnoty vyriešia väčšinu sporov s importérom skôr, než vôbec začnú, no dve z nich by predsa mali byť súčasťou vašich integračných pravidiel. Prvou je značka BOM. Práve tá umožňuje Excelu otvoriť súbor so zachovaním diakritiky, no niektoré prísne parsery považujú tieto tri bajty za dáta; ak je to váš prípad, pred odovzdaním ich odstráňte. Druhou je formát TSV. Nejde o samostatnú funkciu, ale o ten istý zapisovač volaný s oddeľovačom #9 (tabulátor), takže všetko vyššie uvedené platí bez zmien aj preň. Hárok na export sa vyberá pomocou indexu začínajúceho od 0 v preťaženej metóde s viacerými argumentmi, zatiaľ čo skrátené volanie SaveAsCSV(FileName) s jedným argumentom exportuje aktívny hárok

HTML export je snímka, nie výmenný formát

Zatiaľ čo CSV zahadzuje všetko okrem hodnôt, metóda SaveAsHTML sa snaží zachovať vzhľad: jedna tabuľka <table> na každý hárok, zlúčené oblasti vyjadrené pomocou colspan a rowspan a základné štýlovanie inlinované ako CSS. Farby závislé od motívu (themes) sa skôr preskakujú, než prepočítavajú, takže šablóna využívajúca farebné motívy vyzerá po exporte jednoduchšie ako v Exceli. Na všetko, čo musí prežiť export, nastavte explicitné RGB farby. Objekt volieb riadi výsledný formát:

var
  Opts: TXLSXHtmlExportOptions;
begin
  Opts := TXLSXHtmlExportOptions.Create;
  try
    Opts.Title := 'Weekly settlement';
    Opts.TableClass := 'report-grid';     // hook for the host page stylesheet
    Opts.WriteDocument := True;           // full page, not a fragment
    if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
      raise Exception.Create('Sheet index out of range');
  finally
    Opts.Free;
  end;
end;

Dva detaily v tomto fragmente kódu si zaslúžia pozornosť. Nastavte vlastnosť WriteDocument na False a výstupom bude iba čistý fragment tabuľky namiesto celej stránky. To je presne to, čo chcete pri vkladaní náhľadu do existujúceho rozvrhu stránky: nastavte vlastnosť TableClass a nechajte štýlovanie na CSS šablónu hostiteľskej stránky. Návratová hodnota je tiež opačná ako pri väčšine volaní v HotXLS. SaveAsHTML vracia 0 pri úspechu a -1 pri nesprávnom indexe hárka, takže kontrola na hodnotu 1 zo zvyku nahlási každý úspešný export ako zlyhanie. Keď musíte exportovať iba oblasť namiesto celého hárka (napríklad na odoslanie e-mailom alebo vloženie bloku), metóda TXLSXRange.SaveAsHTML exportuje akýkoľvek obdĺžnikový rozsah podľa rovnakých pravidiel vykresľovania

RTF výstup a kde si stále nachádza svoje miesto

Štvrtý cieľ zapisuje tabuľky formátu RTF 1.6, jeden hárok na jedno volanie cez SaveAsRTF. Šírka stĺpcov sa odhaduje približne na 96 twipov na jeden znak šírky stĺpca. Štrukturálnym obmedzením, o ktorom musíte vedieť, je, že zlúčené bunky sa vo výstupe nerozširujú: iba hlavná (ukotvená) bunka nesie svoj obsah a zakryté bunky sa exportujú ako prázdne. To vylučuje použitie RTF pre šablóny s náročným rozvrhnutím. Stále si však nachádza svoje miesto ako cesta najmenšieho odporu pre vkladanie tabuľkových výsledkov do textových procesorov alebo do starších systémov na správu dokumentov, ktoré nepodporujú HTML

Cyklus prevodu: import CSV je zámerne deštruktívny

Čítanie CSV spätne má svoje vlastné pravidlá. Metóda OpenCSV vymaže celý zošit a znova ho zostaví ako jeden hárok s názvom Sheet1. Ide v podstate o konštruktor, nie o zlúčenie, preto ju nikdy nevolajte na zošite, ktorý stále obsahuje neuložený obsah. Odovzdanie hodnoty #0 ako oddeľovača spustí automatickú detekciu oddeľovača. Príznak ADetectTypes riadi konverziu typov: pri jeho zapnutí sa číselné reťazce zmenia na čísla, reťazce ISO-8601 na dátumy a hodnoty true/false na boolean. Vypnite ho, keď importované dáta obsahujú identifikátory s poprednými nulami, PSČ alebo kódy produktov, pretože automatická konverzia by ich potichu zmenila na čísla (popredná nula zmizne v momente, keď sa z 00123 stane 123). Obe rozhrania ponúkajú rovnaký import. Spojte ho s vyššie uvedenými volaniami exportu a získate premostenie formátov, ktoré nevyžaduje inštaláciu Excelu na serveri, čo je scenár popísaný v článku o generovaní správ z databázy do Excelu s HotXLS

Exportovanie priamo do streamu

Každý tu spomenutý zapisovač má popri verzii so súbormi k dispozícii aj preťaženie so streamom: pre CSV, HTML, RTF aj samotné formáty zošitov. V serverovom kóde by ste mali siahnuť práve po týchto preťaženiach. Webové rozhranie, ktoré poskytuje sťahovanie CSV, môže zapisovať do TMemoryStream a odovzdať ho priamo objektu odpovede (response). Nevyžaduje sa žiadny dočasný súbor, žiadne čistenie pamäte a nedochádza k žiadnemu konfliktu medzi požiadavkami, ktoré by si náhodne vybrali rovnaký názov súboru. To isté platí pre ukladanie exportov do cloudových úložísk (blob storage) alebo ich pripájanie k odchádzajúcim e-mailom. Súborový systém tak úplne vypadáva z hry

Tento vzor sa navyše spája so spôsobom nasadenia knižnice. Obe rozhrania sú natívnymi čítačkami a zapisovačmi v Object Pascale, takže netreba inštalovať Excel, nepoužíva sa COM automatizácia a na serveri nevzniká úzke hrdlo pri serializácii požiadaviek na úrovni procesov. Každá požiadavka môže vlastniť svoj objekt zošita, spustiť výpočet hodnôt podľa prvej sekcie a streamovať svoj export paralelne s ostatnými. Jediným zdrojom, na ktorý si treba dávať pozor, je pamäť. Model zošita zostáva v pamäti RAM počas celého exportu, takže služba, ktorá otvára veľmi veľké súbory len preto, aby ich exportovala do CSV, by mala obmedziť počet súbežných úloh alebo radiť nadmerné požiadavky do frontu, namiesto toho, aby o využití pamäte rozhodovala nárazová prevádzka

Menší detail: nastavte vlastnosť IncludeBOM vo voľbách HTML exportu, ak sa fragment bude ukladať ako samostatný súbor, pri ktorom nejaký nadväzujúci nástroj zisťuje kódovanie. Keď poskytujete HTML priamo cez HTTP, nechajte deklaráciu znakovej sady radšej na hlavičky odpovede

Keď bajty stále vychádzajú nesprávne

Najčastejšia otázka podpory ohľadom exportu do CSV je v podstate ten istý problém s otváraním v inom šate: Excel zobrazuje skomolené znaky (mojibake) namiesto diakritiky. Inštinkt velí viniť zapisovač, ale ten vygeneruje UTF-8 BOM presne z tohto dôvodu a súbor je pri opustení vášho kódu takmer vždy správny. BOM však mohol zmiznúť niekde na ceste do Excelu. Prenos cez FTP v textovom režime, kopírovanie streamu, ktoré preskočí prvé tri bajty, alebo proxy server, ktorý prekoduje dáta počas prenosu: čokoľvek z toho môže odstrániť značku a nechať Excel hádať kódovanie, čo robí veľmi zle. Diagnostikujte to na rozhraní, nie priamo vo volaní exportu. Otvorte doručený súbor v hexadecimálnom prehliadači a overte si, či sú bajty EF BB BF stále na samom začiatku

To je spoločná črta pre všetky štyri formáty. Samotné volanie exportu je jednoduchá časť a HotXLS robí rozumné rozhodnutia pri každom kroku zápisu. Zlyhania nastávajú na prechodoch: kde sa text vzorca stretáva s parserom očakávajúcim číslo, kde sa značka BOM stretáva s prenosom, ktorý ju nezachová, a kde sa zlúčená bunka stretáva s plochým modelom tabuľky v RTF. Každá z týchto skutočností by mala byť zakotvená v integračných pravidlách medzi vaším exportérom a systémom, ktorý dáta spracováva, pretože prijímač nedokáže vyčítať vaše zámery priamo z bajtov. Pre kompletný zoznam metód pre obe rozhrania zošitov nájdete referenciu na produktovej stránke HotXLS Component