Artículo técnico

Carga de XLSX solo con metadatos y selectiva con HotXLS

La pregunta «este libro, ¿todavía define un nombre llamado TaxRate?» la responde una parte pequeña de un paquete XLSX. Los nombres de hojas, su estado oculto o muy oculto y cada nombre definido a nivel libro viven en xl/workbook.xml (ECMA-376 Part 1, §18.2), usualmente unos pocos kilobytes. Las celdas viven en otro lado, una parte xl/worksheets/sheetN.xml por hoja más una tabla de shared strings, y ahí es donde están casi todos los bytes. Un cargador que insiste en construir el libro completo antes de que usted pueda preguntar nada inflará y parseará todo eso, por cada archivo de la carpeta, para responder una pregunta que los primeros pocos kilobytes ya resolvieron

La división que importa es entre leer la forma de un libro y leer su peso. Un cargador que puede detenerse después de la forma, cargar el peso para un subconjunto elegido de hojas, y traer una hoja más después convierte «abrir un libro» de un paso todo-o-nada en algo que usted acota por llamada. También trae consigo dos trampas, una sobre las bases de índices y otra sobre cuánto tiempo tiene que quedarse quieto el archivo de origen

Qué lee un Open solo con metadatos, y dónde se detiene

Con TXLSXWorkbook.MetadataOnly en True antes de Open, HotXLS parsea la parte del libro, los estilos, el theme, los nombres definidos, los nombres de hojas y estados de visibilidad, las conexiones, los vínculos externos y las propiedades del documento desde docProps/core.xml y docProps/app.xml. Luego se salta el XML de celdas de cada hoja y las partes satélite por hoja: comentarios, dibujos y tablas. La tabla de shared strings también se salta, lo que importa más de lo que suena, porque en libros cargados de texto esa única parte puede rivalizar con las hojas en tamaño

El libro que vuelve está estructuralmente completo. Sheets.Count es correcto, cada hoja tiene su nombre real y Visibility, DefinedNames está poblado, ExternalLinks lista cada objetivo vinculado, y Title, Author y LastModifiedBy cargan lo que el archivo declaró. Cada hoja simplemente tiene cero celdas

HotXLS MetadataOnly lee propiedades del libro, nombres de hojas, estilos y vínculos en un archivo XLSX en Delphi, y luego se detiene antes de cualquier XML de celdas de hoja
MetadataOnly lee cada parte estructural del archivo y se detiene antes del XML de celdas de hoja. El libro Delphi vuelve con nombres de hojas, visibilidad, nombres definidos y estilos, y cero celdas

Eso es exactamente el dato sobre el que corre una auditoría. ¿Qué libros esconden una hoja? ¿Cuáles todavía definen TaxRate, y hacia qué? ¿Cuáles vinculan a un recurso compartido de archivos que se retiró el año pasado? El escáner de abajo responde las tres para un archivo. Note las dos convenciones de indexado lado a lado: Sheets[] es 1-based, coincidiendo con el motor XLS, mientras que DefinedNames y ExternalLinks son listas ordinarias 0-based

procedure ScanWorkbook(const FileName: WideString; Report: TStrings);
var
  Workbook: TXLSXWorkbook;
  Name: TXLSXDefinedName;
  I: Integer;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Workbook.MetadataOnly := True;           // lo lee Open, así que fíjelo primero
    if Workbook.Open(FileName) <> 1 then
    begin
      Report.Add(FileName + ': open failed');
      Exit;
    end;
    for I := 1 to Workbook.Sheets.Count do   // Sheets[] es 1-based
      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 0-based
      Report.Add(FileName + ': links to ' + Workbook.ExternalLinks[I].Target);
  finally
    Workbook.Free;
  end;
end;

El escáner lee Visibility en lugar del Boolean Visible a propósito. Visible es False para ambos estados ocultos, y una hoja muy oculta es la que una auditoría más quiere marcar, ya que el diálogo Unhide de Excel no la lista y solo VBA o una edición directa del archivo la trae de vuelta

Cargar dos hojas de entre cuarenta con SelectedSheets

SelectedSheets es una TStringList propiedad del libro. Cuando sostiene al menos un nombre, Open parsea el XML de celdas solo para las hojas que nombra; cada otra hoja se crea con su nombre y visibilidad correctos y sin celdas. Una lista vacía, el valor por defecto, carga todas las hojas. Los nombres se buscan con el IndexOf propio de la lista, así que con los ajustes por defecto la coincidencia ignora mayúsculas, lo que se alinea con la regla de Excel de que dos nombres de hoja en un libro no pueden diferir solo en mayúsculas

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 es su propio código de UI

Los dos interruptores interactúan en una sola dirección. MetadataOnly = True se impone a la selección y se salta todas las hojas, así que un libro configurado con ambos vuelve sin celdas en ninguna parte, y la selección se ve rota cuando simplemente fue sobrepasada. Use uno por Open: solo-metadatos para descubrimiento, una selección para trabajo dirigido

La carga selectiva también corre por la fase de parseo paralelo cuando ParallelParse está activo; las hojas no seleccionadas se saltan dentro de los trabajos de los workers, así que los hilos gastan su tiempo en las hojas que usted pidió. Las mismas dos propiedades aplican cuando el origen es un libro binario .xlsb, con una regla de alcance: un .xlsb que carga un proyecto VBA tiene que abrirse sin estas dos opciones; con cualquiera de las dos puestas, Open reporta la lectura selectiva como no soportada

HotXLS SelectedSheets parsea celdas para dos hojas nombradas de entre cuarenta en Delphi mientras LoadSheet materializa una hoja saltada desde el archivo de origen sin cambios
SelectedSheets parsea el XML de celdas solo para las hojas nombradas, dejando al resto como stubs vacíos que conservan nombre y visibilidad. LoadSheet materializa un stub después, siempre que el archivo de origen siga en disco y sin cambios

¿Por qué LoadSheet(1) carga la hoja equivocada?

Porque LoadSheet toma una posición 0-based mientras Sheets[] es 1-based. LoadSheet(1) materializa la segunda hoja del libro, y como cargar una hoja que ya tiene celdas es un no-op que devuelve True, un índice revuelto puede verse como éxito mientras la hoja en que el usuario clicó se queda vacía. La conversión pertenece a exactamente un lugar de su código

La segunda trampa es la vida del archivo. LoadSheet no sostiene el archivo abierto entre llamadas. Reabre el archivo original por nombre, parsea la parte del libro, los estilos y las shared strings, y luego parsea la hoja pedida. Antes de confiar en ese archivo, HotXLS compara su tamaño y su hora de última escritura con los valores capturados en Open; si otro proceso guardó sobre el libro mientras tanto, LoadSheet devuelve False en lugar de mezclar celdas de una versión distinta en el modelo que usted ya sostiene

procedure TReportForm.ShowSheetNo(SheetNo: Integer);  // SheetNo es 1-based, como Sheets[]
var
  Sheet: TXLSXWorksheet;
begin
  Sheet := FWorkbook.Sheets[SheetNo];
  if Sheet.Cells.Count = 0 then
    if not FWorkbook.LoadSheet(SheetNo - 1) then      // LoadSheet quiere la posición 0-based
    begin
      StatusBar.SimpleText := 'The workbook changed on disk; reopen it';
      Exit;
    end;
  Grid.ShowSheet(Sheet);                              // su propio código de grid
end;

Unas consecuencias se siguen de ese diseño. Un libro abierto desde un TStream no tiene archivo de origen al cual volver, así que LoadSheet devuelve False para un .xlsx abierto por stream; lo mismo vale para un libro cifrado, que se descifra a memoria antes de parsear. Un .xlsb binario abierto selectivamente desde un stream es la excepción, porque HotXLS prepara una copia temporal del stream para las hojas diferidas. Y una hoja genuinamente vacía se re-leerá en cada llamada, ya que cero celdas es también lo que parece una hoja saltada

El modelo de costos decide cómo usarlo. Cada llamada a LoadSheet vuelve a parsear la tabla de shared strings, así que materializar diez hojas una por una paga esa parte diez veces. Para un visor donde el usuario abre una pestaña más de vez en cuando, es un precio fino. Cuando usted ya conoce el conjunto de hojas, reabra con SelectedSheets en cambio: una pasada del archivo, un parseo de shared strings. Hay también una diferencia de contenido: LoadSheet trae las celdas, valores, fórmulas y formatos de una hoja, mientras que los comentarios, dibujos y tablas llegan con un Open, así que las hojas cuyos comentarios o tablas lee su código pertenecen a la selección desde el principio

¿Por qué un round trip cambia partes que usted jamás tocó?

Un guardado que regenera todo reescribe bytes que jamás cambiaron. Descomprima el theme, construya un modelo de objetos, serialícelo, comprímalo otra vez, y el resultado difiere del original aunque nadie editó el theme. Para proyectos VBA y pivot caches el costo es mayor que un diff ruidoso: un proyecto VBA es un archivo compuesto binario con sus propios streams internos, y un pivot cache sostiene una instantánea de datos de origen cuyos campos las definiciones de la pivot table direccionan por posición, así que lo más seguro que se puede hacer con cualquiera de los dos, cuando nadie lo cambió, es dejar sus bytes en paz

PreserveRawParts, que vale True por defecto, hace que SaveAs copie los bytes comprimidos de las partes sin cambios directo desde el archivo de origen. Eso cubre el theme, el proyecto VBA, las conexiones, los charts, los medios incrustados, los ajustes de impresora, las pivot tables y las definiciones y registros de pivot cache, cada uno solo mientras el modelo dice que no fue modificado. Esas partes jamás se inflan o se vuelven a desinflar en el camino. Poner la propiedad en False fuerza la re-serialización completa de cada parte, que es el interruptor a alcanzar cuando quiere que la salida refleje solo lo que contiene el modelo de objetos

La ruta de copia cruda usa la misma verificación de archivo que LoadSheet: el origen debe seguir existiendo con el tamaño y la marca de tiempo vistos en Open. Cuando no, SaveAs serializa esas partes de forma normal y el archivo sigue completo. Dos casos toman siempre la ruta de serializar: un libro cuyo origen fue un .xlsb binario, que se convierte a XML al leer, y un guardado con StrictOOXML puesto, donde los namespaces de las partes cambian

Guardar un libro parcialmente cargado necesita una decisión. Para un origen .xlsb, SaveAs se niega mientras alguna hoja siga diferida y registra «Load every deferred binary worksheet before saving» en Diagnostics. Para un .xlsx, trate un libro cargado selectivamente como una vista de lectura: una hoja stub no tiene celdas que el escritor pueda emitir, así que o llame a LoadSheet por cada stub primero o reabra sin selección antes de editar y guardar

Un pipeline de descubrimiento que reabre a propósito

El patrón que aguanta tiene dos pasadas. La pasada uno abre cada archivo solo con metadatos y decide. La pasada dos reabre solo los archivos que coincidieron, con una selección de hojas, y hace el trabajo. Reabrir parece desperdicio hasta que cuenta lo que la pasada uno evitó: el segundo Open es el único que parsea celdas, y parsea solo las hojas que la pasada uno eligió

Pipeline de descubrimiento HotXLS de dos pasadas en Delphi: una pasada solo con metadatos decide qué libros y hojas importan, luego una pasada dirigida parsea solo las hojas seleccionadas
La pasada uno escanea el directorio solo con metadatos y decide qué archivos y hojas importan; la pasada dos reabre las coincidencias con una selección de hojas. Solo la segunda apertura parsea jamás XML de celdas
procedure ProcessInvoiceFile(const FileName: WideString);
var
  Probe, Workbook: TXLSXWorkbook;
  Wanted: TStringList;
  I: Integer;
begin
  Wanted := TStringList.Create;
  try
    // Pasada 1: descubrimiento, sin celdas
    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;                               // nada en este archivo vale la pena parsear

    // Pasada 2: celdas solo para las hojas elegidas
    Workbook := TXLSXWorkbook.Create;
    try
      Workbook.SelectedSheets.Assign(Wanted);
      if Workbook.Open(FileName) = 1 then
        PostInvoices(Workbook);           // su propio código de procesamiento
    finally
      Workbook.Free;
    end;
  finally
    Wanted.Free;
  end;
end;

Limpiar MetadataOnly después de Open no convierte la sonda en un libro completo. La bandera se lee al abrir; cambiarla después solo afecta al próximo Open sobre ese objeto. La sonda todavía puede crecer una hoja a la vez vía LoadSheet, y para una o dos hojas eso es razonable, pero a un pipeline lo sirve mejor la re-apertura limpia, que además trae consigo los comentarios y tablas de cada hoja

Dónde encaja esto con las demás palancas de archivos grandes

La carga selectiva recorta lo que usted parsea. No hace nada por el costo de una hoja una vez cargada, y no puede ayudar cuando el trabajo necesita cada celda de cada hoja. Ese caso es de almacenamiento, cubierto en el artículo sobre almacenamiento de celdas por bloques de filas y estilos por rango, y de throughput de parseo, cubierto en parseo paralelo de XLSX y el allocador de memoria. Cuando la tarea es una lectura pura sin ninguna edición, el lector directo con streaming se salta el modelo de objetos del libro por completo y es mejor herramienta que cualquier estrategia de carga que construya uno

Todo esto corre dentro de su propio ejecutable Delphi o C++Builder, sin instalación de Excel en el servidor que hace el escaneo. Los formatos soportados, las ediciones y las licencias están listados en la página del componente de hojas de cálculo HotXLS para Delphi