llm_ticket3/utils/clean_html.py
2025-04-04 17:08:19 +02:00

331 lines
14 KiB
Python

#!/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):
"""
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
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 des indicateurs de lignes problématiques
problematic_indicators = [
"CBAO", "développeur de rentabilité", "traçabilité",
"http://", "https://", ".fr", ".com", "@",
"Envoyé par", "Afin d'assurer", "Affin d'assurer",
"[", "]", "!/web/image/"
]
# 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 200 caractères), la considérer comme problématique
if len(line) > 200:
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:
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.
"""
# Transformer les balises h1 en titres Markdown
content = re.sub(r'<h1>(.*?)</h1>', r'### \1', html_content)
# Transformer les listes à puces
content = re.sub(r'<ul>(.*?)</ul>', r'\1', content, flags=re.DOTALL)
content = re.sub(r'<li><b>(.*?)</b></li>', r'- **\1**\n', content)
content = re.sub(r'<li>(.*?)</li>', r'- \1\n', content)
# Supprimer les balises simples
content = re.sub(r'<br\s*/?>|<p>|</p>|<div>|</div>', '\n', content)
# Préserver le texte en gras
content = re.sub(r'<(?:b|strong)>(.*?)</(?:b|strong)>', r'**\1**', content)
# Supprimer les balises HTML restantes
content = re.sub(r'<.*?>', '', content)
# Remplacer les entités HTML courantes
content = content.replace('&nbsp;', ' ')
content = content.replace('&lt;', '<')
content = content.replace('&gt;', '>')
content = content.replace('&amp;', '&')
content = content.replace('&quot;', '"')
# Supprimer les lignes avec uniquement des **
content = re.sub(r'^\s*\*\*\s*\*\*\s*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^\s*\*\*\s*$', '', content, flags=re.MULTILINE)
# Nettoyer les espaces et sauts de ligne
content = re.sub(r'\n\s*\n', '\n\n', content)
return content
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 = """<p>Bonjour,</p>
<p>Voici un message avec <b>du HTML</b> et une signature.</p>
<p>Cordialement,</p>
<p>John Doe</p>
<p>Support technique</p>
<p>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</p>
<p>![CBAO - développeur de rentabilité - www.exemple.fr](data:image/png;base64,ABC123)</p>
"""
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)