Si el único trabajo de un servidor es emitir archivos de Excel, no tiene ningún motivo para ejecutar Excel. Instalar Office en un agente de compilación o en un servicio de informes para controlarlo mediante automatización COM es el diseño equivocado, y lo ha sido desde que existe la práctica. Microsoft lo dice ella misma, en una guía que no se ha suavizado en veinte años: Office no está ni construido ni licenciado para ser automatizado desde un proceso desatendido del lado servidor. La respuesta correcta es escribir los bytes de BIFF y OOXML directamente, sin ningún Excel de por medio. Esa es toda la premisa de HotXLS, una biblioteca nativa de Object Pascal que lee y escribe los formatos de hoja de cálculo por sí misma, así que no hay ninguna aplicación de escritorio que se cuelgue, tenga fugas, o haya que pagar por asiento
Por qué controlar EXCEL.EXE desde un servicio falla
La automatización COM controla remotamente un programa de escritorio, y un programa de escritorio asume silenciosamente tres cosas que un servicio de Windows no le puede entregar: un perfil de usuario cargado, una estación de ventana interactiva, y una persona mirando la pantalla. Quite eso y los fallos llegan de una forma que ninguna máquina de desarrollador reproduce nunca. Un aviso de recuperación de archivo, un error de complemento, o un diálogo de activación de licencia se abre en un escritorio que nadie puede ver, y la llamada de automatización que lo disparó nunca regresa. El llamador acaba agotando el tiempo de espera y muriendo; la instancia de Excel frecuentemente no, sobreviviendo como un huérfano que retiene bloqueos de archivo y envenena la siguiente ejecución. Cualquiera que haya visto once procesos EXCEL.EXE sueltos acumularse bajo una cuenta de servicio conoce el resto de esa historia
La historia de escalado no es mejor ni siquiera cuando nada se cae. Una instancia de Excel es un pipeline de un solo libro de trabajo, cada acceso a propiedad paga el coste del marshaling COM entre procesos, y la máquina que ejecuta el código lleva una licencia de Office cuyos términos excluyen exactamente este uso. La mayoría de los equipos se topan con estos límites una interrupción cada vez, que es más o menos cómo "retirar la capa COM" acaba en una hoja de ruta
Antes de que empiece esa reescritura, zanje una cuestión de alcance, porque decide cuánto del trabajo es real. El código COM casi nunca se limita a fijar valores de celda. Llama a Workbook.SaveAs con constantes de formato, fuerza el recálculo, empuja la configuración de impresión, a veces recurre al portapapeles. Recorra el código antiguo y anote cuáles de esos comportamientos realmente llegan a la salida, ya que cada uno aterriza en un rincón distinto de una biblioteca nativa, y un par de ellos (la interoperabilidad con el portapapeles es el caso obvio) no tienen ningún sentido del lado servidor y deberían descartarse en lugar de portarse
Dos motores nativos, dos modelos de propiedad
HotXLS sustituye el proceso de Excel por dos implementaciones de formato directas. Un motor de flujo de registros BIFF8 (TXLSWorkbook, unidad lxHandle) gestiona .xls. Un escritor de paquetes OOXML (TXLSXWorkbook, unidad lxHandleX) produce .xlsx que cumple con ECMA-376 / ISO/IEC 29500. No hay nada que registrar y nada que instalar en el servidor, y puede mantener tantos libros de trabajo abiertos a la vez como permita la memoria
Lo que hace tropezar a la gente pronto es que las dos fachadas son propietarias de su memoria de forma distinta, y la diferencia es silenciosa hasta que se cae:
var
Book: IXLSWorkbook; // referencia de interfaz: se libera automáticamente
Sheet: IXLSWorksheet;
BookX: TXLSXWorkbook; // objeto sencillo: usted lo libera
SheetX: TXLSXWorksheet;
begin
// Salida BIFF8 .xls - sin Free; el recuento de referencias de la interfaz es propietario
Book := TXLSWorkbook.Create;
Sheet := Book.Sheets.Add;
Sheet.Name := 'Report';
Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
Book.SaveAs('report.xls');
// Salida OOXML .xlsx - ciclo de vida explícito
BookX := TXLSXWorkbook.Create;
try
SheetX := BookX.Sheets.Add('Report');
SheetX.Cells[1, 1].Value := 'Generated without Excel';
BookX.SaveAs('report.xlsx');
finally
BookX.Free;
end;
end;
La fachada XLS tiene recuento de referencias a través de la interfaz IXLSWorkbook. Declare la variable como el tipo de interfaz y nunca llame a Free sobre ella; conserve el mismo objeto en una variable de objeto sencillo y libérelo usted mismo, y el recuento de referencias lo libera una segunda vez. La fachada XLSX es un objeto ordinario que quiere un try..finally ordinario. El direccionamiento de celda es de base 1 en ambos lados, que es el único punto en el que los dos coinciden. Las colecciones de hojas no: Entries en el lado XLS es de base 1, el indexador Items de XLSX es de base 0, y ese error de desplazamiento por uno compila limpiamente sea cual sea la forma en que se equivoque, y solo se muestra en tiempo de ejecución
Escribir un libro de trabajo directamente en una respuesta HTTP
Una exportación del lado servidor normalmente no tiene ningún motivo para tocar el disco. Los archivos temporales exigen una política de limpieza, colisionan bajo solicitudes concurrentes, y dejan datos de clientes en volúmenes que a nadie se le ocurrió auditar. Ambas fachadas toman un TStream a través de sus sobrecargas de SaveAs, así que el libro de trabajo puede pasar directamente a la respuesta:
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
Book.SaveAs(Mem); // escribe desde la posición ACTUAL del stream
Mem.Position := 0; // rebobina antes de entregar el stream
Response.ContentType :=
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
Response.ContentStream := Mem; // el framework ahora es propietario de Mem
finally
Book.Free;
end;
El rebobinado es la línea que se gana su comentario. SaveAs(Stream) escribe desde la posición actual del stream y nunca vuelve a buscar el cero después. Olvide Mem.Position := 0 y el cliente recibe una descarga de cero bytes, o Excel dice que el archivo está corrupto. Este es el error más común en código de libro de trabajo orientado a la web, y el más cruel, porque pasa desapercibido por cualquier prueba unitaria que solo afirme que el stream tiene una longitud distinta de cero
Una única rutina de construcción de libro de trabajo llega a cualquier otro formato de entrega sin reestructurar. SaveAsCSV responde a la petición de "dame solo los datos en bruto", SaveAsHTML gestiona "insértalo en una página del portal", SaveAsRTF alimenta pipelines de documentos, y SaveAsODS cubre un mandato de OpenDocument, todos con sobrecargas tanto de archivo como de stream. Una sola rutina de exportación más un parámetro de formato reemplaza lo que solía ser cuatro macros COM separadas. El TXLSXHtmlExportOptions del exportador HTML lleva título, clase CSS, y un interruptor de fragmento o documento completo, lo que mantiene el caso del portal fuera del negocio de editar con expresiones regulares el marcado exportado
Valores de fórmula sin un proceso de Excel que los calcule
Bajo automatización COM, Excel recalculaba todo gratis, y abandonar COM revoca eso silenciosamente. SaveAs almacena las fórmulas como texto sin evaluarlas; los números solo aparecen una vez que Excel abre el archivo y recalcula, un comportamiento que la fachada XLS le permite ajustar mediante RecalcOnSave y CalculationMode. Para un archivo destinado a una persona eso es exactamente correcto. Es incorrecto para un servicio que tiene que confirmar un total antes de entregarlo, e incorrecto para la exportación a CSV, que escribe el texto de la fórmula en lugar de su resultado. Cualquiera de los dos casos tiene que evaluarse en el servidor con el motor integrado:
SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)'; // fachada XLSX: sin el prefijo '='
Total := BookX.Calculate('SUM(A1:A2)'); // evalúa en el servidor, ahora
if Total <> 2150 then
raise Exception.Create('reconciliation failed before delivery');
La convención de fachada muerde otra vez aquí. El lado XLSX asigna expresiones mediante Cell.Formula sin signo igual; el lado XLS las escribe mediante Cell.Value con un '=' inicial. Traslade código de uno a otro sin cambios y la convención equivocada almacena una cadena de texto que meramente se parece a una fórmula, sin ningún error que lo señale. Cuando las fórmulas de un libro de trabajo necesitan alcanzar su propia lógica de negocio, el callback OnUserFunction deja que el motor entregue nombres de función desconocidos al código Delphi en el momento de evaluación. Ese es el sustituto nativo de los complementos UDF que suelen esconderse dentro de las mismas hojas de cálculo en torno a las que creció un sistema de automatización COM
Bordes de despliegue que solo afloran en el servidor
Unos pocos detalles deciden si el despliegue es limpio o desconcertante, y el primero es el grafo de unidades. El exportador de dataset de arrastrar y soltar TDataToXLS arrastra Forms, Controls, y Dialogs de la VCL. Inofensivo en una herramienta de escritorio; en un servicio de consola arrastra toda la VCL detrás de sí. Las unidades núcleo lxHandle y lxHandleX solo recurren a Windows, Classes, SysUtils, y Variants, así que un servicio puro sale mejor parado escribiendo su propio bucle de dataset contra la API núcleo que importando el componente por comodidad
Luego está el threading. Las instancias de libro de trabajo no son seguras para hilos, pero tampoco comparten ningún estado global, así que el patrón que escala es el más simple: un objeto de libro de trabajo por job, o por hilo trabajador. Eso compra generación de informes en paralelo, algo que una única instancia de Excel compartida nunca puede hacer. Un manejador de solicitud que crea, rellena, guarda y libera su propio libro de trabajo no necesita ningún bloqueo en absoluto, y el radio de impacto de un fallo se reduce de "la instancia de Excel compartida está atascada para todos" a "esta solicitud concreta lanzó una excepción", algo que su manejo de errores existente ya sabe cómo tratar
La orientación de formato es la última de ellas. TXLSWorkbook.SaveAs escribe BIFF (xlExcel97) por defecto, y empujar contenido XLS hacia .xlsx pasa por el puente SaveXLSWorkbookAsXLSX con fidelidad reducida. Elija la fachada según el formato que pretenda entregar, en tiempo de diseño, en lugar de construir en una y convertir al final del pipeline
Para la mitad de carga de datos de un proyecto de reemplazo típico, los patrones de exportación de base de datos a libro de trabajo cubren tanto el componente como el bucle escrito a mano, y en cuanto el número de filas alcanza seis cifras, las técnicas de rendimiento con libros de trabajo grandes se convierten en la diferencia entre minutos y segundos. Los informes construidos a partir de diseños mantenidos por un diseñador se cubren en el recorrido de generación de informes basados en plantilla
HotXLS se distribuye como código fuente Object Pascal para Delphi y C++Builder; las ediciones, la licencia, y la referencia completa de la API están en la página de producto de HotXLS Delphi Component