Artículo técnico

HotXLS: workbook audit and conversion workbench in Delphi

Un job de normalización masiva de hojas de cálculo son en realidad tres problemas bajo un mismo abrigo. Tiene un archivo con formatos mixtos: .xls de la era BIFF, .xlsx moderno, un puñado de .ods de algún experimento con LibreOffice, y un puñado de archivos que nadie puede abrir porque la contraseña se marchó con un antiguo empleado. El objetivo es convertir todo a XLSX y CSV. La versión de ese job que escribe la mayoría de la gente es un bucle que abre cada archivo y lo guarda bajo una nueva extensión, y funciona hasta que alguien pregunta qué archivos perdieron sus gráficos, cuáles descartaron sus macros, o cuáles nunca llegaron a abrirse. El bucle no tiene respuesta, porque la conversión por sí sola no lleva ningún registro. Un banco de trabajo sí: primero inventaría, después convierte, y tercero verifica, y las tres etapas tienen que compartir información para que todo ello sea fiable

Montar ese banco de trabajo en Delphi o C++Builder significa conectar cuatro capacidades de HotXLS, ninguna de las cuales necesita Excel instalado en ningún punto del pipeline. Hay dos motores nativos, una fachada BIFF8 para .xls y una fachada OOXML para .xlsx y .ods. Hay llamadas de sondeo baratas que leen metadatos sin analizar el archivo completo. Hay contadores de auditoría por hoja que le dicen qué contiene realmente un libro de trabajo. Y hay una matriz de conversión con un perfil de fidelidad documentado para cada ruta. El trabajo consiste en saber dónde tiene cada una de ellas un filo cortante, porque todas lo tienen, y esos filos son exactamente lo que convierte un lote nocturno limpio en un incidente de lunes por la mañana

Diagrama del pipeline de un banco de trabajo de conversión con auditoría primero de HotXLS en Delphi: un archivo mixto de ficheros xls, xlsx y ods se inventaría, se convierte por ruta, y luego se verifica contra los números previos registrados durante el inventario
El banco de trabajo convierte en tres etapas, y los contadores de auditoría registrados durante el inventario se convierten en las cifras previas contra las que compara la verificación

Sondee antes de cargar: nombres de hoja y detección de cifrado

Abrir un libro de trabajo de 200 MB solo para descubrir que está cifrado desperdicia minutos por archivo, y multiplicado en todo un archivo grande desperdicia días. Ambas fachadas exponen GetSheetNames, que lee metadatos de hoja sin poblar el libro de trabajo. La implementación BIFF solo escanea los registros BoundSheet al principio del stream; la implementación OOXML solo lee workbook.xml dentro del zip. Junto a ella, CanReadEncrypted detecta un contenedor de cifrado sin intentar descifrarlo:

var
  Probe: TXLSXWorkbook;
  Names: TStringList;
begin
  Names := TStringList.Create;
  Probe := TXLSXWorkbook.Create;
  try
    if Probe.CanReadEncrypted(FileName) then
    begin
      Writeln(FileName + ': encrypted container - route to manual handling');
      Exit;
    end;
    if Probe.GetSheetNames(FileName, Names) <= 0 then
      Writeln(FileName + ': unreadable - quarantine')
    else
      Writeln(Format('%s: %d sheet(s), first "%s"',
        [FileName, Names.Count, Names[0]]));
  finally
    Probe.Free;
    Names.Free;
  end;
end;

Dos detalles operativos hacen barato este bucle. GetSheetNames no reinicia ni puebla la instancia del libro de trabajo, así que un único objeto de sondeo puede clasificar miles de archivos sin volver a crearse. Y la versión de la fachada XLS de la misma llamada también entiende paquetes .xlsx, lo que la convierte en un sondeo único conveniente cuando no se puede confiar en las extensiones de archivo, cosa que rara vez ocurre en un archivo tan antiguo. El triaje antes de cargar merece su propio tratamiento; los mecanismos de la inspección ligera están en nuestro artículo sobre listado de hojas e inspección ligera de libros de trabajo

Diagrama de flujo de triaje para lotes de libros HotXLS en Delphi: CanReadEncrypted enruta los contenedores cifrados al manejo manual, GetSheetNames pone en cuarentena los ficheros ilegibles, y los ficheros que pasan entran en la pasada de auditoría que decide la ruta de conversión
Sondar con CanReadEncrypted y GetSheetNames clasifica cada archivo antes de la carga, de modo que los libros cifrados e ilegibles nunca llegan al bucle de conversión

Contar lo que realmente contiene un libro de trabajo

Una vez que un archivo supera el triaje, el paso de auditoría decide su ruta de conversión. La fachada XLSX expone un contador para cada familia de funciones que influye en una decisión de fidelidad: celdas combinadas, gráficos, imágenes, formatos condicionales, validaciones de datos, tablas, hipervínculos, y comentarios, más indicadores a nivel de libro de trabajo para macros, protección, y formato de origen. La ruta de conversión de un archivo depende casi por completo de cuáles de estos vuelven distintos de cero

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open(FileName) <> 1 then Exit;
    for I := 0 to Book.Sheets.Count - 1 do
    begin
      Sheet := Book.Sheets[I];
      Writeln(Format('%s: cells=%d merges=%d charts=%d cf=%d dv=%d protected=%s',
        [Sheet.Name, Sheet.Cells.Count, Sheet.MergedCells.Count,
         Sheet.Charts.Count, Sheet.ConditionalFormats.Count,
         Sheet.DataValidations.Count, BoolToStr(Sheet.IsProtected, True)]));
    end;
    if Book.HasVbaProject then
      Writeln('  contains VBA project - macro policy applies');
    if Book.ExternalLinks.Count > 0 then
      Writeln(Format('  %d external link(s)', [Book.ExternalLinks.Count]));
  finally
    Book.Free;
  end;
end;

Lea Cells.Count con una advertencia en mente. El almacén de celdas es disperso, así que el número cuenta celdas instanciadas, no el área rectangular del rango usado. Una hoja con un valor en A1 y otro en ZZ9999 reporta dos celdas, no el más o menos un millón que hay entre medias. El escaneo equivalente en el lado BIFF usa los límites de UsedRange junto con ForEachCell, y lleva el error de desplazamiento por uno que atrapa a casi todo el mundo la primera vez: UsedRange.FirstRow y sus hermanos son de base 0, mientras que Cells.Item[Row, Col] es de base 1. Un recorrido que olvida sumar uno a cada límite audita el rectángulo equivocado y nunca lo dice

Dos palancas recortan el coste de un paso de solo auditoría sobre archivos heredados grandes. Fijar _DisableGraphics a true antes de abrir un .xls se salta por completo el análisis de la capa de dibujo de OfficeArt, lo que ahorra tiempo real en libros de trabajo densos en formas. Es estrictamente una optimización de solo lectura, eso sí: guardar desde una instancia abierta de esa forma eliminaría los dibujos que nunca analizó, así que el indicador solo pertenece a rutas que nunca escribirán el archivo de vuelta. Cuando la auditoría necesita contenido por celda en lugar de recuentos, el callback ForEachCell recorre directamente las celdas pobladas y esquiva el sobrecoste de Variant por acceso que pagan las propiedades de celda indexadas en cada lectura, lo cual se acumula rápido a lo largo de millones de celdas

Normalice pronto los códigos de retorno inconsistentes

Las llamadas de E/S de HotXLS reportan errores mediante resultados enteros en lugar de excepciones, y las convenciones no son uniformes en toda la API. La mayoría de las llamadas de apertura y guardado devuelven 1 en caso de éxito y -1 en caso de fallo. GetSheetNames devuelve el recuento de hojas, o -1 con la lista vaciada. SaveAsHTML de XLSX rompe el patrón otra vez y devuelve 0 para éxito, -1 para un índice de hoja fuera de rango. Un banco de trabajo que compruebe = 1 en todas partes clasificará mal en silencio las llamadas que señalan éxito de otra forma, y uno que compruebe <> -1 se tragará las que fallan con un código distinto

La regla que sobrevive al contacto con toda la API es más estrecha de lo que parece: trate <= 0 como fallo para las llamadas que devuelven un recuento, compruebe el valor de éxito documentado para cada rutina de guardado que realmente use, y ponga ambas cosas detrás de una pequeña función de comprobación de resultado para que la convención viva en exactamente un lugar. Los pipelines por lotes fallan mucho más a menudo por una acumulación lenta de códigos de retorno sin comprobar que por ningún error exótico del analizador, y el coste de equivocarse aquí llega cuarenta mil archivos después, cuando ya nadie recuerda qué conversiones realmente se hicieron

La matriz de conversión y dónde pierde datos cada camino

Las dos fachadas se reparten el trabajo de conversión entre ellas. TXLSXWorkbook abre XLSX, ODS, y CSV, y guarda XLSX, ODS, CSV, HTML, RTF, y XLSX cifrado con AES. TXLSWorkbook abre y guarda BIFF, y exporta HTML, RTF, y CSV. Lo útil es que cada ruta viene con un perfil de fidelidad documentado, no una promesa vaga de corrección, así que puede decidir de antemano qué rutas son seguras para qué archivos

La exportación a CSV escribe UTF-8 con BOM, finales de línea CRLF, y comillado según RFC 4180. Lo que no hace es evaluar fórmulas: una celda que contiene =SUM(...) se exporta como el texto literal de la fórmula, así que una hoja de fórmulas se convierte en una hoja de cadenas a menos que primero calcule los valores. La exportación a HTML produce una única tabla, con colspan y rowspan haciendo las veces de celdas combinadas y estilos base incrustados. La exportación a RTF tiene un límite más agudo: no puede abarcar celdas combinadas a través de columnas, así que las celdas de continuación de una combinación salen vacías. La importación de ODS es ligera a propósito, según la propia documentación de la biblioteca. Los valores escalares y los resultados de fórmula en caché pasan; los estilos, las expresiones de fórmula ODF vivas, y los dibujos no. Eso importa en el momento en que el archivo contiene archivos OpenDocument reales regidos por OASIS ODF 1.3, donde cualquier cosa cercana a una conversión visualmente fiel necesita más de lo que esta ruta de importación fue construida para llevar, y el paso de auditoría es lo que le dice que esos archivos existen antes de que el lote los aplane en silencio

SaveXLSWorkbookAsXLSX es un puente de datos, no un puente de diseño

La fachada BIFF no puede escribir OOXML directamente, así que el cruce de .xls a .xlsx pasa por la función SaveXLSWorkbookAsXLSX de la unidad lxXlsxExport. Merece la pena expresar con claridad la fidelidad de ese puente, porque el nombre sugiere más de lo que en realidad hace. Copia valores, fórmulas, formatos numéricos, colores de relleno, atributos básicos de fuente, anchos de columna, y ajustes de vista como las líneas de cuadrícula. No copia bordes, rangos combinados, comentarios, gráficos, ni formatos condicionales. Para una normalización de grado de datos, donde los sistemas posteriores analizarán el resultado y nadie mira el formato, eso es exactamente suficiente y no se pierde nada que nadie necesite. Para un informe de consejo con formato pensado para que lo lea una persona, no es suficiente, y aquí es precisamente donde los contadores de auditoría se ganan su lugar: un archivo que la auditoría marcó como portador de gráficos y formatos condicionales debería enrutarse a una cola manual, no a través de un puente que descartará ambos sin decir nada

Diagrama de fidelidad del puente para HotXLS SaveXLSWorkbookAsXLSX en Delphi: valores, fórmulas, formatos numéricos, colores de relleno, atributos básicos de fuente, anchos de columna y ajustes de vista cruzan de BIFF xls a XLSX, mientras bordes, rangos combinados, comentarios, gráficos y formatos condicionales se descartan
SaveXLSWorkbookAsXLSX transporta los datos que un parser necesita a través del puente de BIFF a OOXML, y los contadores de auditoría son lo que marca los archivos cuyos gráficos y combinaciones se perderían
var
  Legacy: IXLSWorkbook;        // referencia de interfaz: no llamar a Free
  Modern: TXLSXWorkbook;
begin
  if SameText(ExtractFileExt(FileName), '.xls') then
  begin
    Legacy := TXLSWorkbook.Create;
    if Legacy.Open(FileName) <= 0 then Exit;
    if SaveXLSWorkbookAsXLSX(Legacy,
         ChangeFileExt(FileName, '.xlsx')) <= 0 then
      Writeln('bridge failed: ' + FileName);
  end
  else
  begin
    Modern := TXLSXWorkbook.Create;
    try
      Modern.StreamingWrite := True;     // transmite el XML de la hoja al zip
      if Modern.Open(FileName) = 1 then
        Modern.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
    finally
      Modern.Free;
    end;
  end;
end;

El bucle de arriba también muestra la palanca de rendimiento en el lado OOXML. Fijar StreamingWrite a true transmite el XML de la hoja directamente al paquete de salida en lugar de organizarlo como una única cadena gigante en memoria, que es la diferencia entre una ejecución cómoda y un fallo por falta de memoria en cuanto los archivos alcanzan cientos de miles de filas. El dimensionado y el comportamiento de memoria de ese modo reciben su propio tratamiento en nuestro artículo sobre escrituras en streaming para jobs por lotes en el servidor. Una propiedad más importa para un lote que quiere usar todos los núcleos: ninguna de las dos fachadas es segura para hilos, pero tampoco comparten estado global, así que el patrón admitido para conversión en paralelo es una instancia de libro de trabajo por hilo trabajador, sin ningún bloqueo entre ellos

Los archivos con contraseña, y qué hacer con ellos

Los archivos bloqueados del archivo se dividen limpiamente por formato, y esa división decide adónde van. El cifrado heredado de .xls, ya sea RC4, RC4 sobre CryptoAPI, o la vieja ofuscación XOR, es legible: pase la contraseña a Open y el archivo se convierte como cualquier otro. Los paquetes .xlsx cifrados son una historia distinta. HotXLS los detecta con CanReadEncrypted pero no puede descifrarlos, así que el único movimiento honesto es enrutarlos a una cola donde una persona abra y vuelva a guardar cada uno en Excel antes de que se reincorpore al pipeline. Esa asimetría merece la pena diseñarla desde el principio, porque los archivos XLSX cifrados son los que con más probabilidad son los registros que a alguien realmente le importan

Cerrar el círculo con verificación

La tercera etapa es la que se salta, y saltársela es lo que convierte una conversión masiva en un pasivo. Ninguna ruta de guardado de HotXLS evalúa fórmulas. Excel recalcula cuando abre un archivo, así que una conversión de XLSX a XLSX se mantiene correcta, pero un destino CSV recibe el texto de la fórmula tal cual a menos que el pipeline ejecute primero Calculate sobre las celdas y escriba los resultados de vuelta. Saber eso de antemano es la diferencia entre un CSV lleno de números y un CSV lleno de cadenas =SUM(...) que nadie nota hasta que una importación posterior se atraganta con ellas

La verificación en sí misma es lo bastante barata como para que no haya excusa para omitirla. Vuelva a abrir cada archivo convertido con la misma biblioteca, vuelva a ejecutar los contadores de auditoría, y compárelos con los números previos a la conversión que ya registró el paso de inventario. Un recuento de hojas que bajó, un recuento de gráficos que llegó a cero donde el origen tenía tres, un recuento de celdas que se desplomó: cada uno es una pérdida silenciosa atrapada por el coste de una segunda apertura. Verifique además una muestra a ojo en Excel o LibreOffice, y la combinación atrapa la inmensa mayoría del daño de conversión antes de que se entregue. Esta es toda la razón por la que la etapa de inventario alimenta la etapa de verificación. Sin los números de antes, los números de después no prueban nada

Un banco de trabajo que audita primero convierte una conversión masiva arriesgada en un proceso medible con un carril de cuarentena para los archivos que no pueden pasar limpiamente. Todas las llamadas de sondeo, recuento, y conversión mostradas aquí forman parte de HotXLS Delphi Component, que las ejecuta nativamente dentro del proceso sin automatización de Excel