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