Artykuł techniczny

Szybkie listowanie arkuszy w Delphi z HotXLS GetSheetNames

Czasem jedyne pytanie, na które procedura przyjmująca potrzebuje odpowiedzi, jest strukturalne: czy ten skoroszyt ma arkusz o nazwie "Mapping" albo ile zakładek niesie. Odpowiadanie na nie przez wywołanie Open jest drogim sposobem. Pełne otwarcie rozdmuchuje tablicę ciągów współdzielonych, dekoduje każdy rekord stylu i przechodzi po komórkach każdego arkusza, bo nie ma jak wiedzieć, że chciałeś tylko spisu treści. Na dużym pliku to setki megabajtów alokacji i kilka sekund procesora wydanych na odczyt listy zajmującej kilka kilobajtów. HotXLS, natywna biblioteka arkuszowa dla Delphi od losLab, daje ci tę listę samą w sobie: GetSheetNames oddaje nazwy arkuszy, w kolejności skoroszytu, bez materializowania ani jednej komórki

Dlaczego katalog jest tani w odczycie

Oba formaty arkuszowe umieszczają swój spis treści blisko początku i to właśnie czyni wywołanie listujące szybkim, a nie sprytnym. Pakiet OOXML trzyma katalog arkuszy w xl/workbook.xml, części, która pozostaje mała niezależnie od tego, czy skoroszyt trzyma dziesięć wierszy, czy dziesięć milionów. Plik BIFF8 .xls przechowuje swoje rekordy BoundSheet na początku strumienia globaliów skoroszytu, przed jakimikolwiek danymi komórek. Więc praca, której unika wywołanie listujące, nie jest błędem zaokrąglenia wobec pełnego otwarcia. To większość pliku. Odczyt katalogu kosztuje tę samą garść kilobajtów niezależnie od liczby wierszy, podczas gdy pełne otwarcie skaluje się z danymi, a na wielomegabajtowym skoroszycie ta przepaść sięga kilku rzędów wielkości zarówno w dotkniętych bajtach, jak i w zaalokowanej pamięci

HotXLS GetSheetNames w Delphi czytający tylko katalog arkuszy pliku XLSX albo XLS, podczas gdy pełne otwarcie przechodzi po każdej komórce
Katalog siedzi w workbook.xml albo w rekordach BoundSheet, więc listowanie kosztuje kilka kilobajtów, a pełne otwarcie skaluje się z danymi

Ten płaski koszt jest właściwością wartą wzięcia za podstawę projektu. Bramka przyjmująca zbudowana na GetSheetNames zachowuje się tak samo na pliku o 200 wierszach i na tym o 200 MB, więc najwolniejszy plik w partii nie narzuca już tempa decyzji o tym, czy plik w ogóle warto przetwarzać

Jedno wywołanie dla .xls, .xlsx i formatów szablonów

Na fasadzie XLS TXLSWorkbook.GetSheetNames czyta więcej niż .xls. Przyjmuje też oparte na zipie .xlsx, .xlsm, .xltx i .xltm, wyciągając z archiwum wyłącznie workbook.xml. Dla prawdziwego wejścia .xls skanuje rekordy BoundSheet i zatrzymuje się na pierwszym rekordzie EOF podstrumienia globaliów, więc duży plik binarny nadal kosztuje tylko swoje początkowe kilobajty. Fasada XLSX niesie gwarancję, która dla długo działającego kodu usługowego znaczy więcej, niż na początku wygląda: TXLSXWorkbook.GetSheetNames zostawia instancję skoroszytu ani zresetowaną, ani wypełnioną, więc instancja trzymająca już otwarty dokument może sondować inne pliki, nie zaburzając tego, który ma w ręku. GetODSSheetNames stosuje to samo podejście do pakietów OpenDocument, a każde z tych wywołań ma przeciążenie strumieniowe, co pozwala obejrzeć wysyłkę, która nigdy nie ląduje na dysku

var
  Book: TXLSXWorkbook;
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Book := TXLSXWorkbook.Create;
  try
    if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
      raise Exception.Create('unreadable workbook package');
    if Names.IndexOf('Mapping') < 0 then
      raise Exception.Create('required Mapping sheet is missing');
    for I := 0 to Names.Count - 1 do
      Writeln(Format('sheet %d: %s', [I, Names[I]]));
  finally
    Book.Free;
    Names.Free;
  end;
end;

To samo wywołanie robi dobre okno importu na desktopie. Wypisz arkusze, pozwól użytkownikowi wybrać jeden i zapłać za pełne otwarcie dopiero po dokonaniu wyboru. Przy pięćdziesięcioarkuszowym skoroszycie różnica jest widoczna: selektor, który pojawia się od razu, kontra taki, który zwleka, podczas gdy za nim wczytuje się cały plik

Pliki .xlsm z obsługą makr i formaty szablonów listują się dokładnie jak zwykły .xlsx, skoro katalog siedzi w tym samym workbook.xml niezależnie od tego, czy w pakiecie jedzie vbaProject.bin. Potok przyjmujący może więc wyliczyć arkusze skoroszytu z makrami na potrzeby routingu, nigdy nie dotykając ładunku makr i nigdy nie robiąc niczego, co by je uruchomiło, a decyzję o polityce wobec makr zostawić etapowi, który naprawdę otwiera plik

Odczyt wartości zwracanej bez oszukiwania samego siebie

Konwencje zwracania nie są w HotXLS jednolite. Niektóre wywołania zwracają 1 przy powodzeniu, inne zwracają liczność, więc dla funkcji listujących jedynym sprawdzeniem, które się broni, jest traktowanie każdej wartości równej zeru lub mniejszej jako porażki, z wyczyszczoną listą ciągów. Oprzyj się pokusie czytania pustej listy jako "skoroszytu bez arkuszy". Zarówno ECMA-376, jak i specyfikacja BIFF8 wymagają co najmniej jednego arkusza w poprawnym skoroszycie, więc zero nazw zawsze znaczy, że odczyt się nie powiódł, nigdy że plik jest zgodnie z prawem pusty

Nieudane listowanie samo w sobie jest sygnałem wartym zachowania. Plik .xlsx, który oblewa to wywołanie, jest jedną z kilku konkretnych rzeczy: obcięty, wcale nie jest pakietem OOXML (nieprawidłowo oznaczone eksporty CSV z innych systemów pojawiają się tu bez przerwy) albo jest zaszyfrowanym kontenerem. Odróżnianie ich to zadanie następnej kontroli. Zalogowanie pierwszych bajtów odrzuconego pliku obok informacji o porażce zwykle zamienia wątek wsparcia w jedną wiadomość

Wykrywanie zaszyfrowanych kontenerów przed routingiem

Zaszyfrowany .xlsx nie jest zipem. To złożony plik OLE opakowujący strumienie EncryptionInfo i EncryptedPackage, więc GetSheetNames nie widzi do środka i zwraca porażkę jak przy każdym innym nieczytelnym pliku. CanReadEncrypted testuje ten kształt kontenera, co pozwala przyjmowaniu skierować zaszyfrowany plik świadomie, zamiast łykać ogólny błąd odczytu skądś z głębi procesu roboczego:

Przepływ selekcji przyjmowania w Delphi z HotXLS, gdzie CanReadEncrypted i GetSheetNames kierują wysyłki do potrzeba-hasła, nieczytelne albo normalne
CanReadEncrypted biegnie pierwsze, bo zaszyfrowany plik OOXML jest kontenerem OLE, do którego wywołania listujące nie potrafią zajrzeć
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // Zaszyfrowany OOXML to kontener OLE, a nie zip: sprawdź najpierw,
    // bo wywołania listujące nie zajrzą do jego wnętrza.
    if Book.CanReadEncrypted(FileName) then
      Exit(irNeedsPassword);
    if SameText(ExtractFileExt(FileName), '.ods') then
    begin
      if Book.GetODSSheetNames(FileName, Names) <= 0 then
        Exit(irUnreadable);
    end
    else if Book.GetSheetNames(FileName, Names) <= 0 then
      Exit(irUnreadable);
    Result := irNormal;
  finally
    Book.Free;
  end;
end;

Szyfrowanie to obszar, w którym HotXLS jest celowo asymetryczny, więc routing musi to respektować. Starsze szyfrowanie .xls (RC4, RC4 CryptoAPI, XOR) jest czytelne: TXLSWorkbook.Open(FileName, Password) odszyfrowuje zapisanym hasłem, a takie pliki mogą zostać na ścieżce automatycznej. Zaszyfrowane pakiety OOXML idą w drugą stronę. HotXLS potrafi taki zapisać przez SaveAsEncrypted, ale nie potrafi odczytać go z powrotem. OpenEncrypted podnosi EXlsxEncryptionNotImplemented, gdy poda mu się zaszyfrowany pakiet, i dlatego uczciwy projekt przyjmowania wysyła zaszyfrowany .xlsx do człowieka z Excelem, a .xls niosący hasło zostawia w kodzie

Przy pracy wsadowej ten klasyfikator zarabia na swoje miejsce, biegnąc po całym katalogu przychodzącym, zanim jakikolwiek proces roboczy zacznie prawdziwe przetwarzanie, bo każda sonda kosztuje mniej więcej jedno otwarcie pliku i kilka kilobajtów odczytów. Wysunięcie go na przód zmienia tryb awarii, na którym operacjom naprawdę zależy. Zamiast zadania umierającego o 3 w nocy na pliku 412 z 600 dostajesz 412 plików w kolejce i 5 odrzuconych na przyjęciu z powodem doczepionym do każdego. Te same wywołania biblioteki, dużo lepsza opowieść operacyjna

Pytania, na które wywołanie listujące nie odpowie

Nazwy i kolejność to wszystko, co dostajesz. Wywołania listujące nie mówią nic o widoczności, więc arkusze ukryte i bardzo ukryte przychodzą na liście, wyglądając jak każde inne. Nie raportują wymiarów użytego zakresu, liczności komórek ani właściwości dokumentu. Część docProps/core.xml też jest mała, ale dziś nie ma sondy wyłącznie po właściwości, więc metadane autora i tytułu nadal kosztują pełne Open. Czystym sposobem na życie z tym jest pozwolić tanim faktom kierować każdym plikiem i zarezerwować te drogie dla plików, które przetrwają routing. Dla plików, które faktycznie idą w głęboki odczyt, skan tylko do odczytu dużego .xls biegnie zauważalnie szybciej z _DisableGraphics := True, które pomija parsowanie OfficeArt. Tylko nigdy nie zapisuj z takiej instancji: pominięta warstwa rysunkowa zniknęła z modelu, a zapis usunąłby ją z pliku

Pliki, które przechodzą selekcję, zwykle kierują się do głębszej analizy. Warsztat audytu i konwersji skoroszytów omawia liczniki na arkusz warte zbierania, gdy pełne otwarcie jest już uzasadnione, a przewodnik po wydajności dużych skoroszytów omawia utrzymanie tego pełnego otwarcia szybkim

HotXLS to natywna biblioteka arkuszowa w Object Pascalu dla Delphi i C++Buildera; kompletna powierzchnia API, wraz z pokazanymi tu wywołaniami inspekcyjnymi, jest udokumentowana na stronie produktu HotXLS Delphi Component