161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Read an .xlsx workbook: inventory, JSON/CSV dumps, formula listing.
|
|
|
|
Modes (pick one):
|
|
--sheets JSON inventory: sheet names, dimensions, row/col counts
|
|
--json dump one sheet's rows as a JSON array of arrays
|
|
--csv dump one sheet as CSV to stdout or --out
|
|
--formulas JSON list of formula cells {"cell", "formula", "cached"}
|
|
--notes JSON list of cell notes/comments across sheets
|
|
--names JSON map of workbook defined names
|
|
|
|
Options:
|
|
--sheet NAME sheet to dump (default: active sheet)
|
|
--data-only load cached formula RESULTS instead of formula strings.
|
|
Caveat: openpyxl never computes formulas; cached values
|
|
exist only if the file was last saved by Excel/LibreOffice.
|
|
--encoding ENC encoding for --csv --out files (default utf-8)
|
|
--out PATH write --csv output to a file instead of stdout
|
|
|
|
Usage:
|
|
xlsx_read.py book.xlsx --sheets
|
|
xlsx_read.py book.xlsx --json --sheet Data
|
|
xlsx_read.py book.xlsx --csv --sheet Data --out data.csv
|
|
xlsx_read.py book.xlsx --formulas
|
|
xlsx_read.py book.xlsx --notes
|
|
xlsx_read.py book.xlsx --names
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import sys
|
|
from datetime import date, datetime, time
|
|
|
|
from openpyxl import load_workbook
|
|
|
|
|
|
def jsonable(value):
|
|
if isinstance(value, (datetime, date, time)):
|
|
return value.isoformat()
|
|
return value
|
|
|
|
|
|
def sheet_rows(ws):
|
|
return [[jsonable(c) for c in row] for row in ws.iter_rows(values_only=True)]
|
|
|
|
|
|
def cmd_sheets(wb):
|
|
info = []
|
|
for ws in wb.worksheets:
|
|
info.append({
|
|
"name": ws.title,
|
|
"dimensions": ws.dimensions,
|
|
"max_row": ws.max_row,
|
|
"max_col": ws.max_column,
|
|
"merged": [str(r) for r in ws.merged_cells.ranges],
|
|
"charts": len(getattr(ws, "_charts", [])),
|
|
"freeze_panes": ws.freeze_panes,
|
|
"autofilter": ws.auto_filter.ref,
|
|
"tables": {t.displayName: t.ref for t in ws.tables.values()},
|
|
"protected": bool(ws.protection.sheet),
|
|
})
|
|
names = {name: dn.attr_text for name, dn in wb.defined_names.items()}
|
|
print(json.dumps({"sheets": info, "defined_names": names},
|
|
ensure_ascii=False, indent=2))
|
|
|
|
|
|
def cmd_notes(wb, sheet):
|
|
out = []
|
|
sheets = [sheet] if sheet else wb.sheetnames
|
|
for name in sheets:
|
|
for row in wb[name].iter_rows():
|
|
for cell in row:
|
|
if cell.comment is not None:
|
|
out.append({"sheet": name, "cell": cell.coordinate,
|
|
"text": cell.comment.text,
|
|
"author": cell.comment.author})
|
|
print(json.dumps({"notes": out}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
def cmd_names(wb):
|
|
names = {name: dn.attr_text for name, dn in wb.defined_names.items()}
|
|
print(json.dumps({"defined_names": names}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
def cmd_formulas(path, sheet):
|
|
wb_f = load_workbook(path, data_only=False)
|
|
wb_v = load_workbook(path, data_only=True)
|
|
out = []
|
|
sheets = [sheet] if sheet else wb_f.sheetnames
|
|
for name in sheets:
|
|
ws_f, ws_v = wb_f[name], wb_v[name]
|
|
for row in ws_f.iter_rows():
|
|
for cell in row:
|
|
if isinstance(cell.value, str) and cell.value.startswith("="):
|
|
out.append({
|
|
"sheet": name,
|
|
"cell": cell.coordinate,
|
|
"formula": cell.value,
|
|
"cached": jsonable(ws_v[cell.coordinate].value),
|
|
})
|
|
print(json.dumps({"formulas": out}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description="Read/inspect an .xlsx workbook.")
|
|
ap.add_argument("file", help="path to .xlsx file")
|
|
mode = ap.add_mutually_exclusive_group(required=True)
|
|
mode.add_argument("--sheets", action="store_true")
|
|
mode.add_argument("--json", action="store_true")
|
|
mode.add_argument("--csv", action="store_true")
|
|
mode.add_argument("--formulas", action="store_true")
|
|
mode.add_argument("--notes", action="store_true")
|
|
mode.add_argument("--names", action="store_true")
|
|
ap.add_argument("--sheet", help="sheet name (default: active)")
|
|
ap.add_argument("--data-only", action="store_true",
|
|
help="return cached formula results (see module docstring)")
|
|
ap.add_argument("--encoding", default="utf-8")
|
|
ap.add_argument("--out", help="output file for --csv")
|
|
args = ap.parse_args(argv)
|
|
|
|
if args.formulas:
|
|
cmd_formulas(args.file, args.sheet)
|
|
return 0
|
|
|
|
wb = load_workbook(args.file, data_only=args.data_only)
|
|
if args.sheets:
|
|
cmd_sheets(wb)
|
|
return 0
|
|
if args.notes:
|
|
cmd_notes(wb, args.sheet)
|
|
return 0
|
|
if args.names:
|
|
cmd_names(wb)
|
|
return 0
|
|
|
|
ws = wb[args.sheet] if args.sheet else wb.active
|
|
rows = sheet_rows(ws)
|
|
if args.json:
|
|
print(json.dumps({"sheet": ws.title, "rows": rows}, ensure_ascii=False))
|
|
else: # --csv
|
|
if args.out:
|
|
with open(args.out, "w", newline="", encoding=args.encoding) as fh:
|
|
csv.writer(fh).writerows(
|
|
[["" if v is None else v for v in r] for r in rows])
|
|
print(json.dumps({"ok": True, "out": args.out, "rows": len(rows)}))
|
|
else:
|
|
w = csv.writer(sys.stdout)
|
|
for r in rows:
|
|
w.writerow(["" if v is None else v for v in r])
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except Exception as exc: # noqa: BLE001
|
|
print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
|
|
sys.exit(1)
|