"""Minimal read-only XLSX reader (stdlib only): sheet -> list of rows of strings.""" from __future__ import annotations import io import re import zipfile import xml.etree.ElementTree as ET _NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main", "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"} _T = "{%s}t" % _NS["m"] def _col_index(ref: str) -> int: letters = re.match(r"[A-Z]+", ref).group(0) n = 0 for ch in letters: n = n * 26 + (ord(ch) - 64) return n - 1 def read_sheet(data: bytes, sheet: str | int = 0) -> list[list[str]]: z = zipfile.ZipFile(io.BytesIO(data)) shared: list[str] = [] if "xl/sharedStrings.xml" in z.namelist(): for si in ET.fromstring(z.read("xl/sharedStrings.xml")).findall("m:si", _NS): shared.append("".join(t.text or "" for t in si.iter(_T))) wb = ET.fromstring(z.read("xl/workbook.xml")) sheets = [s for s in wb.find("m:sheets", _NS)] if isinstance(sheet, str): idx = next(i for i, s in enumerate(sheets) if s.get("name") == sheet) else: idx = sheet rid = sheets[idx].get("{%s}id" % _NS["r"]) rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels")) target = next(r.get("Target") for r in rels if r.get("Id") == rid) path = "xl/" + target.lstrip("/").removeprefix("xl/") root = ET.fromstring(z.read(path)) rows: list[list[str]] = [] for r in root.find("m:sheetData", _NS).findall("m:row", _NS): cells: dict[int, str] = {} for c in r.findall("m:c", _NS): v = c.find("m:v", _NS) if v is None: inline = c.find("m:is", _NS) text = "".join(t.text or "" for t in inline.iter(_T)) if inline is not None else "" else: text = shared[int(v.text)] if c.get("t") == "s" else (v.text or "") cells[_col_index(c.get("r"))] = text width = max(cells) + 1 if cells else 0 rows.append([cells.get(i, "") for i in range(width)]) return rows