Artículo técnico

HotXLS: sheet listing and lightweight workbook inspection

A veces la única pregunta que una rutina de admisión necesita responder es estructural: ¿tiene este libro de trabajo una hoja llamada "Mapping", o cuántas pestañas lleva. Responder a eso llamando a Open es la forma cara de hacerlo. Una apertura completa infla la tabla de cadenas compartidas, decodifica cada registro de estilo, y recorre las celdas de cada hoja, porque no tiene forma de saber que usted solo quería la tabla de contenidos. En un archivo grande eso son cientos de megabytes de asignaciones y varios segundos de CPU gastados en leer una lista que ocupa unos pocos kilobytes. HotXLS, la biblioteca nativa de hoja de cálculo de losLab para Delphi, le da esa lista por sí sola: GetSheetNames devuelve los nombres de hoja, en el orden del libro de trabajo, sin materializar ni una sola celda

Por qué el catálogo es barato de leer

Ambos formatos de hoja de cálculo ponen su tabla de contenidos cerca del principio, que es lo que hace que una llamada de listado sea rápida y no ingeniosa. Un paquete OOXML mantiene el catálogo de hojas en xl/workbook.xml, una parte que se mantiene pequeña tanto si el libro de trabajo tiene diez filas como diez millones. Un .xls BIFF8 almacena sus registros BoundSheet al principio del stream global del libro de trabajo, antes de cualquier dato de celda. Así que el trabajo que evita una llamada de listado no es un error de redondeo frente a una apertura completa. Es la mayor parte del archivo. Leer el catálogo cuesta el mismo puñado de kilobytes sin importar el número de filas, mientras que una apertura completa escala con los datos, y en un libro de trabajo de varios megabytes esa brecha llega a varios órdenes de magnitud tanto en bytes tocados como en memoria asignada

HotXLS GetSheetNames en Delphi leyendo solo el catálogo de hojas de un fichero XLSX o XLS mientras una apertura completa recorre cada celda
El catálogo reside en workbook.xml o en los registros BoundSheet, de modo que listar cuesta unos pocos kilobytes mientras que una apertura completa escala con los datos

Ese coste plano es la propiedad en torno a la que merece la pena diseñar. Una puerta de admisión construida sobre GetSheetNames se comporta igual en un archivo de 200 filas que en uno de 200 MB, así que el archivo más lento de un lote ya no marca el ritmo a la hora de decidir si un archivo siquiera merece procesarse

Una sola llamada para .xls, .xlsx, y los formatos de plantilla

En la fachada XLS, TXLSWorkbook.GetSheetNames lee más que .xls. También acepta los formatos basados en zip .xlsx, .xlsm, .xltx, y .xltm, extrayendo solo workbook.xml del archivo. Para una entrada .xls genuina escanea los registros BoundSheet y se detiene en el primer registro EOF del subflujo global, así que un archivo binario grande solo cuesta sus kilobytes iniciales. La fachada XLSX lleva una garantía que importa más para código de servicio de larga duración de lo que parece a primera vista: TXLSXWorkbook.GetSheetNames no deja la instancia del libro de trabajo ni reiniciada ni poblada, así que una instancia que ya tiene un documento abierto puede sondear otros archivos sin perturbar el que tiene entre manos. GetODSSheetNames aplica el mismo enfoque a los paquetes OpenDocument, y cada una de estas llamadas tiene una sobrecarga de stream, lo que le permite inspeccionar una subida que nunca llega a tocar el disco

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;

La misma llamada sirve para un buen diálogo de importación de escritorio. Liste las hojas, deje que el usuario elija una, y pague la apertura completa solo después de hacer la elección. Con un libro de trabajo de cincuenta hojas la diferencia es visible: un selector que aparece al instante frente a uno que se estanca mientras todo el archivo se carga detrás

Los archivos .xlsm con macros habilitadas y los formatos de plantilla se listan exactamente igual que un .xlsx simple, ya que el catálogo se sitúa en el mismo workbook.xml lleve o no un vbaProject.bin a bordo del paquete. Un pipeline de admisión puede por tanto enumerar las hojas de un libro de trabajo con macros para su enrutamiento, sin tocar nunca la carga de macros ni hacer nunca nada que la ejecute, y dejar la decisión de política de macros a la etapa que realmente abre el archivo

Leer el valor de retorno sin engañarse

Las convenciones de retorno no son uniformes en HotXLS. Algunas llamadas devuelven 1 en caso de éxito, otras devuelven un recuento, así que para las funciones de listado la única comprobación que se sostiene es tratar cualquier valor de cero o por debajo como fallo, con la lista de cadenas vaciada. Resista la tentación de leer una lista vacía como "un libro de trabajo sin hojas". Tanto ECMA-376 como la especificación BIFF8 exigen al menos una hoja en un libro de trabajo válido, así que cero nombres siempre significa que la lectura falló, nunca que el archivo esté legítimamente vacío

Un listado fallido es en sí mismo una señal que merece la pena conservar. Un archivo .xlsx que falla la llamada es una de unas pocas cosas concretas: está truncado, no es en realidad un paquete OOXML (exportaciones CSV mal etiquetadas de otros sistemas aparecen aquí constantemente), o es un contenedor cifrado. Distinguir entre esas opciones es el trabajo de la siguiente comprobación. Registrar los primeros bytes del archivo rechazado junto con el fallo suele convertir un hilo de soporte en un único mensaje

Detectar contenedores cifrados antes de enrutar

Un .xlsx cifrado no es un zip. Es un archivo compuesto OLE que envuelve los streams EncryptionInfo y EncryptedPackage, así que GetSheetNames no puede ver dentro de él y devuelve fallo como cualquier otro archivo ilegible. CanReadEncrypted comprueba esa forma de contenedor, lo que permite que la admisión enrute un archivo cifrado a propósito en lugar de tragarse un error de lectura genérico procedente de algún punto profundo de un worker:

Flujo de triaje de admisión en Delphi con CanReadEncrypted y GetSheetNames de HotXLS enrutando las cargas a necesita-contraseña, ilegible o normal
CanReadEncrypted corre primero porque un archivo OOXML cifrado es un contenedor OLE que las llamadas de listado no pueden ver dentro
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // El OOXML cifrado es un contenedor OLE, no un zip: compruébelo primero,
    // porque las llamadas de listado no pueden ver dentro de él.
    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;

El cifrado es donde HotXLS es deliberadamente asimétrico, así que el enrutamiento tiene que respetar eso. El cifrado heredado de .xls (RC4, RC4 CryptoAPI, XOR) es legible: TXLSWorkbook.Open(FileName, Password) descifra con una contraseña almacenada, y esos archivos pueden permanecer en la ruta automatizada. Los paquetes OOXML cifrados van al revés. HotXLS puede escribir uno con SaveAsEncrypted, pero no puede volver a leerlo. OpenEncrypted lanza EXlsxEncryptionNotImplemented cuando se le entrega un paquete cifrado, razón por la cual un diseño de admisión honesto envía los .xlsx cifrados a una persona con Excel y mantiene el .xls con contraseña dentro del código

Para trabajo por lotes, este clasificador se gana su lugar ejecutándose sobre todo un directorio entrante antes de que cualquier worker empiece el procesamiento real, ya que cada sondeo cuesta más o menos una apertura de archivo y unos pocos kilobytes de lecturas. Adelantarlo cambia el modo de fallo que realmente le importa a operaciones. En lugar de un job de las 3 de la madrugada que muere en el archivo 412 de 600, obtiene 412 archivos en cola y 5 rechazados en la admisión con un motivo adjunto a cada uno. Las mismas llamadas de biblioteca, una historia operativa mucho mejor

Las preguntas que una llamada de listado no puede responder

Nombres y orden son todo lo que se obtiene. Las llamadas de listado no dicen nada sobre visibilidad, así que las hojas ocultas y muy ocultas llegan a la lista con el mismo aspecto que cualquier otra. No reportan dimensiones de rango usado, ni recuentos de celdas, ni propiedades de documento. La parte docProps/core.xml también es pequeña, pero hoy no existe ningún sondeo exclusivo de propiedades, así que los metadatos de autor y título siguen costando un Open completo. La forma limpia de convivir con eso es dejar que los hechos baratos enruten cada archivo y reservar los caros para los archivos que sobreviven al enrutamiento. Para los archivos que sí avanzan hacia una lectura profunda, un escaneo de solo lectura de un .xls grande se ejecuta notablemente más rápido con _DisableGraphics := True, que se salta el análisis de OfficeArt. Eso sí, nunca guarde desde esa instancia: la capa de dibujo que se saltó ha desaparecido del modelo, y guardar la eliminaría del archivo

Los archivos que superan el triaje suelen dirigirse hacia un análisis más profundo. El banco de trabajo de auditoría y conversión de libros cubre los contadores por hoja que merece la pena recopilar una vez que una apertura completa está justificada, y la guía de rendimiento con libros de trabajo grandes cubre cómo mantener rápida esa apertura completa

HotXLS es una biblioteca nativa de hoja de cálculo en Object Pascal para Delphi y C++Builder; la superficie completa de la API, incluidas las llamadas de inspección mostradas aquí, está documentada en la página de producto de HotXLS Delphi Component