Skip to content

Crear un Libro XLSX desde Cero

XlsxWriter construye un archivo Excel .xlsx completamente nuevo en memoria y lo escribe en disco o en un buffer de bytes — sin plantilla, sin instalación de Excel, sin automatización COM. Es la herramienta adecuada cuando necesita control total sobre hojas, celdas, estilos, combinaciones y anchos de columna, en lugar de trabajar a través del IR agnóstico de formato.

El escritor forma parte del mismo núcleo en Rust que extrae XLSX a 5,0 ms de media con un 100 % de tasa de éxito en archivos Office válidos, y está disponible en Python, Rust, Go, C# y JavaScript (Node nativo).

¿Cómo creo un archivo XLSX desde cero?

Construya un XlsxWriter, agregue una hoja, escriba celdas por (row, col) (ambos basados en 0) y luego llame a save. Los valores de celda pueden ser str, int, float, bool o None. El mismo patrón funciona en todos los bindings.

Rust

use office_oxide::xlsx::write::{XlsxWriter, CellData, CellStyle, NumberFormat};

fn main() -> office_oxide::Result<()> {
    let mut wb = XlsxWriter::new();
    let mut sheet = wb.add_sheet("Sales");

    // Bold header row
    let header = CellStyle::new().bold().background("D3D3D3");
    sheet.set_cell_styled(0, 0, CellData::String("Item".into()),   header.clone());
    sheet.set_cell_styled(0, 1, CellData::String("Amount".into()), header);

    // Data rows
    sheet.set_cell(1, 0, CellData::String("Widget".into()));
    sheet.set_cell(1, 1, CellData::Number(1500.0));
    sheet.set_cell(2, 0, CellData::String("Gadget".into()));
    sheet.set_cell(2, 1, CellData::Number(2400.0));

    // SUM formula with currency formatting (omit the leading '=')
    let currency = CellStyle::new().number_format(NumberFormat::Currency);
    sheet.set_cell_styled(3, 1, CellData::Formula("SUM(B2:B3)".into()), currency);

    // Merge a title banner across two columns
    sheet.merge_cells(4, 0, 1, 2);

    sheet.set_column_width(0, 20.0);
    sheet.set_column_width(1, 15.0);

    wb.save("sales.xlsx")?;
    Ok(())
}

Python

from office_oxide import XlsxWriter

wb = XlsxWriter()
sheet = wb.add_sheet("Sales")          # -> 0 (the sheet index)

# Header row — bold, light-grey fill
wb.set_cell_styled(sheet, 0, 0, "Item",   bold=True, bg_color="D3D3D3")
wb.set_cell_styled(sheet, 0, 1, "Amount", bold=True, bg_color="D3D3D3")

# Data rows: row, col are 0-based
wb.set_cell(sheet, 1, 0, "Widget")
wb.set_cell(sheet, 1, 1, 1500.0)
wb.set_cell(sheet, 2, 0, "Gadget")
wb.set_cell(sheet, 2, 1, 2400.0)

# Widen the columns (Excel character units)
wb.set_column_width(sheet, 0, 20.0)
wb.set_column_width(sheet, 1, 15.0)

wb.save("sales.xlsx")

JavaScript

import { writeFileSync } from 'node:fs';
import { XlsxWriter } from 'office-oxide';

const wb = new XlsxWriter();
const sheet = wb.addSheet('Sales'); // 0-based index

// Header row — bold, grey fill (6-char hex string or null)
wb.setCellStyled(sheet, 0, 0, 'Item', true, 'D3D3D3');
wb.setCellStyled(sheet, 0, 1, 'Amount', true, 'D3D3D3');

// Data rows: value may be null, string, number, or boolean
wb.setCell(sheet, 1, 0, 'Widget');
wb.setCell(sheet, 1, 1, 1500.0);
wb.setCell(sheet, 2, 0, 'Gadget');
wb.setCell(sheet, 2, 1, 2400.0);

wb.mergeCells(sheet, 3, 0, 1, 2);   // title banner: 1 row x 2 cols
wb.setColumnWidth(sheet, 0, 20.0);
wb.setColumnWidth(sheet, 1, 15.0);

wb.save('sales.xlsx');

// Or export to a Buffer:
const data = wb.toBytes();
writeFileSync('sales-copy.xlsx', data);

wb.close(); // release the native handle

Go

package main

import (
	"os"

	officeoxide "github.com/yfedoseev/office_oxide/go"
)

func main() {
	wb := officeoxide.NewXlsxWriter()
	defer wb.Close()

	sheet := wb.AddSheet("Sales") // uint32, 0-based index

	// Header row — bold with a grey fill
	wb.SetCellStyled(sheet, 0, 0, "Item", true, "D3D3D3")
	wb.SetCellStyled(sheet, 0, 1, "Amount", true, "D3D3D3")

	// Data rows: value may be nil, string, float64, int, or bool
	wb.SetCell(sheet, 1, 0, "Widget")
	wb.SetCell(sheet, 1, 1, 1500.0)
	wb.SetCell(sheet, 2, 0, "Gadget")
	wb.SetCell(sheet, 2, 1, 2400.0)

	wb.MergeCells(sheet, 3, 0, 1, 2) // title banner: 1 row x 2 cols
	wb.SetColumnWidth(sheet, 0, 20.0)
	wb.SetColumnWidth(sheet, 1, 15.0)

	if err := wb.Save("sales.xlsx"); err != nil {
		panic(err)
	}

	// Or export to bytes:
	data, err := wb.ToBytes()
	if err != nil {
		panic(err)
	}
	_ = os.WriteFile("sales-copy.xlsx", data, 0o644)
}

C#

using OfficeOxide;

using var wb = new XlsxWriter();
uint sheet = wb.AddSheet("Sales"); // 0-based index

// Header row — bold, grey fill (6-char hex, no '#')
wb.SetCellStyled(sheet, 0, 0, "Item", bold: true, bgColor: "D3D3D3");
wb.SetCellStyled(sheet, 0, 1, "Amount", bold: true, bgColor: "D3D3D3");

// Data rows: value may be null, string, double, int, long, or bool
wb.SetCell(sheet, 1, 0, "Widget");
wb.SetCell(sheet, 1, 1, 1500.0);
wb.SetCell(sheet, 2, 0, "Gadget");
wb.SetCell(sheet, 2, 1, 2400.0);

wb.MergeCells(sheet, 3, 0, 1, 2);   // title banner: 1 row x 2 cols
wb.SetColumnWidth(sheet, 0, 20.0);
wb.SetColumnWidth(sheet, 1, 15.0);

wb.Save("sales.xlsx");

// Or export to a byte[]:
byte[] data = wb.ToBytes();
File.WriteAllBytes("sales-copy.xlsx", data);

C

OfficeXlsxWriterHandle *w = office_xlsx_writer_new();
uint32_t s = office_xlsx_writer_add_sheet(w, "Sales");   /* 0-based index */

/* value_type in the writer: EMPTY=0, STRING=1, NUMBER=2 (no BOOLEAN) */
/* header row — bold + 6-char hex bg ("D3D3D3") or NULL */
office_xlsx_sheet_set_cell_styled(w, s, 0, 0, OFFICE_CELL_STRING, "Item",   0.0, true, "D3D3D3");
office_xlsx_sheet_set_cell_styled(w, s, 0, 1, OFFICE_CELL_STRING, "Amount", 0.0, true, "D3D3D3");

/* data rows */
office_xlsx_sheet_set_cell(w, s, 1, 0, OFFICE_CELL_STRING, "Widget", 0.0);
office_xlsx_sheet_set_cell(w, s, 1, 1, OFFICE_CELL_NUMBER, NULL,     1500.0);
office_xlsx_sheet_set_cell(w, s, 2, 0, OFFICE_CELL_STRING, "Gadget", 0.0);
office_xlsx_sheet_set_cell(w, s, 2, 1, OFFICE_CELL_NUMBER, NULL,     2400.0);

office_xlsx_sheet_merge_cells(w, s, 3, 0, 1, 2);     /* title banner: row_span/col_span >= 1 */
office_xlsx_sheet_set_column_width(w, s, 0, 20.0);   /* Excel char units */
office_xlsx_sheet_set_column_width(w, s, 1, 15.0);

int err = 0;
office_xlsx_writer_save(w, "sales.xlsx", &err);
/* or: uint8_t *b = office_xlsx_writer_to_bytes(w, &out_len, &err); ... office_oxide_free_bytes */
office_xlsx_writer_free(w);

Para pasar los bytes a una respuesta web o un almacén de objetos en lugar de escribirlos en disco, use to_bytes() (ejemplo en Python a continuación):

Python

data = wb.to_bytes()          # -> bytes (a complete .xlsx ZIP)
with open("sales.xlsx", "wb") as f:
    f.write(data)

Firmas de métodos (Python)

Método Firma
Construir XlsxWriter()
Agregar hoja add_sheet(name: str) -> int
Establecer celda set_cell(sheet: int, row: int, col: int, value: None|str|bool|int|float) -> None
Establecer celda con estilo set_cell_styled(sheet: int, row: int, col: int, value, bold: bool, bg_color: str|None = None) -> None
Combinar merge_cells(sheet: int, row: int, col: int, row_span: int, col_span: int) -> None
Ancho de columna set_column_width(sheet: int, col: int, width: float) -> None
Guardar save(path) -> None
Exportar to_bytes() -> bytes

add_sheet devuelve el índice basado en 0 de la nueva hoja — páselo como argumento sheet a todas las demás llamadas. Las filas y columnas son 0-basadas, por lo que la celda A1 es (row=0, col=0) y B1 es (row=0, col=1).

¿Cómo combino celdas y creo un banner de título?

merge_cells(sheet, row, col, row_span, col_span) combina un rango rectangular anclado en (row, col). Ambos spans deben ser >= 1; el valor combinado proviene de la celda ancla.

Python

from office_oxide import XlsxWriter

wb = XlsxWriter()
s = wb.add_sheet("Report")

# A title that spans columns A through C of the first row
wb.set_cell_styled(s, 0, 0, "Q3 Revenue Report", bold=True, bg_color="FFE699")
wb.merge_cells(s, 0, 0, 1, 3)          # 1 row tall, 3 columns wide

# Sub-header
wb.set_cell_styled(s, 1, 0, "Region", bold=True)
wb.set_cell_styled(s, 1, 1, "Q3",     bold=True)
wb.set_cell_styled(s, 1, 2, "QoQ %",  bold=True)

wb.set_cell(s, 2, 0, "NA")
wb.set_cell(s, 2, 1, 1_200_000)
wb.set_cell(s, 2, 2, 0.18)

wb.set_column_width(s, 0, 18.0)
wb.save("report.xlsx")

Firmas por lenguaje

Rust — el crate de Rust expone un builder más completo. add_sheet devuelve un handle SheetData prestado, y las celdas reciben un valor CellData (String, Number, Boolean, Formula o Empty) más un CellStyle opcional. CellStyle es un builder que admite bold(), italic(), background(), number_format(), align() y más. Firmas clave (en SheetData):

pub fn set_cell(&mut self, row: usize, col: usize, value: CellData) -> &mut Self
pub fn set_cell_styled(&mut self, row: usize, col: usize, value: CellData, style: CellStyle) -> &mut Self
pub fn merge_cells(&mut self, row: usize, col: usize, row_span: usize, col_span: usize) -> &mut Self
pub fn set_column_width(&mut self, col: usize, width: f64) -> &mut Self

y en XlsxWriter: add_sheet(name: &str) -> SheetData<'_>, save(path) -> Result<()> y write_to<W: Write + Seek>(writer) -> Result<()> para salida en memoria. También está disponible un espejo basado en índices (sheet_set_cell, sheet_set_cell_styled, sheet_merge_cells, sheet_set_column_width) para quienes prefieren la misma forma plana que usan los otros bindings.

PythonXlsxWriter(), add_sheet(name: str) -> int, set_cell(sheet: int, row: int, col: int, value: None|str|bool|int|float) -> None, set_cell_styled(sheet: int, row: int, col: int, value, bold: bool, bg_color: str|None = None) -> None, merge_cells(sheet: int, row: int, col: int, row_span: int, col_span: int) -> None, set_column_width(sheet: int, col: int, width: float) -> None, save(path) -> None, to_bytes() -> bytes.

JavaScriptaddSheet(name), setCell(sheet, row, col, value), setCellStyled(sheet, row, col, value, bold, bgColor = null), mergeCells(sheet, row, col, rowSpan, colSpan), setColumnWidth(sheet, col, width), save(path), toBytes(), close().

GoNewXlsxWriter() *XlsxWriter, AddSheet(name string) uint32, SetCell(sheet, row, col uint32, value any), SetCellStyled(sheet, row, col uint32, value any, bold bool, bgColor string), MergeCells(sheet, row, col, rowSpan, colSpan uint32), SetColumnWidth(sheet, col uint32, width float64), Save(path string) error, ToBytes() ([]byte, error). Pase "" como bgColor para sin relleno.

C#AddSheet(string name) -> uint, SetCell(uint sheet, uint row, uint col, object? value), SetCellStyled(uint sheet, uint row, uint col, object? value, bool bold, string? bgColor = null), MergeCells(uint sheet, uint row, uint col, uint rowSpan, uint colSpan), SetColumnWidth(uint sheet, uint col, double width), Save(string path), ToBytes() -> byte[]. XlsxWriter implementa IDisposable.

Coffice_xlsx_writer_new() -> OfficeXlsxWriterHandle*, office_xlsx_writer_add_sheet(w, name) -> uint32_t, office_xlsx_sheet_set_cell(w, sheet, row, col, value_type, value_str, value_num), office_xlsx_sheet_set_cell_styled(w, sheet, row, col, value_type, value_str, value_num, bold, bg_color), office_xlsx_sheet_merge_cells(w, sheet, row, col, row_span, col_span), office_xlsx_sheet_set_column_width(w, sheet, col, width), office_xlsx_writer_save(w, path, &err), office_xlsx_writer_to_bytes(w, &out_len, &err), office_xlsx_writer_free(w). value_type es OFFICE_CELL_EMPTY (0), OFFICE_CELL_STRING (1) o OFFICE_CELL_NUMBER (2) — el writer no tiene tipo de celda booleana.

Nota: el XLSX-writer-from-scratch está disponible en los bindings nativos (Rust, Python, JavaScript/Node, Go, C#) y en el C ABI. La compilación WASM para navegador es de solo lectura — no tiene clases de writer — así que construya los libros con uno de los anteriores y transmita los bytes.

Referencia de estilos

En los bindings Python, Go, C#, JavaScript y C, el setter con estilo expone dos opciones:

  • bold — un booleano que aplica negrita a la celda.
  • bg_color / bgColor — una cadena hexadecimal RGB de 6 caracteres sin # inicial (por ejemplo, "D3D3D3" para gris claro, "FFE699" para ámbar). Pase None / "" / null para sin relleno.

El builder CellStyle de Rust es la superficie completa — cursiva, subrayado, color de fuente, tamaño de fuente, nombre de fuente, formatos numéricos (General, Integer, Decimal2, Currency, Percent, Percent2, Date, DateTime), alineación horizontal y ajuste de texto. Los demás bindings exponen directamente las dos opciones más comunes; para un estilo más detallado desde esos lenguajes, construya a través del IR o realice un postprocesamiento.

Preguntas frecuentes

¿Las filas y columnas son 0-basadas o 1-basadas? Tanto row como col son enteros basados en 0. La celda A1 es (row=0, col=0), B1 es (row=0, col=1) y A2 es (row=1, col=0). El índice de hoja que devuelve add_sheet también es 0-basado.

¿Necesito tener instalado Excel o algún runtime de Microsoft? No. XlsxWriter genera un .xlsx ZIP OOXML válido completamente en Rust. No hay automatización COM, JVM ni dependencias del sistema — el mismo motor lee XLSX a 5,0 ms de media con un 100 % de tasa de éxito en archivos Office válidos.

¿Cómo obtengo el archivo como bytes en lugar de escribirlo en disco? Llame a to_bytes() (Python/C#/JS) o ToBytes() (Go), que devuelve un buffer completo de .xlsx. En Rust use write_to(writer) con cualquier objetivo Write + Seek, como un Cursor<Vec<u8>>. Esto es ideal para respuestas HTTP y cargas a almacenes de objetos.

¿Puedo escribir fórmulas? Sí, en Rust mediante CellData::Formula("SUM(B2:B3)") (omita el = inicial). Los bindings planos (Python/Go/C#/JS) actualmente exponen valores de celda de cadena, número, booleano y vacío a través de set_cell.

¿Cómo agrego una segunda hoja de cálculo? Llame a add_sheet de nuevo — cada llamada agrega una hoja y devuelve el siguiente índice 0-basado. Pase ese índice como el argumento sheet en las llamadas posteriores a set_cell / merge_cells / set_column_width.

Véase también