144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
xlsx_create.py — Create xlsx from JSON data using openpyxl.
|
|
|
|
Usage:
|
|
python3 xlsx_create.py --data data.json --out output.xlsx
|
|
python3 xlsx_create.py --data-inline '{"headers":["A","B"],"rows":[["1","2"]]}' --out output.xlsx
|
|
|
|
JSON format:
|
|
{
|
|
"title": "Sheet title (optional, used as sheet name)",
|
|
"headers": ["Col1", "Col2", ...],
|
|
"rows": [["val1", "val2", ...], ...],
|
|
"col_widths": [15, 20, ...] // optional
|
|
}
|
|
|
|
Exit codes: 0 success, 1 bad args, 2 missing dep
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
|
except ImportError:
|
|
print(json.dumps({"status": "error", "error": "openpyxl not installed", "hint": "pip install openpyxl"}))
|
|
sys.exit(2)
|
|
|
|
|
|
def create_xlsx(data: dict, out_path: str) -> dict:
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.title = data.get("title", "Sheet1")[:31]
|
|
|
|
headers = data.get("headers", [])
|
|
rows = data.get("rows", [])
|
|
col_widths = data.get("col_widths", [])
|
|
|
|
# Header row styling
|
|
header_font = Font(bold=True, color="FFFFFF")
|
|
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
|
header_align = Alignment(horizontal="center", vertical="center")
|
|
thin_border = Border(
|
|
left=Side(style="thin"),
|
|
right=Side(style="thin"),
|
|
top=Side(style="thin"),
|
|
bottom=Side(style="thin"),
|
|
)
|
|
|
|
# Write headers
|
|
for col_idx, header in enumerate(headers, 1):
|
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = header_align
|
|
cell.border = thin_border
|
|
|
|
# Write data rows
|
|
for row_idx, row_data in enumerate(rows, 2):
|
|
for col_idx, value in enumerate(row_data, 1):
|
|
cell = ws.cell(row=row_idx, column=col_idx, value=_auto_type(value))
|
|
cell.border = thin_border
|
|
cell.alignment = Alignment(vertical="center")
|
|
|
|
# Column widths
|
|
for col_idx, width in enumerate(col_widths, 1):
|
|
ws.column_dimensions[chr(64 + col_idx) if col_idx <= 26 else f"{chr(64 + (col_idx-1)//26)}{chr(65 + (col_idx-1)%26)}"].width = width
|
|
|
|
# Auto-width for columns without explicit width
|
|
if not col_widths:
|
|
for col_idx in range(1, len(headers) + 1):
|
|
max_len = len(str(headers[col_idx - 1])) if col_idx <= len(headers) else 8
|
|
for row_idx in range(2, min(len(rows) + 2, 52)):
|
|
if col_idx <= len(rows[row_idx - 2]):
|
|
cell_len = len(str(rows[row_idx - 2][col_idx - 1]))
|
|
max_len = max(max_len, cell_len)
|
|
col_letter = chr(64 + col_idx) if col_idx <= 26 else f"{chr(64 + (col_idx-1)//26)}{chr(65 + (col_idx-1)%26)}"
|
|
ws.column_dimensions[col_letter].width = min(max_len + 4, 50)
|
|
|
|
# Freeze header row
|
|
ws.freeze_panes = "A2"
|
|
|
|
# Auto-filter
|
|
if headers:
|
|
last_col = chr(64 + len(headers)) if len(headers) <= 26 else f"{chr(64 + (len(headers)-1)//26)}{chr(65 + (len(headers)-1)%26)}"
|
|
ws.auto_filter.ref = f"A1:{last_col}{len(rows) + 1}"
|
|
|
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
wb.save(out_path)
|
|
|
|
return {
|
|
"status": "ok",
|
|
"out": out_path,
|
|
"rows": len(rows),
|
|
"columns": len(headers),
|
|
"size_kb": os.path.getsize(out_path) // 1024,
|
|
}
|
|
|
|
|
|
def _auto_type(value):
|
|
if value is None:
|
|
return ""
|
|
if isinstance(value, (int, float)):
|
|
return value
|
|
s = str(value)
|
|
try:
|
|
return int(s)
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
return float(s)
|
|
except ValueError:
|
|
pass
|
|
return s
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Create xlsx from JSON data")
|
|
parser.add_argument("--data", help="Path to JSON data file")
|
|
parser.add_argument("--data-inline", help="Inline JSON data string")
|
|
parser.add_argument("--out", required=True, help="Output xlsx file path")
|
|
args = parser.parse_args()
|
|
|
|
if args.data and os.path.exists(args.data):
|
|
with open(args.data, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
elif args.data_inline:
|
|
data = json.loads(args.data_inline)
|
|
elif args.data and (args.data.strip().startswith("{") or args.data.strip().startswith("[")):
|
|
data = json.loads(args.data)
|
|
else:
|
|
print(json.dumps({"status": "error", "error": "No data provided. Use --data <file> or --data-inline '<json>'"}))
|
|
sys.exit(1)
|
|
|
|
result = create_xlsx(data, args.out)
|
|
print(json.dumps(result, ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|