Artykuł techniczny

Ładowanie XLSX tylko metadanych i selektywne z HotXLS

Pytanie „czy ten skoroszyt nadal definiuje nazwę TaxRate?" odpowiada mały kawałek pakietu XLSX. Nazwy arkuszy, ich stany ukryte albo bardzo ukryte i wszystkie nazwy zdefiniowane na poziomie skoroszytu mieszkają w xl/workbook.xml (ECMA-376 Part 1, §18.2), zwykle w kilku kilobajtach. Komórki mieszkają gdzie indziej — jedna część xl/worksheets/sheetN.xml na arkusz plus tabela współdzielonych łańcuchów — i tam jest prawie cała masa bajtów. Loader upierający się przy zbudowaniu kompletnego skoroszytu, zanim wolno cokolwiek zapytać, rozepchnie i sparsuje wszystko, dla każdego pliku w folderze, by odpowiedzieć na pytanie, które pierwsze kilka kilobajtów już rozstrzygnęło

Podział, który się liczy, biegnie między czytaniem kształtu skoroszytu a czytaniem jego ciężaru. Loader potrafiący stanąć po kształcie, wczytać ciężar dla wybranego podzbioru arkuszy i dociągnąć jeszcze jeden arkusz później zamienia „otwórz skoroszyt" z kroku wszystko albo nic w coś, co określasz per wywołanie. Przynosi też dwie pułapki — jedną o podstawach indeksowania i drugą o tym, jak długo plik źródłowy musi leżeć spokojnie

Co czyta Open tylko-metadane i gdzie się zatrzymuje

Przy TXLSXWorkbook.MetadataOnly ustawionym na True przed Open HotXLS parsuje część skoroszytu, style, motyw, nazwy zdefiniowane, nazwy arkuszy i stany widoczności, połączenia, linki zewnętrzne oraz właściwości dokumentu z docProps/core.xml i docProps/app.xml. Potem pomija XML komórek każdego arkusza i satelickie części per arkusz: komentarze, rysunki i tabele. Tabela współdzielonych łańcuchów też jest pomijana — co znaczy więcej, niż brzmi, bo w skoroszytach pełnych tekstu ta jedna część potrafi dorównać rozmiarem arkuszom

Skoroszyt, który wraca, jest strukturalnie kompletny. Sheets.Count jest prawidłowy, każdy arkusz ma swoją prawdziwą nazwę i Visibility, DefinedNames jest zaludniony, ExternalLinks wypisuje każdy cel linku, a Title, Author i LastModifiedBy niosą to, co plik zadeklarował. Każdy arkusz ma po prostu zero komórek

HotXLS MetadataOnly czyta właściwości skoroszytu, nazwy arkuszy, style i łącza w archiwum XLSX Delphi, potem zatrzymuje się przed jakimkolwiek XML komórek arkusza
MetadataOnly czyta każdą strukturalną część archiwum i zatrzymuje się przed XML komórek arkusza. Skoroszyt Delphi wraca z nazwami arkuszy, widocznością, nazwami zdefiniowanymi i stylami — i zerem komórek

To dokładnie dane, na których biegnie audyt. Które skoroszyty ukrywają arkusz? Które nadal definiują TaxRate — i co? Które linkują do udziału plikowego wyłączonego w zeszłym roku? Skaner poniżej odpowiada na wszystkie trzy dla jednego pliku. Zauważ dwie konwencje indeksowania obok siebie: Sheets[] jest liczone od 1, w rytm silnika XLS, podczas gdy DefinedNames i ExternalLinks to zwykłe listy liczone od 0

procedure ScanWorkbook(const FileName: WideString; Report: TStrings);
var
  Workbook: TXLSXWorkbook;
  Name: TXLSXDefinedName;
  I: Integer;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Workbook.MetadataOnly := True;           // czytane przez Open, więc ustaw najpierw
    if Workbook.Open(FileName) <> 1 then
    begin
      Report.Add(FileName + ': open failed');
      Exit;
    end;
    for I := 1 to Workbook.Sheets.Count do   // Sheets[] liczone od 1
      case Workbook.Sheets[I].Visibility of
        xlsxSheetHidden:
          Report.Add(FileName + ': hidden sheet ' + Workbook.Sheets[I].Name);
        xlsxSheetVeryHidden:
          Report.Add(FileName + ': very hidden sheet ' + Workbook.Sheets[I].Name);
      end;
    Name := Workbook.DefinedNames.FindByName('TaxRate');
    if Name <> nil then
      Report.Add(FileName + ': TaxRate = ' + Name.Formula);
    for I := 0 to Workbook.ExternalLinks.Count - 1 do   // lista liczona od 0
      Report.Add(FileName + ': links to ' + Workbook.ExternalLinks[I].Target);
  finally
    Workbook.Free;
  end;
end;

Skaner czyta Visibility zamiast Booleana Visible celowo. Visible jest False dla obu stanów ukrycia, a arkusz bardzo ukryty to ten, którego audyt najbardziej chce flagować — bo okno dialogowe Excela Odkryj go nie wypisuje i tylko VBA albo bezpośrednia edycja pliku przyprowadza go z powrotem

Wczytywanie dwóch arkuszy z czterdziestu przez SelectedSheets

SelectedSheets to TStringList należąca do skoroszytu. Gdy trzyma co najmniej jedną nazwę, Open parsuje XML komórek wyłącznie dla arkuszy, które wypisuje; każdy inny arkusz jest tworzony ze swoją poprawną nazwą i widocznością i bez komórek. Pusta lista, stan domyślny, wczytuje wszystkie arkusze. Nazwy są szukane przez własne IndexOf listy, więc przy domyślnych ustawieniach dopasowanie ignoruje wielkość liter — co zgadza się z regułą Excela, że dwie nazwy arkuszy w jednym skoroszycie nie mogą różnić się wyłącznie wielkością liter

Workbook := TXLSXWorkbook.Create;
Workbook.SelectedSheets.Add('Summary');
Workbook.SelectedSheets.Add('Q3 Detail');
if Workbook.Open('consolidated-2026.xlsx') = 1 then
  ShowSheet(Workbook.Sheets[1]);   // ShowSheet to twój własny kod UI

Te dwa przełączniki współgrają tylko w jedną stronę. MetadataOnly = True nadpisuje wybór i pomija wszystkie arkusze, więc skoroszyt skonfigurowany z oboma wraca bez komórek gdziekolwiek, a wybór wygląda na zepsuty, choć został po prostu zdystansowany. Używaj jednego na Open: tylko-metadane do odkrywania, wybór do pracy celowanej

Ładowanie selektywne biegnie też przez fazę parsowania równoległego, gdy ParallelParse jest włączone; niewybrane arkusze są pomijane wewnątrz zadań wątków, więc wątki spędzają czas na arkuszach, o które prosiłeś. Te same dwie właściwości obowiązują, gdy źródłem jest binarny skoroszyt .xlsb, z jedną regułą zakresu: .xlsb niosący projekt VBA musi być otwierany bez tych dwóch opcji; przy którejkolwiek ustawionej Open raportuje odczyt selektywny jako niewspierany

HotXLS SelectedSheets parsuje komórki dla dwóch nazwanych arkuszy z czterdziestu w Delphi, podczas gdy LoadSheet zmaterializowuje pominięty arkusz z wciąż otwartego archiwum
SelectedSheets parsuje XML komórek tylko dla wskazanych arkuszy, zostawiając resztę jako puste kikuty, które zachowują nazwę i widoczność. LoadSheet zmaterializuje kikut później, pod warunkiem że archiwum źródłowe jest wciąż otwarte

Dlaczego LoadSheet(1) wczytuje zły arkusz?

Bo LoadSheet bierze pozycję liczoną od 0, podczas gdy Sheets[] jest liczone od 1. LoadSheet(1) materializuje drugi arkusz skoroszytu, a ponieważ wczytanie arkusza, który już ma komórki, jest no-opem zwracającym True, pomyliowany indeks może wyglądać na sukces, podczas gdy arkusz, w który kliknął użytkownik, zostaje pusty. Konwersja należy w dokładnie jedno miejsce w twoim kodzie

Druga pułapka to żywotność pliku. LoadSheet nie trzyma archiwum otwartego między wywołaniami. Otwiera ponownie oryginalny plik po nazwie, parsuje część skoroszytu, style i współdzielone łańcuchy, po czym parsuje żądany arkusz. Zanim zaufa temu plikowi, HotXLS porównuje jego rozmiar i czas ostatniego zapisu z wartościami przechwyconymi przy Open; jeśli inny proces zapisał skoroszyt po drodze, LoadSheet zwraca False zamiast mieszać komórki z innej wersji do modelu, który już trzymasz

procedure TReportForm.ShowSheetNo(SheetNo: Integer);  // SheetNo liczone od 1, jak Sheets[]
var
  Sheet: TXLSXWorksheet;
begin
  Sheet := FWorkbook.Sheets[SheetNo];
  if Sheet.Cells.Count = 0 then
    if not FWorkbook.LoadSheet(SheetNo - 1) then      // LoadSheet chce pozycji liczonej od 0
    begin
      StatusBar.SimpleText := 'The workbook changed on disk; reopen it';
      Exit;
    end;
  Grid.ShowSheet(Sheet);                              // twój własny kod siatki
end;

Z tego projektu płynie kilka konsekwencji. Skoroszyt otwarty z TStream nie ma pliku źródłowego, do którego można wrócić, więc LoadSheet zwraca False dla .xlsx otwartego ze strumienia; to samo tyczy skoroszytu zaszyfrowanego, który jest odszyfrowywany do pamięci przed parsowaniem. Binarny .xlsb otwierany selektywnie ze strumienia jest wyjątkiem, bo HotXLS odkłada tymczasową kopię strumienia dla odłożonych arkuszy. A arkusz naprawdę pusty będzie czytany od nowa przy każdym wywołaniu, bo zero komórek to też to, jak wygląda arkusz pominięty

Model kosztów rozstrzyga, jak go używać. Każde wywołanie LoadSheet parsuje ponownie tabelę współdzielonych łańcuchów, więc materializowanie dziesięciu arkuszy po kolei płaci za tę część dziesięć razy. Dla przeglądarki, w której użytkownik otwiera jeszcze jedną zakładkę od czasu do czasu, to uczciwa cena. Gdy znasz już zestaw arkuszy, otwórz ponownie z SelectedSheets: jeden przebieg archiwum, jedno parsowanie współdzielonych łańcuchów. Jest i różnica w treści: LoadSheet przynosi komórki, wartości, formuły i formaty arkusza, podczas gdy komentarze, rysunki i tabele przychodzą z Open — więc arkusze, których komentarze albo tabele czyta twój kod, należą do wyboru z góry

Dlaczego podróż w obie strony zmienia części, których nigdy nie tknąłeś?

Zapis, który regeneruje wszystko, przepisuje bajty, które nigdy się nie zmieniły. Rozepchnij motyw, zbuduj model obiektowy, zserializuj go, skompresuj jeszcze raz — a wynik różni się od oryginału, choć nikt nie ruszył motywu. Dla projektów VBA i cache tabel przestawnych koszt jest wyższy niż hałaśliwy diff: projekt VBA to binarny plik złożony z własnymi wewnętrznymi strumieniami, a cache tabeli przestawnej trzyma migawkę danych źródłowych, których pola definicje tabeli przestawnej adresują po pozycji — więc najbezpieczniejsze, co można zrobić z każdym z nich, gdy nikt go nie zmienił, to zostawić jego bajty w spokoju

PreserveRawParts, domyślnie True, każe SaveAs kopiować skompresowane bajty niezmienionych części prosto z archiwum źródłowego. To obejmuje motyw, projekt VBA, połączenia, wykresy, osadzone media, ustawienia drukarki, tabele przestawne oraz definicje i rekordy cache tabel przestawnych — każde tylko dopóki model mówi, że nie było modyfikowane. Te części nigdy nie są rozepchnywane ani ponownie upychane po drodze. Ustawienie właściwości na False wymusza pełną reserializację każdej części — to przełącznik, po który sięgasz, gdy chcesz, by wyjście odzwierciedlało wyłącznie to, co zawiera model obiektowy

Ścieżka surowej kopii używa tej samej kontroli pliku co LoadSheet: źródło musi nadal istnieć z rozmiarem i znacznikiem czasu zobaczonym przy Open. Gdy nie istnieje, SaveAs serializuje te części normalnie i plik nadal jest kompletny. Dwa przypadki zawsze idą ścieżką serializacji: skoroszyt, którego źródłem był binarny .xlsb, konwertowany na XML przy odczycie, oraz zapis z ustawionym StrictOOXML, gdzie przestrzenie nazw części się zmieniają

Zapis częściowo wczytanego skoroszytu wymaga decyzji. Dla źródła .xlsb SaveAs odmawia, dopóki jakikolwiek arkusz jest jeszcze odłożony, i zapisuje „Load every deferred binary worksheet before saving" w Diagnostics. Dla .xlsx traktuj selektywnie wczytany skoroszyt jako widok do czytania: arkusz kikut nie ma komórek do wyemitowania przez zapisującego, więc albo zawołaj LoadSheet dla każdego kikuta najpierw, albo otwórz ponownie bez wyboru, zanim edytujesz i zapiszesz

Potok wykrywania, który otwiera ponownie celowo

Wzorzec, który się broni, ma dwa przebiegi. Przebieg pierwszy otwiera każdy plik tylko-metadane i rozstrzyga. Przebieg drugi otwiera ponownie wyłącznie pliki, które pasowały, z wyborem arkuszy, i robi robotę. Ponowne otwieranie wygląda na marnotrawstwo, dopóki nie policzysz, czego uniknął przebieg pierwszy: drugie Open jest jedynym, które parsuje komórki — i parsuje tylko arkusze wybrane w przebiegu pierwszym

Dwuprzebiegowy potok odkrywania HotXLS w Delphi: przebieg tylko-metadane decyduje, które skoroszyty i arkusze mają znaczenie, potem ukierunkowany przebieg parsuje tylko wybrane arkusze
Przebieg pierwszy skanuje katalog tylko-metadane i rozstrzyga, które pliki i arkusze się liczą; przebieg drugi otwiera trafienia ponownie z wyborem arkuszy. Tylko drugie otwarcie kiedykolwiek parsuje XML komórek
procedure ProcessInvoiceFile(const FileName: WideString);
var
  Probe, Workbook: TXLSXWorkbook;
  Wanted: TStringList;
  I: Integer;
begin
  Wanted := TStringList.Create;
  try
    // Przebieg 1: odkrywanie, bez komórek
    Probe := TXLSXWorkbook.Create;
    try
      Probe.MetadataOnly := True;
      if Probe.Open(FileName) <> 1 then
        Exit;
      for I := 1 to Probe.Sheets.Count do
        if Pos('Invoice', Probe.Sheets[I].Name) > 0 then
          Wanted.Add(Probe.Sheets[I].Name);
    finally
      Probe.Free;
    end;
    if Wanted.Count = 0 then
      Exit;                               // nic w tym pliku nie jest wart parsowania

    // Przebieg 2: komórki tylko dla wybranych arkuszy
    Workbook := TXLSXWorkbook.Create;
    try
      Workbook.SelectedSheets.Assign(Wanted);
      if Workbook.Open(FileName) = 1 then
        PostInvoices(Workbook);           // twój własny kod przetwarzania
    finally
      Workbook.Free;
    end;
  finally
    Wanted.Free;
  end;
end;

Zerwanie MetadataOnly po Open nie zamieni sondy w pełny skoroszyt. Flaga jest czytana podczas otwierania; zmiana jej później dotyczy tylko następnego Open na tym obiekcie. Sonda może nadal dociągać arkusz po arkuszu przez LoadSheet i dla jednego czy dwóch arkuszy to rozsądne, ale potok lepiej obsłuży czyste ponowne otwarcie, które przy okazji przynosi komentarze i tabele każdego arkusza

Gdzie to się mieści z innymi dźwigniami dużego pliku

Ładowanie selektywne tnie to, co parsujesz. Nie robi niczego dla kosztu arkusza, gdy już jest wczytany, i nie pomoże, gdy zadanie potrzebuje każdej komórki każdego arkusza. Ten przypadek jest o magazynie — pokrywa go artykuł pamięć komórek w blokach wierszy i style oparte na zakresach — i o przepustowości parsowania, pokrytej w artykule równoległe parsowanie XLSX i alokator pamięci. Gdy zadaniem jest czysty odczyt bez żadnej edycji, strumieniowy odczyt bezpośredni omija model obiektowy skoroszytu całkowicie i jest lepszym narzędziem niż jakakolwiek strategia ładowania, która jeden buduje

Całość biegnie wewnątrz twojego własnego pliku wykonywalnego Delphi albo C++Builder, bez instalacji Excela na serwerze, który skanuje. Wspierane formaty, edycje i licencjonowanie są wypisane na stronie komponentu arkuszowego HotXLS Delphi