Excel files are everywhere — client reports, financial data, government datasets, legacy exports. But most APIs, databases, and data pipelines speak CSV or JSON. Converting between spreadsheet formats is a daily developer task, and it's trickier than it looks.
What is a spreadsheet format?
| Format | Extension | Structure | Best for |
|---|---|---|---|
| Excel | .xlsx, .xls |
Binary + XML (ZIP) | Rich formatting, multiple sheets, formulas |
| CSV | .csv |
Plain text, comma-separated | Databases, APIs, data pipelines |
| TSV | .tsv |
Plain text, tab-separated | Exports with commas in values |
| JSON | .json |
Structured text | APIs, web apps, NoSQL databases |
| ODS | .ods |
OpenDocument (LibreOffice) | Open-source office suites |
The right output depends on your target: use CSV for SQL imports, JSON for APIs, TSV when values contain commas.
Why you can't just rename the file
An .xlsx file is a ZIP archive containing XML, images, and metadata. Renaming it to .csv produces a corrupt file. You need a real parser that:
- Unzips the XLSX container
- Reads
xl/sharedStrings.xml(the string table) - Reads
xl/worksheets/sheet1.xml(the cell data) - Decodes cell types — strings, numbers, booleans, and date serials
- Serialises the result to your target format
This is why every serious language has a dedicated spreadsheet library.
Excel's date serial number trap
The biggest gotcha when converting Excel: dates are stored as numbers. Excel counts days from January 1, 1900 (serial 1). The date 2026-07-13 is stored as 46215, not as a string.
A naive converter will output 46215 instead of 2026-07-13. Every library below handles this — but you must tell it the expected output format.
There is also a Lotus 1-2-3 leap year bug: Excel incorrectly treats 1900 as a leap year, so serial 60 maps to the non-existent February 29, 1900. Libraries account for this offset.
JavaScript — SheetJS (xlsx)
SheetJS (xlsx on npm) reads and writes every spreadsheet format. It works in Node.js and the browser.
import * as XLSX from "xlsx";
import { readFileSync, writeFileSync } from "fs";
// Excel → CSV
function xlsxToCsv(inputPath, outputPath, sheetIndex = 0) {
const workbook = XLSX.readFile(inputPath);
const sheetName = workbook.SheetNames[sheetIndex];
const sheet = workbook.Sheets[sheetName];
const csv = XLSX.utils.sheet_to_csv(sheet);
writeFileSync(outputPath, csv, "utf8");
}
// Excel → JSON
function xlsxToJson(inputPath, sheetIndex = 0) {
const workbook = XLSX.readFile(inputPath);
const sheetName = workbook.SheetNames[sheetIndex];
const sheet = workbook.Sheets[sheetName];
// header: 1 returns an array of arrays; omit for array of objects (first row = keys)
return XLSX.utils.sheet_to_json(sheet, { defval: "" });
}
// CSV → Excel
function csvToXlsx(csvPath, outputPath) {
const csv = readFileSync(csvPath, "utf8");
const workbook = XLSX.read(csv, { type: "string" });
XLSX.writeFile(workbook, outputPath);
}
// JSON → Excel
function jsonToXlsx(data, outputPath, sheetName = "Sheet1") {
const workbook = XLSX.utils.book_new();
const sheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
XLSX.writeFile(workbook, outputPath);
}
For browser uploads, replace readFile with XLSX.read(arrayBuffer, { type: "array" }) using a FileReader.
Python — openpyxl and pandas
For simple Excel → CSV, openpyxl is lightweight. For complex transforms or large files, pandas is the standard choice.
import openpyxl
import csv
# Excel → CSV with openpyxl (no pandas needed)
def xlsx_to_csv(input_path: str, output_path: str, sheet_index: int = 0) -> None:
wb = openpyxl.load_workbook(input_path, data_only=True) # data_only=True reads values, not formulas
ws = wb.worksheets[sheet_index]
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
for row in ws.iter_rows(values_only=True):
writer.writerow(["" if cell is None else cell for cell in row])
# Excel → JSON with pandas (handles dates automatically)
import pandas as pd
import json
def xlsx_to_json(input_path: str, sheet_name: str = 0) -> list[dict]:
df = pd.read_excel(input_path, sheet_name=sheet_name)
# Convert datetime columns to ISO strings
for col in df.select_dtypes(include=["datetime64"]):
df[col] = df[col].dt.strftime("%Y-%m-%d")
return df.to_dict(orient="records")
# CSV → Excel with pandas
def csv_to_xlsx(csv_path: str, output_path: str) -> None:
df = pd.read_csv(csv_path)
df.to_excel(output_path, index=False)
# JSON → Excel
def json_to_xlsx(data: list[dict], output_path: str) -> None:
df = pd.DataFrame(data)
df.to_excel(output_path, index=False)
data_only=True in openpyxl is critical — without it you get the formula string (=SUM(A1:A10)) instead of the computed value.
Go — excelize
The excelize library is the standard for Go spreadsheet handling.
package main
import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"github.com/xuri/excelize/v2"
)
// Excel → CSV
func xlsxToCSV(inputPath, outputPath string, sheetName string) error {
f, err := excelize.OpenFile(inputPath)
if err != nil {
return fmt.Errorf("open xlsx: %w", err)
}
defer f.Close()
rows, err := f.GetRows(sheetName)
if err != nil {
return fmt.Errorf("get rows: %w", err)
}
out, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("create csv: %w", err)
}
defer out.Close()
w := csv.NewWriter(out)
return w.WriteAll(rows)
}
// Excel → JSON (array of objects using first row as keys)
func xlsxToJSON(inputPath, sheetName string) ([]map[string]string, error) {
f, err := excelize.OpenFile(inputPath)
if err != nil {
return nil, err
}
defer f.Close()
rows, err := f.GetRows(sheetName)
if err != nil || len(rows) == 0 {
return nil, err
}
headers := rows[0]
result := make([]map[string]string, 0, len(rows)-1)
for _, row := range rows[1:] {
obj := make(map[string]string, len(headers))
for i, h := range headers {
if i < len(row) {
obj[h] = row[i]
} else {
obj[h] = ""
}
}
result = append(result, obj)
}
return result, nil
}
func main() {
// Example usage
if err := xlsxToCSV("report.xlsx", "output.csv", "Sheet1"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
data, _ := xlsxToJSON("report.xlsx", "Sheet1")
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(data)
}
PHP — PhpSpreadsheet
PhpSpreadsheet is the successor to PHPExcel and supports all major formats.
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Csv as CsvWriter;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
// Excel → CSV
function xlsxToCsv(string $inputPath, string $outputPath): void {
$spreadsheet = IOFactory::load($inputPath);
$writer = new CsvWriter($spreadsheet);
$writer->setSheetIndex(0); // which sheet to export
$writer->setDelimiter(',');
$writer->setEnclosure('"');
$writer->setLineEnding("\n");
$writer->save($outputPath);
}
// Excel → JSON (array of associative arrays)
function xlsxToJson(string $inputPath): array {
$spreadsheet = IOFactory::load($inputPath);
$sheet = $spreadsheet->getActiveSheet();
$data = $sheet->toArray(null, true, true, false); // formatted values, calculate formulas
if (empty($data)) return [];
$headers = array_shift($data);
$result = [];
foreach ($data as $row) {
$result[] = array_combine($headers, $row);
}
return $result;
}
// CSV → Excel
function csvToXlsx(string $csvPath, string $outputPath): void {
$reader = IOFactory::createReader('Csv');
$spreadsheet = $reader->load($csvPath);
$writer = new XlsxWriter($spreadsheet);
$writer->save($outputPath);
}
Quick reference
| Conversion | JS (SheetJS) | Python | Go | PHP |
|---|---|---|---|---|
| XLSX → CSV | sheet_to_csv() |
openpyxl + csv.writer |
excelize.GetRows + csv.NewWriter |
CsvWriter |
| XLSX → JSON | sheet_to_json() |
pd.read_excel().to_dict() |
excelize.GetRows → map |
toArray() + array_combine() |
| CSV → XLSX | XLSX.read(str) |
pd.read_csv().to_excel() |
excelize.SetSheetRow |
Csv reader + XlsxWriter |
| JSON → XLSX | json_to_sheet() |
pd.DataFrame().to_excel() |
excelize.SetSheetRow |
fromArray() + XlsxWriter |
Common pitfalls
1. Formulas instead of values
If you see =SUM(A1:A10) in your output, you're reading the formula string, not the computed value. Fix:
- openpyxl: pass
data_only=Truetoload_workbook - PhpSpreadsheet: pass
trueas the second argument totoArray() - SheetJS: formulas are evaluated by default in most read modes
2. Multiple sheets — which one do you want?
XLSX files often have several sheets. Always specify the sheet by index or name. If you need all sheets, loop over workbook.SheetNames (SheetJS) or wb.worksheets (openpyxl).
3. Encoding issues (Windows legacy files)
Older .xls files (Excel 97–2003) sometimes use Windows-1252 encoding. If you see garbled characters, check the codepage option in your library or convert to XLSX first using Excel or LibreOffice.
4. Merged cells produce None/undefined in the merged range
Merged cells only store a value in the top-left cell. All other cells in the merge are empty. If you need the value in every row, unmerge cells before exporting, or handle None values explicitly.
5. Date columns become floats
Excel date serials are floating-point numbers. If 45486 appears where you expect 2024-06-01, your library isn't detecting the cell format as a date. Check for a parse_dates option (pandas) or date cell type handling in your library.
6. Trailing empty rows/columns
Excel files often have formatting applied to empty cells, which causes libraries to include blank rows at the end of the exported data. Filter rows where all values are empty or None before writing the output.
FAQ
What is the difference between .xls and .xlsx?
.xls is the legacy binary format (Excel 97–2003, up to 65,536 rows). .xlsx is the modern XML-based Open XML format (Excel 2007+, up to 1,048,576 rows). Use .xlsx for everything new; most libraries still read .xls for backwards compatibility.
Can I convert without installing anything? Yes — use an online spreadsheet converter that runs entirely in your browser. Your file never leaves your computer.
How do I convert only one sheet from a multi-sheet workbook?
Specify the sheet by name or index: pd.read_excel(path, sheet_name="Sales") in Python, workbook.Sheets["Sales"] in SheetJS, or f.GetRows("Sales") in Go.
Does CSV preserve formulas? No. CSV stores only plain values. Formulas, formatting, charts, and images are all lost. If you need to preserve them, keep the XLSX original.
What about very large files (millions of rows)?
Standard in-memory libraries load the entire file at once. For large files, use streaming readers: SheetJS supports stream.to_csv, pandas supports chunksize, and go-excel has a row iterator. For truly massive files (>500 MB), consider converting to Parquet or a database instead.
How do I handle special characters and non-ASCII text?
Always use UTF-8 when writing CSV. Most modern libraries default to UTF-8. For Excel files to open correctly in Windows Excel, you may need to add a UTF-8 BOM: write \xEF\xBB\xBF before the CSV content (or use encoding="utf-8-sig" in Python).