#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Fonctions utilitaires pour nettoyer le HTML et formater les dates. Version simplifiée et robuste: ignore les lignes problématiques. """ import re from datetime import datetime def clean_html(html_content, is_description=False, strategy="standard", preserve_links=False, preserve_images=False): """ Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques. Args: html_content (str): Contenu HTML à nettoyer is_description (bool): Indique si le contenu est une description de ticket strategy (str): Stratégie de nettoyage à utiliser ("standard", "strict", ou "raw") preserve_links (bool): Indique s'il faut préserver les liens preserve_images (bool): Indique s'il faut préserver les images Returns: str: Texte nettoyé """ if not html_content: return "*Contenu vide*" # 0. PRÉVENIR LES DOUBLONS - Détecter et supprimer les messages dupliqués # Cette étape permet d'éliminer les messages qui apparaissent en double # D'abord, nettoyer le HTML pour comparer les sections de texte réel cleaned_for_comparison = pre_clean_html(html_content) # Détection des doublons basée sur les premières lignes # Si le même début apparaît deux fois, ne garder que jusqu'à la première occurrence first_paragraph = "" for line in cleaned_for_comparison.split('\n'): if len(line.strip()) > 10: # Ignorer les lignes vides ou trop courtes first_paragraph = line.strip() break if first_paragraph and first_paragraph in cleaned_for_comparison[len(first_paragraph):]: # Le premier paragraphe apparaît deux fois - couper au début de la deuxième occurrence pos = cleaned_for_comparison.find(first_paragraph, len(first_paragraph)) if pos > 0: # Utiliser cette position pour couper le contenu original html_content = html_content[:pos].strip() # Diviser le contenu en sections potentielles (souvent séparées par des lignes vides doubles) sections = re.split(r'\n\s*\n\s*\n', html_content) # Si le contenu a plusieurs sections, ne garder que la première section significative if len(sections) > 1: # Rechercher la première section qui contient du texte significatif (non des en-têtes/métadonnées) significant_content = "" for section in sections: # Ignorer les sections très courtes ou qui ressemblent à des en-têtes if len(section.strip()) > 50 and not re.search(r'^(?:Subject|Date|From|To|Cc|Objet|De|À|Copie à):', section, re.IGNORECASE): significant_content = section break # Si on a trouvé une section significative, l'utiliser comme contenu if significant_content: html_content = significant_content # 1. CAS SPÉCIAUX - Traités en premier avec leurs propres règles # 1.1. Traitement spécifique pour les descriptions if is_description: # Suppression complète des balises HTML de base content = pre_clean_html(html_content) content = re.sub(r'\n\s*\n', '\n\n', content) return content.strip() # 1.2. Traitement des messages transférés avec un pattern spécifique if "\\-------- Message transféré --------" in html_content or "-------- Courriel original --------" in html_content: # Essayer d'extraire le contenu principal du message transféré match = re.search(r'(?:De|From|Copie à|Cc)\s*:.*?\n\s*\n(.*?)(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)', html_content, re.DOTALL | re.IGNORECASE) if match: return match.group(1).strip() else: # Essayer une autre approche si la première échoue match = re.search(r'Bonjour.*?(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)', html_content, re.DOTALL) if match: return match.group(0).strip() # 1.3. Traitement des notifications d'appel if "Notification d'appel" in html_content: match = re.search(r'(?:Sujet d\'appel:[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n)[^\n]*\n[^\n]*([^|]+)', html_content, re.DOTALL) if match: message_content = match.group(1).strip() # Construire un message formaté avec les informations essentielles infos = {} date_match = re.search(r'Date:.*?\|(.*?)(?:\n|$)', html_content) appelant_match = re.search(r'\*\*Appel de:\*\*.*?\|(.*?)(?:\n|$)', html_content) telephone_match = re.search(r'Téléphone principal:.*?\|(.*?)(?:\n|$)', html_content) mobile_match = re.search(r'Mobile:.*?\|(.*?)(?:\n|$)', html_content) sujet_match = re.search(r'Sujet d\'appel:.*?\|(.*?)(?:\n|$)', html_content) if date_match: infos["date"] = date_match.group(1).strip() if appelant_match: infos["appelant"] = appelant_match.group(1).strip() if telephone_match: infos["telephone"] = telephone_match.group(1).strip() if mobile_match: infos["mobile"] = mobile_match.group(1).strip() if sujet_match: infos["sujet"] = sujet_match.group(1).strip() # Construire le message formaté formatted_message = f"**Notification d'appel**\n\n" if "appelant" in infos: formatted_message += f"De: {infos['appelant']}\n" if "date" in infos: formatted_message += f"Date: {infos['date']}\n" if "telephone" in infos: formatted_message += f"Téléphone: {infos['telephone']}\n" if "mobile" in infos: formatted_message += f"Mobile: {infos['mobile']}\n" if "sujet" in infos: formatted_message += f"Sujet: {infos['sujet']}\n\n" formatted_message += f"Message: {message_content}" return formatted_message # 2. NOUVELLE APPROCHE SIMPLE - Filtrer les lignes problématiques # 2.1. D'abord nettoyer le HTML cleaned_content = pre_clean_html(html_content) # 2.2. Diviser en lignes et filtrer les lignes problématiques filtered_lines = [] # Liste modifiée - moins restrictive pour les informations de contact problematic_indicators = [ "!/web/image/", # Garder celui-ci car c'est spécifique aux images embarquées "[CBAO - développeur de rentabilité", # Signature standard à filtrer "Afin d'assurer une meilleure traçabilité" # Début de disclaimer standard ] # Mémoriser l'indice de la ligne contenant "Cordialement" ou équivalent signature_line_idx = -1 lines = cleaned_content.split('\n') for i, line in enumerate(lines): # Détecter la signature if any(sig in line.lower() for sig in ["cordialement", "cdlt", "bien à vous", "salutation"]): signature_line_idx = i # Vérifier si la ligne contient un indicateur problématique is_problematic = any(indicator in line for indicator in problematic_indicators) # Si la ligne est très longue (plus de 500 caractères), la considérer comme problématique if len(line) > 500: is_problematic = True # Ajouter la ligne seulement si elle n'est pas problématique if not is_problematic: filtered_lines.append(line) # 2.3. Si on a trouvé une signature, ne garder que 2 lignes après maximum if signature_line_idx >= 0: # Suppression de la limitation à 2 lignes après la signature # Gardons toutes les lignes après la signature si ce sont des informations techniques # Ce commentaire est laissé intentionnellement pour référence historique pass # filtered_lines = filtered_lines[:min(signature_line_idx + 3, len(filtered_lines))] # 2.4. Recombiner les lignes filtrées content = '\n'.join(filtered_lines) # 2.5. Nettoyer les espaces et lignes vides content = re.sub(r'\n{3,}', '\n\n', content) content = content.strip() # 2.6. VÉRIFICATION FINALE: S'assurer qu'il n'y a pas de duplication dans le contenu final # Si le même paragraphe apparaît deux fois, ne garder que jusqu'à la première occurrence lines = content.split('\n') unique_lines = [] seen_paragraphs = set() for line in lines: clean_line = line.strip() # Ne traiter que les lignes non vides et assez longues pour être significatives if clean_line and len(clean_line) > 10: if clean_line in seen_paragraphs: # On a déjà vu cette ligne, c'est probablement une duplication # Arrêter le traitement ici break seen_paragraphs.add(clean_line) unique_lines.append(line) content = '\n'.join(unique_lines) # Résultat final if not content or len(content.strip()) < 10: return "*Contenu non extractible*" return content def pre_clean_html(html_content): """ Effectue un nettoyage préliminaire du HTML en préservant la structure et le formatage basique. """ # Remplacer les balises de paragraphe et saut de ligne par des sauts de ligne content = re.sub(r'|]*>|

|]*>|', '\n', html_content) # Préserver le formatage de base (gras, italique, etc.) content = re.sub(r'<(?:b|strong)>(.*?)', r'**\1**', content) content = re.sub(r'<(?:i|em)>(.*?)', r'*\1*', content) # Transformer les listes content = re.sub(r'
  • (.*?)
  • ', r'- \1\n', content) # Supprimer les balises HTML avec leurs attributs mais conserver le contenu content = re.sub(r'<[^>]+>', '', content) # Remplacer les entités HTML courantes content = content.replace(' ', ' ') content = content.replace('<', '<') content = content.replace('>', '>') content = content.replace('&', '&') content = content.replace('"', '"') # Nettoyer les espaces multiples content = re.sub(r' {2,}', ' ', content) # Nettoyer les sauts de ligne multiples (mais pas tous, pour préserver la structure) content = re.sub(r'\n{3,}', '\n\n', content) return content.strip() def format_date(date_str): """ Formate une date ISO en format lisible. """ if not date_str: return "" try: dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) return dt.strftime("%d/%m/%Y %H:%M:%S") except (ValueError, TypeError): return date_str if __name__ == "__main__": # Tests html = """

    Bonjour,

    Voici un message avec du HTML et une signature.

    Cordialement,

    John Doe

    Support technique

    Afin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@exemple.fr

    ![CBAO - développeur de rentabilité - www.exemple.fr](data:image/png;base64,ABC123)

    """ cleaned = clean_html(html) print("HTML nettoyé :\n", cleaned) # Test avec un message transféré forwarded = """\\-------- Message transféré -------- Sujet : | Test message ---|--- Date : | Mon, 30 Mar 2020 11:18:20 +0200 De : | [test@example.com](mailto:test@example.com) Pour : | John Doe [](mailto:john@example.com) Copie à : | [other@example.com](mailto:other@example.com) Bonjour John, Voici un message de test. Cordialement, Test User __________________________________________________________________ Ce message et toutes les pièces jointes sont confidentiels et établis à l'intention exclusive de ses destinataires. __________________________________________________________________""" cleaned_forwarded = clean_html(forwarded) print("\nMessage transféré nettoyé :\n", cleaned_forwarded) # Test avec le cas problématique du ticket T0282 test_t0282 = """Bonjour, Je reviens vers vous pour savoir si vous souhaitez toujours renommer le numéro d'identification de certaines formules dans BCN ou si vous avez trouvé une solution alternative ? En vous remerciant par avance, je reste à votre disposition pour tout complément d'information. Cordialement. **Youness BENDEQ** [ Affin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@cbao.fr Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.""" cleaned_t0282 = clean_html(test_t0282) print("\nTest ticket T0282 nettoyé :\n", cleaned_t0282) # Test avec le cas problématique de bas de page avec formatage markdown test_cbao_markdown = """Bonjour, Voici un message de test pour vérifier la suppression des bas de page CBAO. Cordialement, Jean Dupont [ CBAO S.A.R.L. ](https://example.com/link) . ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)""" cleaned_markdown = clean_html(test_cbao_markdown) print("\nTest avec formatage Markdown CBAO nettoyé :\n", cleaned_markdown) # Test avec le cas exact du rapport test_rapport = """Bonjour, Voici un message de test. Cordialement, Pierre Martin Envoyé par [ CBAO S.A.R.L. ](https://ciibcee.r.af.d.sendibt2.com/tr/cl/h2uBsi9hBosNYeSHMsPH47KAmufMTuNZjreF6M_tfRE63xzft8fwSbEQNb0aYIor74WQB5L6TF4kR9szVpQnalHFa3PUn_0jeLw42JNzIwsESwVlYad_3xCC1xi7qt3-dQ7i_Rt62MG217XgidnJxyNVcXWaWG5B75sB0GoqJq13IZc-hQ) . ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)""" cleaned_rapport = clean_html(test_rapport) print("\nTest avec cas exact du rapport nettoyé :\n", cleaned_rapport)