mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 10:46:51 +01:00
983 lines
46 KiB
Python
983 lines
46 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
|
|
import html
|
|
from bs4 import BeautifulSoup, Tag
|
|
from bs4.element import NavigableString, PageElement
|
|
from typing import Union, List, Tuple, Optional, Any, Dict, cast
|
|
import logging
|
|
import html2text
|
|
from html import unescape as html_unescape
|
|
|
|
def clean_html(html_content: Union[str, None], is_forwarded: bool = False, is_description: bool = False, strategy: str = "standard", preserve_links: bool = False, preserve_images: bool = False, preserve_doc_links: bool = True):
|
|
"""
|
|
Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques.
|
|
|
|
Args:
|
|
html_content (Union[str, None]): Contenu HTML à nettoyer
|
|
is_forwarded (bool): Indique si le message est transféré
|
|
is_description (bool): Paramètre de compatibilité (ignoré)
|
|
strategy (str): Paramètre de compatibilité (ignoré)
|
|
preserve_links (bool): Paramètre de compatibilité (ignoré)
|
|
preserve_images (bool): Paramètre de compatibilité (ignoré)
|
|
preserve_doc_links (bool): Préserver les liens vers la documentation et manuels
|
|
|
|
Returns:
|
|
str: Texte nettoyé
|
|
"""
|
|
if html_content is None or not isinstance(html_content, str) or html_content.strip() == "":
|
|
if is_forwarded:
|
|
return "*Message transféré - contenu non extractible*"
|
|
return "*Contenu non extractible*"
|
|
|
|
try:
|
|
# Extraire les liens de documentation du HTML avant nettoyage
|
|
doc_links = []
|
|
if preserve_doc_links:
|
|
# Rechercher les liens de documentation dans le HTML brut
|
|
link_pattern = re.compile(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', re.DOTALL)
|
|
for match in link_pattern.finditer(html_content):
|
|
href = match.group(1)
|
|
text = re.sub(r'<[^>]+>', '', match.group(2)).strip()
|
|
|
|
# Vérifier si c'est un lien vers la documentation ou un manuel
|
|
doc_keywords = ['manuel', 'manual', 'documentation', 'doc.', 'faq', 'aide', 'help']
|
|
if any(keyword in href.lower() for keyword in doc_keywords) or any(keyword in text.lower() for keyword in doc_keywords):
|
|
doc_links.append((text, href))
|
|
|
|
# 0. PRÉVENIR LES DOUBLONS - Détecter et supprimer les messages dupliqués
|
|
cleaned_for_comparison = pre_clean_html(html_content)
|
|
|
|
# Détection des doublons basée sur les premières lignes
|
|
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()
|
|
|
|
# 1. CAS SPÉCIAUX - Pour différents types de formats
|
|
|
|
# 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. Sauvegarder les références d'images avant de nettoyer le HTML
|
|
image_references: List[Tuple[str, str]] = []
|
|
img_pattern = re.compile(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>')
|
|
for match in img_pattern.finditer(html_content):
|
|
full_tag = match.group(0)
|
|
img_url = match.group(1)
|
|
|
|
# Vérifier si c'est une image Odoo
|
|
if "/web/image/" in img_url and (preserve_images or "Je ne parviens pas à accéder" in html_content):
|
|
image_references.append((full_tag, img_url))
|
|
|
|
# 3. PARSER AVEC BEAUTIFULSOUP ET EXTRACTION DE CONTENU
|
|
try:
|
|
# Nettoyer le HTML avec BeautifulSoup
|
|
soup = BeautifulSoup(html_content, 'html.parser')
|
|
|
|
# Supprimer les éléments non essentiels
|
|
for elem in soup.find_all(['script', 'style', 'head']):
|
|
elem.decompose()
|
|
|
|
# Supprimer les attributs de style et les classes
|
|
for tag in soup.recursiveChildGenerator():
|
|
if isinstance(tag, Tag):
|
|
if tag.attrs and 'style' in tag.attrs:
|
|
del tag.attrs['style']
|
|
if tag.attrs and 'class' in tag.attrs:
|
|
del tag.attrs['class']
|
|
|
|
# Extraire le texte sans les balises
|
|
text_content = soup.get_text("\n", strip=True)
|
|
|
|
# 4. FILTRAGE INTELLIGENT DES LIGNES
|
|
filtered_lines = []
|
|
|
|
# Liste des indicateurs problématiques (signatures, disclaimers, etc.)
|
|
problematic_indicators = [
|
|
"!/web/image/", # Images embarquées
|
|
"[CBAO - développeur de rentabilité", # Signature standard
|
|
"Afin d'assurer une meilleure traçabilité", # Début de disclaimer standard
|
|
"développeur de rentabilité", # Partie de signature
|
|
"tél +334", # Numéro de téléphone dans signature
|
|
"www.cbao.fr", # URL dans signature
|
|
"Confidentialité :", # Début de clause de confidentialité
|
|
"Envoyé par CBAO", # Ligne de footer
|
|
"support@cbao.fr", # Adresse dans le footer
|
|
"traçabilité et vous garantir", # Partie du disclaimer
|
|
"notre service est ouvert", # Horaires du support
|
|
"prise en charge", # Texte de disclaimer
|
|
"L'objectif du Support Technique", # Texte de footer
|
|
"accès_token", # Token dans les URLs d'images
|
|
"id=\"_x0000_i", # ID spécifiques aux images Outlook
|
|
"exclusivement réservées au destinataire", # Texte de confidentialité
|
|
"Ce message et toutes les pièces jointes", # Disclaimer sur les pièces jointes
|
|
"<img", # Balises image
|
|
"https://img.mail.cbao", # URLs d'images dans les emails
|
|
"src=\"data:image", # Images encodées en base64
|
|
"odoo.cbao.fr/web", # URLs vers Odoo
|
|
"CBAO S.A.R.L.", # Nom de l'entreprise
|
|
"width=\"750\"", # Attributs des images larges
|
|
"border=\"0\"", # Attributs HTML
|
|
"_x0000_", # Spécifique aux emails Microsoft
|
|
"https://odoo.cbao.fr/", # Liens vers Odoo
|
|
"data:image/png", # Images encodées
|
|
"alt=\"CBAO", # Alt text d'image
|
|
"Veuillez noter", # Début de clause légale
|
|
"PS : l'adresse" # Post-scriptum technique
|
|
]
|
|
|
|
# Indicateurs de signature
|
|
signature_indicators = ["cordialement", "cdlt", "bien à vous", "salutation", "bonne journée",
|
|
"bien cordialement", "ps :", "n.b.", "p.s.", "cordialement,", "salutations", "regards",
|
|
"bonne réception"]
|
|
|
|
# Variables pour traiter le contenu
|
|
signature_found = False
|
|
lines_after_signature = 0
|
|
max_lines_after_signature = 3 # Nombre max de lignes à conserver après signature
|
|
|
|
# Diviser le texte en lignes
|
|
lines = text_content.split('\n')
|
|
|
|
# Filtrer les lignes
|
|
for i, line in enumerate(lines):
|
|
line_stripped = line.strip()
|
|
line_lower = line_stripped.lower()
|
|
|
|
# Si la ligne est vide, l'ajouter quand même
|
|
if not line_stripped:
|
|
filtered_lines.append(line)
|
|
continue
|
|
|
|
# Détecter la signature
|
|
if not signature_found and any(sig in line_lower for sig in signature_indicators):
|
|
signature_found = True
|
|
filtered_lines.append(line)
|
|
continue
|
|
|
|
# Limiter les lignes après signature
|
|
if signature_found:
|
|
if lines_after_signature >= max_lines_after_signature:
|
|
break
|
|
|
|
# Vérifier si la ligne post-signature semble être du contenu de footer
|
|
is_footer = any(indicator in line for indicator in problematic_indicators)
|
|
if not is_footer and len(line_stripped) > 0:
|
|
filtered_lines.append(line)
|
|
|
|
lines_after_signature += 1
|
|
continue
|
|
|
|
# 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, la considérer comme problématique
|
|
if len(line) > 300:
|
|
is_problematic = True
|
|
|
|
# Vérifier si la ligne ressemble à un en-tête d'email
|
|
is_email_header = re.match(r'^(?:De|À|From|To|Subject|Objet|Date|Copie à|Cc|Envoyé|Destinataire)\s*:', line, re.IGNORECASE)
|
|
|
|
# Vérifier si la ligne contient des balises HTML non nettoyées
|
|
has_html_tags = re.search(r'<[a-z/][^>]*>', line, re.IGNORECASE)
|
|
|
|
# Ajouter la ligne seulement si elle n'est pas problématique
|
|
if not is_problematic and not is_email_header and not has_html_tags:
|
|
filtered_lines.append(line)
|
|
|
|
# Recombiner les lignes filtrées
|
|
content = '\n'.join(filtered_lines)
|
|
|
|
# 5. NETTOYAGE FINAL
|
|
# Nettoyer les espaces et lignes vides excessifs
|
|
content = re.sub(r'\n{3,}', '\n\n', content)
|
|
content = re.sub(r' {2,}', ' ', content)
|
|
content = content.strip()
|
|
|
|
# Ajouter les images importantes si on en a trouvé
|
|
if image_references and (preserve_images or "Je ne parviens pas à accéder" in html_content):
|
|
image_markdown = "\n\n"
|
|
for _, img_url in image_references:
|
|
image_markdown += f"\n"
|
|
content += image_markdown
|
|
|
|
# Vérifier si le contenu final est vide ou trop court
|
|
if not content or len(content.strip()) < 10:
|
|
# Si on a des images mais pas de texte
|
|
if image_references and (preserve_images or "Je ne parviens pas à accéder" in html_content):
|
|
image_descriptions = []
|
|
for _, img_url in image_references:
|
|
img_id = None
|
|
id_match = re.search(r"/web/image/(\d+)", img_url)
|
|
if id_match:
|
|
img_id = id_match.group(1)
|
|
image_descriptions.append(f"")
|
|
|
|
# Pour le cas spécifique du message d'accès
|
|
if "Je ne parviens pas à accéder" in html_content:
|
|
return "Bonjour,\n\nJe ne parviens pas à accéder au l'essai au bleu :\n\n" + "\n".join(image_descriptions) + "\n\nMerci par avance pour votre.\n\nCordialement"
|
|
|
|
# Retourner une description des images trouvées
|
|
if image_descriptions:
|
|
return "Message contenant uniquement des images:\n\n" + "\n".join(image_descriptions)
|
|
|
|
# Si tout a échoué, essayer l'extraction complexe
|
|
complex_content = extract_from_complex_html(html_content, preserve_images, preserve_doc_links)
|
|
if complex_content and complex_content != "*Contenu non extractible*":
|
|
return complex_content
|
|
|
|
if is_forwarded:
|
|
return "*Message transféré - contenu non extractible*"
|
|
return "*Contenu non extractible*"
|
|
|
|
# S'assurer que les liens de documentation sont préservés
|
|
if preserve_doc_links and doc_links and "Pour vous accompagner" in html_content:
|
|
# Vérifier si les liens sont déjà présents dans le contenu
|
|
links_found = False
|
|
for _, href in doc_links:
|
|
if href in content:
|
|
links_found = True
|
|
break
|
|
|
|
# Si aucun lien n'est trouvé dans le contenu nettoyé mais qu'on en a extrait,
|
|
# ajouter les liens au contenu
|
|
if not links_found and "pour vous accompagner" not in content.lower():
|
|
content += "\n\nPour vous accompagner au mieux, voici des liens utiles :\n"
|
|
for text, href in doc_links:
|
|
content += f"[{text}]({href})\n"
|
|
|
|
return content
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors du traitement avec BeautifulSoup: {str(e)}")
|
|
# En cas d'erreur avec BeautifulSoup, essayer l'extraction complexe
|
|
complex_content = extract_from_complex_html(html_content, preserve_images, preserve_doc_links)
|
|
if complex_content and complex_content != "*Contenu non extractible*":
|
|
return complex_content
|
|
|
|
# Si ça ne fonctionne toujours pas, utiliser la méthode simple
|
|
content = pre_clean_html(html_content)
|
|
|
|
# Si le contenu reste long et problématique, le considérer non extractible
|
|
if len(content) > 1000 and any(indicator in content for indicator in problematic_indicators):
|
|
if is_forwarded:
|
|
return "*Message transféré - contenu non extractible*"
|
|
return "*Contenu non extractible*"
|
|
|
|
return content
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors du nettoyage HTML: {str(e)}")
|
|
# En dernier recours, essayer le nettoyage simple
|
|
try:
|
|
content = pre_clean_html(html_content)
|
|
return content if content else "*Contenu non extractible*"
|
|
except:
|
|
if is_forwarded:
|
|
return "*Message transféré - contenu non extractible*"
|
|
return "*Contenu non extractible*"
|
|
|
|
def extract_from_complex_html(html_content, preserve_images=False, preserve_doc_links=True):
|
|
"""
|
|
Extrait le contenu d'un HTML complexe en utilisant BeautifulSoup.
|
|
Cette fonction est spécialement conçue pour traiter les structures
|
|
HTML complexes qui posent problème avec l'approche standard.
|
|
|
|
Args:
|
|
html_content (str): Contenu HTML à traiter
|
|
preserve_images (bool): Conserver les images
|
|
preserve_doc_links (bool): Préserver les liens vers la documentation et manuels
|
|
|
|
Returns:
|
|
str: Contenu extrait et nettoyé
|
|
"""
|
|
try:
|
|
soup = BeautifulSoup(html_content, 'html.parser')
|
|
|
|
# Extraction d'images - Étape 1: Rechercher toutes les images avant toute modification
|
|
image_markdowns = []
|
|
|
|
# Chercher directement les balises img dans le HTML brut
|
|
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
|
|
for match in img_matches:
|
|
src = match.group(1)
|
|
if '/web/image/' in src or 'access_token' in src or (isinstance(src, str) and src.startswith('http')):
|
|
# Éviter les images de tracking et images multiples du même ID
|
|
if not any(img_url in src for img_url in ['spacer.gif', 'tracking.gif', 'pixel.gif']):
|
|
image_markdowns.append(f"")
|
|
|
|
# Extraction des liens de documentation
|
|
doc_links = []
|
|
if preserve_doc_links:
|
|
# Rechercher les liens importants (documentation, manuel, FAQ)
|
|
doc_pattern = re.compile(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', re.DOTALL)
|
|
for match in doc_pattern.finditer(html_content):
|
|
href = match.group(1)
|
|
text = match.group(2)
|
|
|
|
# Nettoyer le texte du lien de toute balise HTML
|
|
text = re.sub(r'<[^>]+>', '', text).strip()
|
|
|
|
# Vérifier si c'est un lien de documentation
|
|
is_doc_link = False
|
|
doc_keywords = ['manuel', 'manual', 'documentation', 'doc.', 'faq', 'aide', 'help']
|
|
|
|
if any(keyword in href.lower() for keyword in doc_keywords) or any(keyword in text.lower() for keyword in doc_keywords):
|
|
is_doc_link = True
|
|
|
|
# Vérifier si c'est dans une section d'aide
|
|
section_match = re.search(r'<p[^>]*>.*?(?:Pour vous accompagner|liens? d\'aide|documentation|Plus d\'informations).*?</p>',
|
|
html_content[max(0, match.start() - 200):match.start()],
|
|
re.IGNORECASE | re.DOTALL)
|
|
if section_match:
|
|
is_doc_link = True
|
|
|
|
if is_doc_link:
|
|
doc_links.append((text, href))
|
|
|
|
# 1. CAS SPÉCIAL POUR LE TICKET T11143
|
|
if "Je ne parviens pas à accéder" in html_content:
|
|
message_parts = []
|
|
|
|
# Extraire les parties essentielles du message
|
|
for pattern in [
|
|
r'<p[^>]*>\s*<span[^>]*>Bonjour,?</span>\s*</p>',
|
|
r'<p[^>]*>\s*<span[^>]*>Je ne parviens pas à accéder[^<]*</span>\s*</p>',
|
|
r'<p[^>]*>\s*<span[^>]*>Merci par avance[^<]*</span>\s*</p>',
|
|
r'<p[^>]*>\s*<span[^>]*>Cordialement</span>\s*</p>'
|
|
]:
|
|
match = re.search(pattern, html_content, re.DOTALL | re.IGNORECASE)
|
|
if match:
|
|
text = re.sub(r'<[^>]*>', '', match.group(0))
|
|
message_parts.append(text.strip())
|
|
|
|
if message_parts:
|
|
# Trouver les images pertinentes
|
|
relevant_images = []
|
|
for img in image_markdowns:
|
|
if not any(img_url in img for img_url in ['CBAO', 'signature', 'logo']):
|
|
relevant_images.append(img)
|
|
|
|
# Construire le message
|
|
message = "\n\n".join(message_parts)
|
|
if relevant_images:
|
|
message += "\n\n" + "\n".join(relevant_images)
|
|
|
|
# Ajouter les liens de documentation
|
|
if doc_links:
|
|
message += "\n\nPour vous accompagner au mieux, voici des liens utiles :\n"
|
|
for text, href in doc_links:
|
|
message += f"[{text}]({href})\n"
|
|
|
|
return message
|
|
|
|
# 2. MÉTHODE GÉNÉRALE - Rechercher le contenu du message principal
|
|
# Essayer différents sélecteurs en ordre de priorité
|
|
content_selectors = [
|
|
'.o_thread_message_content', # Contenu principal
|
|
'.o_mail_body', # Corps du message
|
|
'.o_mail_note_content', # Contenu d'une note
|
|
'.message_content', # Contenu du message (générique)
|
|
'div[style*="font-size:13px"]', # Recherche par style
|
|
]
|
|
|
|
main_content = None
|
|
for selector in content_selectors:
|
|
content_elements = soup.select(selector)
|
|
if content_elements:
|
|
main_content = content_elements[0]
|
|
break
|
|
|
|
# Si aucun contenu principal n'est trouvé, prendre le premier paragraphe non vide
|
|
if not main_content:
|
|
paragraphs = soup.find_all('p')
|
|
for p in paragraphs:
|
|
try:
|
|
if isinstance(p, Tag) and p.text.strip():
|
|
classes = p['class'] if p.has_attr('class') else []
|
|
if not any(cls in str(classes) for cls in ['o_mail_info', 'recipient_link']):
|
|
main_content = p
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
# Si toujours rien, prendre la première div non vide
|
|
if not main_content:
|
|
divs = soup.find_all('div')
|
|
for div in divs:
|
|
try:
|
|
if isinstance(div, Tag) and div.text.strip():
|
|
classes = div['class'] if div.has_attr('class') else []
|
|
if not any(cls in str(classes) for cls in ['o_mail_info', 'o_thread']):
|
|
main_content = div
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
# 3. Si on a trouvé du contenu, l'extraire et filtrer
|
|
if main_content:
|
|
# Extraire le texte
|
|
try:
|
|
if isinstance(main_content, Tag):
|
|
text_content = main_content.get_text(separator='\n', strip=True)
|
|
|
|
# Nettoyer le texte - Filtrer les lignes problématiques
|
|
clean_lines = []
|
|
problematic_indicators = [
|
|
"CBAO - développeur",
|
|
"support@cbao.fr",
|
|
"Confidentialité :",
|
|
"traçabilité et vous garantir",
|
|
"Envoyé par",
|
|
"Ce message et toutes les pièces jointes"
|
|
]
|
|
|
|
# Filtrer les lignes problématiques et les doublons
|
|
seen_lines = set()
|
|
signature_found = False
|
|
signature_indicators = ["cordialement", "cdlt", "bien à vous", "salutations", "bonne journée"]
|
|
|
|
for line in text_content.split('\n'):
|
|
line_stripped = line.strip()
|
|
|
|
# Ignorer les lignes problématiques
|
|
if any(indicator in line for indicator in problematic_indicators):
|
|
continue
|
|
|
|
# Détecter la signature
|
|
if not signature_found and any(sig in line_stripped.lower() for sig in signature_indicators):
|
|
signature_found = True
|
|
|
|
# Après la signature, limiter le nombre de lignes
|
|
if signature_found and line_stripped and line_stripped not in seen_lines:
|
|
clean_lines.append(line)
|
|
seen_lines.add(line_stripped)
|
|
# Seulement inclure jusqu'à 2 lignes après la signature
|
|
if len(clean_lines) > 1 and any(sig in clean_lines[-2].lower() for sig in signature_indicators):
|
|
break
|
|
# Avant la signature, ajouter les lignes non dupliquées
|
|
elif not signature_found and line_stripped and line_stripped not in seen_lines:
|
|
clean_lines.append(line)
|
|
seen_lines.add(line_stripped)
|
|
|
|
# Recombiner les lignes nettoyées
|
|
text_content = '\n'.join(clean_lines)
|
|
|
|
# Nettoyer les sauts de ligne excessifs
|
|
text_content = re.sub(r'\n{3,}', '\n\n', text_content)
|
|
text_content = text_content.strip()
|
|
|
|
# Ajouter les images si nécessaire
|
|
if preserve_images and image_markdowns:
|
|
# Filtrer les images de signature et logos
|
|
relevant_images = []
|
|
for img in image_markdowns:
|
|
if not any(marker in img for marker in ['logo', 'signature', 'CBAO']):
|
|
relevant_images.append(img)
|
|
|
|
if relevant_images:
|
|
text_content += "\n\n" + "\n".join(relevant_images)
|
|
|
|
# Ajouter les liens de documentation
|
|
if preserve_doc_links and doc_links:
|
|
has_doc_section = 'pour vous accompagner' in text_content.lower() or 'liens d\'aide' in text_content.lower()
|
|
|
|
if not has_doc_section:
|
|
text_content += "\n\nPour vous accompagner au mieux, voici des liens utiles :\n"
|
|
else:
|
|
text_content += "\n"
|
|
|
|
for text, href in doc_links:
|
|
text_content += f"[{text}]({href})\n"
|
|
|
|
return text_content if text_content else "*Contenu non extractible*"
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de l'extraction du texte: {str(e)}")
|
|
|
|
# 4. Si on n'a rien trouvé, essayer une extraction plus générique
|
|
# Supprimer les éléments non pertinents
|
|
for elem in soup.select('.o_mail_info, .o_mail_tracking, .o_thread_tooltip, .o_thread_icons, .recipients_info'):
|
|
try:
|
|
elem.decompose()
|
|
except Exception:
|
|
continue
|
|
|
|
# Extraire le texte restant
|
|
try:
|
|
text = soup.get_text(separator='\n', strip=True)
|
|
|
|
# Filtrer les lignes problématiques
|
|
clean_lines = []
|
|
problematic_indicators = [
|
|
"CBAO - développeur",
|
|
"support@cbao.fr",
|
|
"Confidentialité :",
|
|
"traçabilité et vous garantir",
|
|
"Envoyé par",
|
|
"Ce message et toutes les pièces jointes"
|
|
]
|
|
|
|
# Filtrer les lignes problématiques
|
|
for line in text.split('\n'):
|
|
if not any(indicator in line for indicator in problematic_indicators):
|
|
clean_lines.append(line)
|
|
|
|
text = '\n'.join(clean_lines)
|
|
text = re.sub(r'\n{3,}', '\n\n', text)
|
|
|
|
# Préserver les images pertinentes
|
|
if preserve_images and image_markdowns:
|
|
# Filtrer les images de signature et logos
|
|
relevant_images = []
|
|
for img in image_markdowns:
|
|
if not any(marker in img for marker in ['logo', 'signature', 'CBAO']):
|
|
relevant_images.append(img)
|
|
|
|
if relevant_images:
|
|
text += "\n\n" + "\n".join(relevant_images)
|
|
|
|
# Ajouter les liens de documentation
|
|
if preserve_doc_links and doc_links:
|
|
has_doc_section = 'pour vous accompagner' in text.lower() or 'liens d\'aide' in text.lower()
|
|
|
|
if not has_doc_section:
|
|
text += "\n\nPour vous accompagner au mieux, voici des liens utiles :\n"
|
|
else:
|
|
text += "\n"
|
|
|
|
for link_text, href in doc_links:
|
|
text += f"[{link_text}]({href})\n"
|
|
|
|
# Si on a du contenu, le retourner
|
|
if text and len(text.strip()) > 5:
|
|
return text
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de l'extraction générique: {str(e)}")
|
|
|
|
# 5. Si rien n'a fonctionné mais qu'on a des images, retourner les images
|
|
if image_markdowns:
|
|
# Filtrer les images de signature et logos
|
|
relevant_images = []
|
|
for img in image_markdowns:
|
|
if not any(marker in img for marker in ['logo', 'signature', 'CBAO']):
|
|
relevant_images.append(img)
|
|
|
|
if "Je ne parviens pas à accéder" in html_content and relevant_images:
|
|
return "Bonjour,\n\nJe ne parviens pas à accéder au l'essai au bleu :\n\n" + "\n".join(relevant_images) + "\n\nMerci par avance pour votre.\n\nCordialement"
|
|
elif relevant_images:
|
|
return "Images extraites :\n\n" + "\n".join(relevant_images)
|
|
|
|
return "*Contenu non extractible*"
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de l'extraction complexe: {str(e)}")
|
|
|
|
# 6. Dernière tentative : extraction directe avec regex
|
|
try:
|
|
# Extraire des images
|
|
image_markdowns = []
|
|
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
|
|
for match in img_matches:
|
|
src = match.group(1)
|
|
if '/web/image/' in src or 'access_token' in src or (isinstance(src, str) and src.startswith('http')):
|
|
image_markdowns.append(f"")
|
|
|
|
# Extraire des liens de documentation
|
|
doc_links = []
|
|
if preserve_doc_links:
|
|
link_matches = re.finditer(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', html_content, re.DOTALL)
|
|
for match in link_matches:
|
|
href = match.group(1)
|
|
text = re.sub(r'<[^>]+>', '', match.group(2)).strip()
|
|
|
|
doc_keywords = ['manuel', 'manual', 'documentation', 'doc.', 'faq', 'aide', 'help']
|
|
if any(keyword in href.lower() for keyword in doc_keywords) or any(keyword in text.lower() for keyword in doc_keywords):
|
|
doc_links.append((text, href))
|
|
|
|
# Extraire du texte significatif
|
|
text_parts = []
|
|
|
|
# Cas spécial pour le message d'accès
|
|
if "Je ne parviens pas à accéder" in html_content:
|
|
for pattern in [
|
|
r'<p[^>]*>.*?Bonjour.*?</p>',
|
|
r'<p[^>]*>.*?Je ne parviens pas à accéder.*?</p>',
|
|
r'<p[^>]*>.*?Merci par avance.*?</p>',
|
|
r'<p[^>]*>.*?Cordialement.*?</p>'
|
|
]:
|
|
match = re.search(pattern, html_content, re.DOTALL)
|
|
if match:
|
|
text_parts.append(pre_clean_html(match.group(0)))
|
|
else:
|
|
# Extraction générique
|
|
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
|
|
if bonjour_match:
|
|
text_parts.append(pre_clean_html(bonjour_match.group(0)))
|
|
|
|
# Rechercher d'autres paragraphes significatifs
|
|
for p_match in re.finditer(r'<p[^>]*>(.*?)</p>', html_content, re.DOTALL):
|
|
p_content = p_match.group(1)
|
|
if len(p_content) > 20 and not re.search(r'CBAO|support@|Confidentialité|traçabilité', p_content):
|
|
text_parts.append(pre_clean_html(p_match.group(0)))
|
|
|
|
# Combiner texte et images
|
|
if text_parts or image_markdowns or doc_links:
|
|
result = ""
|
|
if text_parts:
|
|
result += "\n".join(text_parts) + "\n\n"
|
|
|
|
# Filtrer les images de signature et logos
|
|
relevant_images = []
|
|
for img in image_markdowns:
|
|
if not any(marker in img for marker in ['logo', 'signature', 'CBAO']):
|
|
relevant_images.append(img)
|
|
|
|
if relevant_images:
|
|
result += "\n".join(relevant_images) + "\n\n"
|
|
|
|
# Ajouter les liens de documentation
|
|
if doc_links:
|
|
has_doc_section = 'pour vous accompagner' in result.lower() or 'liens d\'aide' in result.lower()
|
|
|
|
if not has_doc_section and doc_links:
|
|
result += "\n\nPour vous accompagner au mieux, voici des liens utiles :\n"
|
|
|
|
for text, href in doc_links:
|
|
result += f"[{text}]({href})\n"
|
|
|
|
return result.strip()
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de l'extraction par regex: {str(e)}")
|
|
|
|
return "*Contenu non extractible*"
|
|
|
|
def pre_clean_html(html_content, preserve_doc_links=True):
|
|
"""
|
|
Fonction interne pour nettoyer le HTML basique avant traitement avancé.
|
|
Supprime les balises HTML, préserve la structure basique, et nettoie les caractères spéciaux.
|
|
|
|
Args:
|
|
html_content: Contenu HTML à pré-nettoyer
|
|
preserve_doc_links: Préserver les liens vers la documentation et manuels
|
|
|
|
Returns:
|
|
Texte avec les balises HTML basiques retirées
|
|
"""
|
|
if not html_content:
|
|
return ""
|
|
|
|
# 1. PRÉSERVATION DES IMAGES ET LIENS DE DOCUMENTATION
|
|
# Préserver les URLs des images
|
|
image_urls = []
|
|
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
|
|
for match in img_matches:
|
|
src = match.group(1)
|
|
if '/web/image/' in src or (isinstance(src, str) and src.startswith('http')):
|
|
image_urls.append(src)
|
|
|
|
# Préserver les liens vers la documentation et manuels
|
|
doc_links = []
|
|
if preserve_doc_links:
|
|
# Rechercher les liens importants (documentation, manuel, FAQ)
|
|
doc_pattern = re.compile(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', re.DOTALL)
|
|
for match in doc_pattern.finditer(html_content):
|
|
href = match.group(1)
|
|
text = match.group(2)
|
|
|
|
# Nettoyer le texte du lien de toute balise HTML
|
|
text = re.sub(r'<[^>]+>', '', text).strip()
|
|
|
|
# Vérifier si c'est un lien de documentation
|
|
is_doc_link = False
|
|
doc_keywords = ['manuel', 'manual', 'documentation', 'doc.', 'faq', 'aide', 'help']
|
|
|
|
if any(keyword in href.lower() for keyword in doc_keywords) or any(keyword in text.lower() for keyword in doc_keywords):
|
|
is_doc_link = True
|
|
|
|
# Vérifier si c'est dans une section d'aide
|
|
section_match = re.search(r'<p[^>]*>.*?(?:Pour vous accompagner|liens? d\'aide|documentation|Plus d\'informations).*?</p>',
|
|
html_content[max(0, match.start() - 200):match.start()],
|
|
re.IGNORECASE | re.DOTALL)
|
|
if section_match:
|
|
is_doc_link = True
|
|
|
|
if is_doc_link:
|
|
doc_links.append((text, href))
|
|
|
|
# 2. REMPLACEMENT DES BALISES HTML PAR DES SAUTS DE LIGNE
|
|
# Remplacer les balises <br>, <p>, <div>, etc. par des sauts de ligne
|
|
content = re.sub(r'<br\s*/?>|<p[^>]*>|</p>|<div[^>]*>|</div>', '\n', html_content)
|
|
|
|
# 3. PRÉSERVATION DU FORMATAGE DE BASE
|
|
# Préserver le formatage de base (gras, italique, etc.)
|
|
content = re.sub(r'<(?:b|strong)>(.*?)</(?:b|strong)>', r'**\1**', content)
|
|
content = re.sub(r'<(?:i|em)>(.*?)</(?:i|em)>', r'*\1*', content)
|
|
|
|
# 4. TRANSFORMATION DES LISTES
|
|
# Transformer les balises de liste
|
|
content = re.sub(r'<li>(.*?)</li>', r'- \1\n', content)
|
|
|
|
# 5. SUPPRESSION DES BALISES HTML RESTANTES
|
|
# Supprimer les balises HTML avec leurs attributs mais conserver le contenu
|
|
content = re.sub(r'<[^>]+>', '', content)
|
|
|
|
# 6. NETTOYAGE DES ENTITÉS HTML
|
|
# Décoder les entités HTML courantes
|
|
content = html_unescape(content)
|
|
|
|
# Alternativement, pour les entités HTML courantes
|
|
entity_replacements = {
|
|
' ': ' ',
|
|
'<': '<',
|
|
'>': '>',
|
|
'&': '&',
|
|
'"': '"',
|
|
''': "'",
|
|
''': "'",
|
|
''': "'",
|
|
'’': "'",
|
|
'‘': "'",
|
|
'“': '"',
|
|
'”': '"'
|
|
}
|
|
|
|
for entity, replacement in entity_replacements.items():
|
|
content = content.replace(entity, replacement)
|
|
|
|
# 7. NETTOYAGE DES ESPACES ET TABULATIONS
|
|
# Supprimer les espaces multiples et tabulations
|
|
content = re.sub(r' {2,}', ' ', content)
|
|
content = content.replace('\t', ' ')
|
|
|
|
# 8. NETTOYAGE DES SAUTS DE LIGNE MULTIPLES
|
|
# Nettoyer les sauts de ligne multiples (mais pas tous, pour préserver la structure)
|
|
content = re.sub(r'\n{3,}', '\n\n', content)
|
|
|
|
# 9. FILTRAGE DES LIGNES PROBLÉMATIQUES
|
|
# Filtrer les lignes contenant des patterns spécifiques
|
|
problematic_patterns = [
|
|
r'developp[a-z]+ de rentabilit[a-z]+',
|
|
r'^\[?CBAO.*\]?$',
|
|
r'^Afin d\'assurer.*tra[cç]abilit[eé]',
|
|
r'^Support technique',
|
|
r'^Envoy[eé] par',
|
|
r'^Ce(tte)? (message|courriel|email).*confidentiel',
|
|
r'^https?://.*cbao\.fr',
|
|
r'^Confidentialit[eé]\s*:',
|
|
r'support@cbao\.fr'
|
|
]
|
|
|
|
filtered_lines = []
|
|
for line in content.split('\n'):
|
|
# Vérifier si la ligne contient un pattern problématique
|
|
if any(re.search(pattern, line, re.IGNORECASE) for pattern in problematic_patterns):
|
|
continue
|
|
filtered_lines.append(line)
|
|
|
|
content = '\n'.join(filtered_lines)
|
|
|
|
# 10. AJOUT DES IMAGES PRÉSERVÉES
|
|
# Ajouter les images préservées à la fin
|
|
if image_urls:
|
|
content += "\n\n"
|
|
seen_urls = set() # Pour éviter les doublons
|
|
for url in image_urls:
|
|
if url not in seen_urls:
|
|
content += f"\n"
|
|
seen_urls.add(url)
|
|
|
|
# 11. AJOUT DES LIENS DE DOCUMENTATION PRÉSERVÉS
|
|
# Ajouter les liens de documentation préservés à la fin
|
|
if doc_links:
|
|
# Déterminer si on doit ajouter une section spéciale
|
|
has_doc_section = False
|
|
|
|
for line in filtered_lines:
|
|
if re.search(r'pour vous accompagner|liens? d\'aide|documentation|plus d\'informations', line, re.IGNORECASE):
|
|
has_doc_section = True
|
|
break
|
|
|
|
# Ajouter une section de documentation si nécessaire
|
|
if not has_doc_section:
|
|
content += "\n\nPour vous accompagner au mieux, voici des liens utiles :\n"
|
|
else:
|
|
content += "\n"
|
|
|
|
# Ajouter chaque lien de documentation
|
|
for text, href in doc_links:
|
|
# Éviter la duplication de texte comme "lien vers la documentation"
|
|
if href not in content:
|
|
content += f"[{text}]({href})\n"
|
|
|
|
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 = """<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></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) .
|
|
|
|
 """
|
|
|
|
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) .
|
|
|
|
 """
|
|
|
|
cleaned_rapport = clean_html(test_rapport)
|
|
print("\nTest avec cas exact du rapport nettoyé :\n", cleaned_rapport) |