"""XLSX writer — pure Python, zero dependencies. Builds an OOXML .xlsx file using ``zipfile`` + ``xml.etree.ElementTree``. """ import zipfile import xml.etree.ElementTree as ET from io import BytesIO from typing import Optional from utils import col_to_letter, rc_to_cell_ref def write_xlsx(data: dict[str, str], output_path: str): """Write a cell map to an .xlsx file. Parameters ---------- data : dict[str, str] Mapping of Excel cell references to values. Example: {'A1': '封装信息', 'A2': 'PinName1', 'B2': '1'} output_path : str Path for the output .xlsx file. """ writer = XLSXWriter() writer.write(data, output_path) class XLSXWriter: """Build an OOXML .xlsx file from a cell map.""" def __init__(self): self._strings: list[str] = [] self._string_index: dict[str, int] = {} def write(self, data: dict[str, str], output_path: str): """Write *data* to *output_path* as an .xlsx file.""" # Collect all unique strings for the shared strings table for value in data.values(): self._add_string(value) with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf: zf.writestr('[Content_Types].xml', self._content_types_xml()) zf.writestr('_rels/.rels', self._rels_xml()) zf.writestr('xl/workbook.xml', self._workbook_xml()) zf.writestr('xl/_rels/workbook.xml.rels', self._workbook_rels_xml()) zf.writestr('xl/sharedStrings.xml', self._shared_strings_xml()) zf.writestr('xl/worksheets/sheet1.xml', self._sheet_xml(data)) def _add_string(self, s: str) -> int: """Add a string to the SST and return its index.""" if s in self._string_index: return self._string_index[s] idx = len(self._strings) self._strings.append(s) self._string_index[s] = idx return idx # ── XML templates ─────────────────────────────────────────────── def _content_types_xml(self) -> str: return ''' ''' def _rels_xml(self) -> str: return ''' ''' def _workbook_xml(self) -> str: return ''' ''' def _workbook_rels_xml(self) -> str: return ''' ''' def _shared_strings_xml(self) -> str: parts = [''] parts.append(f'') for s in self._strings: # Escape XML special characters escaped = s.replace('&', '&').replace('<', '<').replace('>', '>') parts.append(f' {escaped}') parts.append('') return '\n'.join(parts) def _sheet_xml(self, data: dict[str, str]) -> str: """Build sheet1.xml from the cell map. data keys are Excel cell references like 'A1', 'B2', etc. All values are treated as shared strings. """ # Determine dimensions max_row = 0 max_col = 0 for ref in data: row, col = self._ref_to_rc(ref) max_row = max(max_row, row) max_col = max(max_col, col) parts = [''] parts.append('') parts.append(f' ') parts.append(' ') # Group cells by row rows: dict[int, list[tuple[int, str]]] = {} for ref, value in data.items(): row, col = self._ref_to_rc(ref) if row not in rows: rows[row] = [] rows[row].append((col, value)) for row_num in sorted(rows): parts.append(f' ') for col, value in sorted(rows[row_num]): cell_ref = rc_to_cell_ref(row_num, col) si = self._add_string(value) parts.append(f' {si}') parts.append(' ') parts.append(' ') parts.append('') return '\n'.join(parts) @staticmethod def _ref_to_rc(ref: str) -> tuple[int, int]: """Convert cell reference to (row, col) 0-based.""" col_letters = [] row_digits = [] for ch in ref: if ch.isalpha(): col_letters.append(ch) else: row_digits.append(ch) col = 0 for ch in ''.join(col_letters).upper(): col = col * 26 + (ord(ch) - ord('A') + 1) col -= 1 row = int(''.join(row_digits)) - 1 return row, col