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