118 lines
4.7 KiB
Python
118 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Original XLSX helper for inspection, error scans, and CSV conversion."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from openpyxl import Workbook, load_workbook
|
|
from openpyxl.styles import Font, PatternFill
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
|
|
ERROR_TOKENS = ("#REF!", "#DIV/0!", "#VALUE!", "#NAME?", "#N/A", "#NUM!", "#NULL!")
|
|
|
|
|
|
def inspect(path: Path) -> None:
|
|
workbook = load_workbook(path, data_only=False, read_only=False)
|
|
sheets = []
|
|
for sheet in workbook.worksheets:
|
|
formula_count = 0
|
|
nonempty = 0
|
|
for row in sheet.iter_rows():
|
|
for cell in row:
|
|
if cell.value is not None:
|
|
nonempty += 1
|
|
if isinstance(cell.value, str) and cell.value.startswith("="):
|
|
formula_count += 1
|
|
sheets.append(
|
|
{
|
|
"name": sheet.title,
|
|
"state": sheet.sheet_state,
|
|
"max_row": sheet.max_row,
|
|
"max_column": sheet.max_column,
|
|
"nonempty_cells": nonempty,
|
|
"formulas": formula_count,
|
|
"merged_ranges": len(sheet.merged_cells.ranges),
|
|
"charts": len(sheet._charts),
|
|
"freeze_panes": str(sheet.freeze_panes) if sheet.freeze_panes else None,
|
|
}
|
|
)
|
|
print(json.dumps({"file": str(path.resolve()), "sheets": sheets}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
def formula_scan(path: Path) -> None:
|
|
formula_book = load_workbook(path, data_only=False, read_only=False)
|
|
value_book = load_workbook(path, data_only=True, read_only=False)
|
|
findings = []
|
|
for formula_sheet in formula_book.worksheets:
|
|
value_sheet = value_book[formula_sheet.title]
|
|
for row in formula_sheet.iter_rows():
|
|
for cell in row:
|
|
formula = cell.value
|
|
cached = value_sheet[cell.coordinate].value
|
|
if isinstance(formula, str) and formula.startswith("="):
|
|
formula_upper = formula.upper()
|
|
if any(token in formula_upper for token in ERROR_TOKENS):
|
|
findings.append({"sheet": formula_sheet.title, "cell": cell.coordinate, "kind": "formula_text", "value": formula})
|
|
if isinstance(cached, str) and cached.upper() in ERROR_TOKENS:
|
|
findings.append({"sheet": formula_sheet.title, "cell": cell.coordinate, "kind": "cached_error", "value": cached})
|
|
print(json.dumps({"file": str(path.resolve()), "errors": findings, "count": len(findings)}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
def csv_to_xlsx(source: Path, output: Path, delimiter: str | None) -> None:
|
|
chosen = delimiter
|
|
if chosen is None:
|
|
chosen = "\t" if source.suffix.lower() == ".tsv" else ","
|
|
workbook = Workbook()
|
|
sheet = workbook.active
|
|
sheet.title = "Data"
|
|
with source.open("r", encoding="utf-8-sig", newline="") as handle:
|
|
reader = csv.reader(handle, delimiter=chosen)
|
|
for row_index, row in enumerate(reader, start=1):
|
|
for column_index, value in enumerate(row, start=1):
|
|
sheet.cell(row=row_index, column=column_index, value=value)
|
|
if sheet.max_row >= 1:
|
|
for cell in sheet[1]:
|
|
cell.font = Font(bold=True, color="FFFFFF")
|
|
cell.fill = PatternFill("solid", fgColor="1F4E78")
|
|
sheet.freeze_panes = "A2"
|
|
sheet.auto_filter.ref = sheet.dimensions
|
|
for column in range(1, sheet.max_column + 1):
|
|
width = max((len(str(sheet.cell(row=row, column=column).value or "")) for row in range(1, min(sheet.max_row, 200) + 1)), default=8)
|
|
sheet.column_dimensions[get_column_letter(column)].width = min(max(width + 2, 10), 45)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
workbook.save(output)
|
|
inspect(output)
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser()
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
inspect_cmd = sub.add_parser("inspect")
|
|
inspect_cmd.add_argument("input", type=Path)
|
|
scan_cmd = sub.add_parser("formula-scan")
|
|
scan_cmd.add_argument("input", type=Path)
|
|
convert_cmd = sub.add_parser("csv-to-xlsx")
|
|
convert_cmd.add_argument("input", type=Path)
|
|
convert_cmd.add_argument("--output", required=True, type=Path)
|
|
convert_cmd.add_argument("--delimiter")
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
args = build_parser().parse_args()
|
|
if args.command == "inspect":
|
|
inspect(args.input)
|
|
elif args.command == "formula-scan":
|
|
formula_scan(args.input)
|
|
elif args.command == "csv-to-xlsx":
|
|
csv_to_xlsx(args.input, args.output, args.delimiter)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|