- Sauvegarde automatique du CSV avant chaque écriture (backups/, 30 versions) - Export Excel .xlsx sans dépendance (module excel.py) - Journal de scraping (scrape.log) + panneau et endpoints /api/logs - Note libre par prospect (colonne Notes, zone de texte) - Notification de relance in-app (statut Contacté/En discussion + délai configurable)
79 lines
3.7 KiB
Python
79 lines
3.7 KiB
Python
"""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'<c r="{_ref(col, ligne)}" t="inlineStr">'
|
|
f'<is><t xml:space="preserve">{texte}</t></is></c>')
|
|
|
|
|
|
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 = ['<row r="1">' + "".join(_cellule(c, 1, colonnes[c]) for c in range(len(colonnes))) + "</row>"]
|
|
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'<row r="{i}">{cellules}</row>')
|
|
|
|
feuille = (
|
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
|
f'<sheetData>{"".join(rangs)}</sheetData></worksheet>'
|
|
)
|
|
content_types = (
|
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
'<Default Extension="xml" ContentType="application/xml"/>'
|
|
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
|
|
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
|
|
'</Types>'
|
|
)
|
|
rels = (
|
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
|
|
'</Relationships>'
|
|
)
|
|
workbook = (
|
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
|
|
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
|
f'<sheets><sheet name="{escape(nom_feuille[:31])}" sheetId="1" r:id="rId1"/></sheets></workbook>'
|
|
)
|
|
workbook_rels = (
|
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
|
|
'</Relationships>'
|
|
)
|
|
|
|
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()
|