Un trabajo de normalización masiva de hojas de cálculo son tres problemas con un mismo abrigo. Tienes un archivo histórico de formatos mezclados: .xls de la era BIFF, .xlsx modernos, unos cuantos .ods de algún experimento con LibreOffice, y un puñado de archivos que nadie puede abrir porque la contraseña se fue con un exempleado. El objetivo es convertir todo a XLSX y CSV. La versión de ese trabajo que la mayoría escribe es un bucle que abre cada archivo y lo guarda con una nueva extensión, y funciona justo hasta que alguien pregunta qué archivos perdieron sus gráficos, descartaron sus macros o nunca se abrieron. El bucle no tiene respuesta, porque la conversión por sí sola no lleva registro. Un banco de trabajo sí: primero inventaría, segundo convierte y tercero verifica, y las tres etapas tienen que compartir información para que algo de eso sea confiable
Armar 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 económicas que leen metadatos sin analizar el archivo completo. Hay contadores de auditoría por hoja que te 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 está 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
Sondea 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 por un archivo histórico grande desperdicia días. Ambas fachadas exponen GetSheetNames, que lee los metadatos de hoja sin poblar el libro de trabajo. La implementación BIFF escanea únicamente los registros BoundSheet al frente del flujo; la implementación OOXML lee solo 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 económico este bucle. GetSheetNames no reinicia ni pobla la instancia del libro de trabajo, así que un solo objeto de sondeo puede clasificar miles de archivos sin ser recreado. 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, como rara vez se puede en un archivo histórico tan antiguo. La clasificación previa a la carga merece su propio tratamiento; la mecánica de la inspección ligera está en nuestro artículo sobre listado de hojas e inspección ligera de libros de trabajo
Contar lo que un libro de trabajo realmente contiene
Una vez que un archivo pasa la clasificación, el pase 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 devuelven un valor distinto 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;
Lee Cells.Count con una salvedad en mente. El almacén de celdas es disperso, así que el número cuenta las 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 millón largo que hay entre ellas. El escaneo equivalente del lado BIFF usa los límites de UsedRange junto con ForEachCell, y arrastra el desfase de uno que hace tropezar a casi todos la primera vez: UsedRange.FirstRow y sus hermanos tienen base cero, mientras que Cells.Item[Row, Col] tiene base uno. Un recorrido que olvida sumar uno a cada límite audita el rectángulo equivocado y nunca lo dice
Dos palancas reducen el costo de un pase de solo auditoría sobre archivos heredados grandes. Establecer _DisableGraphics en true antes de abrir un .xls omite por completo el análisis de la capa de dibujo OfficeArt, lo que ahorra tiempo real en libros de trabajo densos en formas. Sin embargo, es estrictamente una optimización de solo lectura: guardar desde una instancia abierta de esa manera descartaría los dibujos que nunca analizó, así que el indicador pertenece solo a rutas que nunca escribirán el archivo de vuelta. Cuando la auditoría necesita el contenido por celda en lugar de conteos, el callback ForEachCell recorre directamente las celdas pobladas y evita la sobrecarga de Variant por acceso que las propiedades de celda indexadas pagan en cada lectura, lo cual se acumula rápido a lo largo de millones de celdas
Normaliza temprano 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 conteo de hojas, o -1 con la lista vaciada. SaveAsHTML de XLSX rompe el patrón de nuevo y devuelve 0 para el éxito, -1 para un índice de hoja fuera de rango. Un banco de trabajo que prueba = 1 en todas partes clasificará mal silenciosamente las llamadas que señalan el éxito de otra manera, y uno que prueba <> -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: trata <= 0 como fallo para las llamadas que devuelven conteos, comprueba el valor de éxito documentado para cada rutina de guardado que realmente uses, y pon ambas cosas detrás de una pequeña función de comprobación de resultados para que la convención viva en exactamente un lugar. Los pipelines por lotes fallan mucho más a menudo por una lenta acumulación de códigos de retorno sin comprobar que por cualquier error exótico del analizador, y el costo de equivocarse en esto llega cuarenta mil archivos después, cuando nadie recuerda qué conversiones realmente se aplicaron
La matriz de conversión y dónde pierde datos cada camino
Las dos fachadas se reparten el trabajo de conversión. 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 con una vaga promesa de corrección, así que puedes 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 entrecomillado 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 calcules los valores primero. La exportación a HTML produce una sola tabla, con colspan y rowspan en lugar de las celdas combinadas y los estilos base en línea. La exportación a RTF tiene un límite más marcado: no puede extender 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 activas y los dibujos no. Eso importa en el momento en que el archivo histórico 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 transportar, y el pase de auditoría es lo que te dice que esos archivos existen antes de que el lote los aplane en silencio
SaveXLSWorkbookAsXLSX es un puente de datos, no 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. Vale la pena declarar con claridad la fidelidad de ese puente, porque el nombre sugiere más de lo que hace. Copia valores, fórmulas, formatos de número, 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 alguien necesite. Para un informe de directorio con formato destinado a ser leído por 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 encaminarse a una cola manual, no a través de un puente que descartará ambos sin decir palabra
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 hoja al zip
if Modern.Open(FileName) = 1 then
Modern.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Modern.Free;
end;
end;
end;
El bucle anterior también muestra la palanca de rendimiento del lado OOXML. Establecer StreamingWrite en true transmite el XML de la hoja de cálculo directamente al paquete de salida en lugar de prepararlo como una cadena gigante en memoria, que es la diferencia entre una ejecución cómoda y un fallo por falta de memoria una vez que los archivos alcanzan cientos de miles de filas. El dimensionamiento y el comportamiento de memoria de ese modo tienen su propio tratamiento en nuestro artículo sobre escrituras en streaming para trabajos por lotes en servidor. Una propiedad más importa para un lote que quiera usar todos los núcleos: ninguna de las fachadas es segura para subprocesos, pero ninguna comparte estado global tampoco, así que el patrón soportado para la conversión en paralelo es una instancia de libro de trabajo por subproceso de trabajo, sin bloqueos entre ellas
Los archivos con contraseña, y qué hacer con ellos
Los archivos bloqueados del archivo histórico se dividen limpiamente por formato, y la división decide adónde van. El cifrado heredado de .xls, ya sea RC4, RC4 sobre CryptoAPI o la antigua ofuscación XOR, es legible: pasa la contraseña a Open y el archivo se convierte como cualquier otro. Los paquetes .xlsx cifrados son otra historia. HotXLS los detecta con CanReadEncrypted pero no puede descifrarlos, así que la única jugada honesta es encaminarlos a una cola donde un humano abra y vuelva a guardar cada uno en Excel antes de que se reincorpore al pipeline. Vale la pena diseñar para esa asimetría desde el principio, porque los archivos XLSX cifrados son los que con mayor probabilidad son los registros que a alguien realmente le importan
Cerrar el ciclo con la verificación
La tercera etapa es la que se omite, y omitirla 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 literal a menos que el pipeline ejecute primero Calculate sobre las celdas y escriba los resultados de vuelta. Saberlo 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í es tan barata que no hay excusa para dejarla fuera. Reabre cada archivo convertido con la misma biblioteca, vuelve a ejecutar los contadores de auditoría y compáralos con las cifras previas a la conversión que el pase de inventario ya registró. Un conteo de hojas que bajó, un conteo de gráficos que llegó a cero donde el origen tenía tres, un conteo de celdas que se desplomó: cada uno es una pérdida silenciosa detectada por el costo de una segunda apertura. Revisa además una muestra a ojo en Excel o LibreOffice, y la combinación detecta la abrumadora mayoría de los daños de conversión antes de que se envíen. Esta es toda la razón por la que la etapa de inventario alimenta a la etapa de verificación. Sin las cifras previas, las cifras posteriores no demuestran nada
Un banco de trabajo con auditoría previa convierte una conversión masiva riesgosa en un proceso medible con un carril de cuarentena para los archivos que no pueden pasar limpiamente. Todas las llamadas de sondeo, conteo y conversión mostradas aquí forman parte del componente HotXLS para Delphi, que las ejecuta de forma nativa en el proceso sin automatización de Excel