"""Minimal dependency-free .xlsx reader (xlsx = zip of XML).
We only need: shared strings, cell text by reference, and image/drawing anchors.
Using stdlib zipfile + regex keeps this migration tooling free of openpyxl so it
runs anywhere Python 3 is present.
"""
import zipfile
import re
import html
import os
_T = re.compile(r"]*>(.*?)", re.S)
_SI = re.compile(r"(.*?)", re.S)
_CELL = re.compile(
r']*?)>(?:]*>.*?)?(?:(.*?)|(.*?))?', re.S)
_ATTR = re.compile(r'(\w+)="([^"]*)"')
def col_to_num(col):
n = 0
for ch in col:
n = n * 26 + (ord(ch) - 64)
return n
def num_to_col(n):
s = ""
while n > 0:
n, r = divmod(n - 1, 26)
s = chr(65 + r) + s
return s
def shared_strings(z):
try:
raw = z.read("xl/sharedStrings.xml").decode("utf-8")
except KeyError:
return []
return [html.unescape("".join(_T.findall(si))).strip() for si in _SI.findall(raw)]
def sheet_paths(z):
"""Return worksheet xml paths in workbook order (best effort)."""
paths = sorted(n for n in z.namelist()
if re.match(r"xl/worksheets/sheet\d+\.xml$", n))
return paths
def read_cells(z, sheet_path, ss=None):
"""Return {(colnum, row): text} for a worksheet."""
if ss is None:
ss = shared_strings(z)
raw = z.read(sheet_path).decode("utf-8")
cells = {}
for attrs, v, istr in _CELL.findall(raw):
a = dict(_ATTR.findall(attrs))
ref = a.get("r")
if not ref:
continue
m = re.match(r"([A-Z]+)(\d+)", ref)
if not m:
continue
col, row = m.group(1), int(m.group(2))
typ = a.get("t")
if typ == "s" and v != "":
try:
text = ss[int(v)]
except (ValueError, IndexError):
text = ""
elif typ == "inlineStr" and istr:
text = html.unescape("".join(_T.findall(istr)))
elif v != "":
text = html.unescape(v)
else:
continue
if text.strip():
cells[(col_to_num(col), row)] = text.strip()
return cells
def cell_fill_sex(z, sheet_path):
"""{(colnum, row): 'male' | 'female'} from the cell's box fill colour.
Breeder convention (Julian, 2026-06-06): a BLUE box = männlich (male), a WHITE box =
weiblich (female). In these Stammbaum templates every coloured box uses one solid theme
fill (Office accent5 = blue); unfilled/none cells render white. So: a cell whose style
uses a real solid fill -> 'male'; an unfilled (none/gray125) cell -> 'female'.
"""
try:
st = z.read("xl/styles.xml").decode("utf-8")
except KeyError:
return {}
fills = re.search(r"", st, re.S)
colored = set()
if fills:
for i, fb in enumerate(re.findall(r"(.*?)", fills.group(0), re.S)):
if 'patternType="solid"' in fb and re.search(r"", st, re.S)
idx2fill = {}
if xfs:
for i, xf in enumerate(re.findall(r"]*?)/?>", xfs.group(0))):
m = re.search(r'fillId="(\d+)"', xf)
idx2fill[i] = int(m.group(1)) if m else 0
raw = z.read(sheet_path).decode("utf-8")
out = {}
for m in re.finditer(r"]*?)>", raw):
a = dict(_ATTR.findall(m.group(1)))
ref = a.get("r")
if not ref:
continue
mm = re.match(r"([A-Z]+)(\d+)", ref)
if not mm:
continue
s = int(a.get("s", "0"))
out[(col_to_num(mm.group(1)), int(mm.group(2)))] = (
"male" if idx2fill.get(s, 0) in colored else "female")
return out
def header_row(cells):
"""Return {colnum: header_text} for the topmost row that has text."""
if not cells:
return {}
top = min(r for (_, r) in cells)
return {c: t for (c, r), t in cells.items() if r == top}
def image_anchors(z):
"""Yield (sheet_path, from_col, from_row, media_zip_path) for every image anchor.
drawing rels map rId -> media target; the worksheet rels map the drawing to a
sheet. We resolve sheet via worksheet _rels where possible, else attribute all
anchors to the single worksheet (these files are single-sheet charts).
"""
out = []
# sheet -> drawing
sheet_drawing = {}
for sp in sheet_paths(z):
rels = "xl/worksheets/_rels/" + os.path.basename(sp) + ".rels"
try:
r = z.read(rels).decode("utf-8")
except KeyError:
continue
for rid, tgt in re.findall(r'Id="([^"]+)"[^>]*Target="([^"]+)"', r):
if "drawing" in tgt:
dpath = os.path.normpath(os.path.join(
"xl/worksheets", tgt)).replace("\\", "/")
sheet_drawing[sp] = dpath
for sp, dpath in sheet_drawing.items():
try:
d = z.read(dpath).decode("utf-8")
except KeyError:
continue
drels = os.path.join(os.path.dirname(dpath), "_rels",
os.path.basename(dpath) + ".rels").replace("\\", "/")
relmap = {}
try:
rr = z.read(drels).decode("utf-8")
for rid, tgt in re.findall(r'Id="([^"]+)"[^>]*Target="([^"]+)"', rr):
relmap[rid] = os.path.normpath(os.path.join(
os.path.dirname(dpath), tgt)).replace("\\", "/")
except KeyError:
pass
for anc in re.findall(r"", d, re.S):
fm = re.search(
r".*?(\d+).*?(\d+)", anc, re.S)
emb = re.search(r'r:embed="([^"]+)"', anc)
if fm and emb:
media = relmap.get(emb.group(1))
if media:
# xdr col/row are 0-based; convert to 1-based to match cell refs
out.append((sp, int(fm.group(1)) + 1,
int(fm.group(2)) + 1, media))
return out