web-fetcher/build/lib/web_fetcher/xlsx.py
tegwick 49de8d15b6
All checks were successful
Tests / unittest-py310 (push) Successful in 9s
Tests / unittest-py313 (push) Successful in 13s
WEBFETCH-WP-0003: Forgejo CI, org licence, inbox acks, progress
- .forgejo/workflows/test.yaml: offline tests on Python 3.10 and 3.13
- LICENSE: Target Revenue Source License V1C1 (org-wide preliminary licence)
- pyproject: license field; AGENTS/SCOPE: CI and licence facts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 153021@bnt-lap001
Assistant-Session: 46917be4-6400-4106-b8b7-61953a0caf24
2026-10-19 14:27:11 +02:00

53 lines
2 KiB
Python

"""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