"""Génération d'un classeur .xlsx minimal, sans dépendance. Un fichier .xlsx est une archive ZIP de fichiers XML. On utilise des chaînes « inline » (t="inlineStr") pour éviter la table des chaînes partagées : le résultat s'ouvre dans Excel, Numbers et LibreOffice. Bibliothèque standard uniquement. """ import io import zipfile from xml.sax.saxutils import escape def _ref(col, ligne): """Référence de cellule façon tableur : (0, 1) -> « A1 », (27, 3) -> « AB3 ».""" lettres, n = "", col while True: n, reste = divmod(n, 26) lettres = chr(65 + reste) + lettres if n == 0: break n -= 1 return f"{lettres}{ligne}" def _cellule(col, ligne, valeur): texte = escape("" if valeur is None else str(valeur)) return (f'' f'{texte}') def construire_xlsx(colonnes, lignes, nom_feuille="Prospects"): """Octets d'un classeur .xlsx : une feuille avec en-tête puis une ligne par dict.""" rangs = ['' + "".join(_cellule(c, 1, colonnes[c]) for c in range(len(colonnes))) + ""] for i, ligne in enumerate(lignes, start=2): cellules = "".join(_cellule(c, i, ligne.get(colonnes[c], "")) for c in range(len(colonnes))) rangs.append(f'{cellules}') feuille = ( '' '' f'{"".join(rangs)}' ) content_types = ( '' '' '' '' '' '' '' ) rels = ( '' '' '' '' ) workbook = ( '' '' f'' ) workbook_rels = ( '' '' '' '' ) tampon = io.BytesIO() with zipfile.ZipFile(tampon, "w", zipfile.ZIP_DEFLATED) as z: z.writestr("[Content_Types].xml", content_types) z.writestr("_rels/.rels", rels) z.writestr("xl/workbook.xml", workbook) z.writestr("xl/_rels/workbook.xml.rels", workbook_rels) z.writestr("xl/worksheets/sheet1.xml", feuille) return tampon.getvalue()