llm_ticket3/formatters/clean_html.bak
2025-04-16 09:20:42 +02:00

503 lines
21 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
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):
"""
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é)
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:
# 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:
image_references.append((full_tag, img_url))
# Nettoyer le HTML
soup = BeautifulSoup(html_content, 'html.parser')
# Supprimer les éléments script, style et head
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']
# Conserver uniquement les balises HTML essentielles
allowed_tags = ['p', 'br', 'b', 'i', 'u', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code', 'pre', 'hr', 'div', 'span',
'table', 'tr', 'td', 'th', 'thead', 'tbody']
# Supprimer les balises HTML inutiles mais conserver leur contenu
for tag in soup.find_all():
if isinstance(tag, Tag) and tag.name.lower() not in allowed_tags:
tag.unwrap()
# Amélioration: vérifier si nous avons du contenu significatif
text_content = soup.get_text().strip()
if not text_content and not image_references:
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
# Obtenir le HTML nettoyé
clean_content = str(soup)
# Vérifier si le contenu a été vidé par le nettoyage
if clean_content.strip() == "" or clean_content.strip() == "<html><body></body></html>":
# Si nous avons des références d'images mais pas de texte
if image_references:
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"[Image {img_id}]")
# Retourner une description des images trouvées
if image_descriptions:
return "Message contenant uniquement des images: " + ", ".join(image_descriptions)
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
return clean_content
except Exception as e:
logging.error(f"Erreur lors du nettoyage HTML: {str(e)}")
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
def extract_from_complex_html(html_content, preserve_images=False):
"""
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
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 = []
if preserve_images or True: # Toujours préserver les images
# 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 src.startswith('http'):
image_markdowns.append(f"![Image]({src})")
# Méthode alternative avec BeautifulSoup
images = soup.find_all('img')
for img in images:
try:
if isinstance(img, Tag) and img.has_attr('src'):
src = img['src']
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
alt = img['alt'] if img.has_attr('alt') else 'Image'
image_markdowns.append(f"![{alt}]({src})")
except Exception:
continue
# 1. Rechercher d'abord 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
# 2. Si on a trouvé du contenu, l'extraire
if main_content:
# Extraire toutes les images si demandé
if preserve_images or True: # Toujours préserver les images
try:
if isinstance(main_content, Tag):
content_images = main_content.find_all('img')
for img in content_images:
try:
if isinstance(img, Tag) and img.has_attr('src'):
src = img['src']
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
alt = img['alt'] if img.has_attr('alt') else 'Image'
image_markdowns.append(f"![{alt}]({src})")
# Supprimer l'image pour éviter qu'elle apparaisse dans le texte
img.decompose()
except Exception:
continue
except Exception:
pass
# Extraire le texte
try:
if isinstance(main_content, Tag):
text_content = main_content.get_text(separator='\n', strip=True)
# Nettoyer le texte
text_content = re.sub(r'\n{3,}', '\n\n', text_content)
text_content = text_content.strip()
# Recherche spécifique pour certaines phrases clés
if "Je ne parviens pas à accéder" in html_content:
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
acces_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder[^<]*</p>', html_content, re.DOTALL)
specific_content = []
if bonjour_match:
specific_content.append(pre_clean_html(bonjour_match.group(0)))
if acces_match:
specific_content.append(pre_clean_html(acces_match.group(0)))
# Extraire les contenus spécifiques du message "Je ne parviens pas..."
merci_match = re.search(r'<p[^>]*>.*?Merci par avance.*?</p>', html_content, re.DOTALL)
if merci_match:
specific_content.append(pre_clean_html(merci_match.group(0)))
cordial_match = re.search(r'<p[^>]*>.*?Cordialement.*?</p>', html_content, re.DOTALL)
if cordial_match:
specific_content.append(pre_clean_html(cordial_match.group(0)))
if specific_content:
text_content = '\n'.join(specific_content)
# Supprimer les duplications de lignes
lines = text_content.split('\n')
unique_lines = []
for line in lines:
if line not in unique_lines:
unique_lines.append(line)
text_content = '\n'.join(unique_lines)
# Ajouter les images à la fin
if image_markdowns:
# Supprimer les doublons d'images
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
text_content += "\n\n" + "\n".join(unique_images)
return text_content if text_content else "*Contenu non extractible*"
except Exception as e:
print(f"Erreur lors de l'extraction du texte: {e}")
# 3. 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)
text = re.sub(r'\n{3,}', '\n\n', text)
# Préserver les images si demandé
if preserve_images or True: # Toujours préserver les images
# Les images ont déjà été extraites au début de la fonction
if image_markdowns:
# Supprimer les doublons d'images
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
text += "\n\n" + "\n".join(unique_images)
# Si on a du contenu, le retourner
if text and len(text.strip()) > 5:
return text
except Exception as e:
print(f"Erreur lors de l'extraction générique: {e}")
# Si rien n'a fonctionné mais qu'on a des images, au moins les retourner
if image_markdowns:
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
if any("Je ne parviens pas à accéder" in html_content for img in image_markdowns):
return "Bonjour,\nJe ne parviens pas à accéder au l'essai au bleu :\n\n" + "\n".join(unique_images) + "\n\nMerci par avance pour votre.\nCordialement"
else:
return "Images extraites :\n\n" + "\n".join(unique_images)
return "*Contenu non extractible*"
except Exception as e:
print(f"Erreur lors de l'extraction complexe: {e}")
# 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 src.startswith('http'):
image_markdowns.append(f"![Image]({src})")
# Extraire du texte significatif
text_parts = []
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)))
content_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder.*?</p>', html_content, re.DOTALL)
if content_match:
text_parts.append(pre_clean_html(content_match.group(0)))
# Combiner texte et images
if text_parts or image_markdowns:
result = ""
if text_parts:
result += "\n".join(text_parts) + "\n\n"
if image_markdowns:
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
result += "\n".join(unique_images)
return result
except Exception:
pass
return "*Contenu non extractible*"
def pre_clean_html(html_content):
"""
Fonction interne pour nettoyer le HTML basique avant traitement avancé.
Args:
html_content: Contenu HTML à pré-nettoyer
Returns:
Texte avec les balises HTML basiques retirées
"""
if not html_content:
return ""
# Remplacer les balises <br>, <p>, <div> par des sauts de ligne
content = html_content.replace('<br>', '\n').replace('<br/>', '\n').replace('<br />', '\n')
content = content.replace('</p>', '\n').replace('</div>', '\n')
# Préserver les URLs des images
image_urls = []
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', content)
for match in img_matches:
if '/web/image/' in match.group(1) or match.group(1).startswith('http'):
image_urls.append(match.group(1))
# Supprimer les balises HTML
content = re.sub(r'<[^>]*>', '', content)
# Supprimer les espaces multiples
content = re.sub(r' {2,}', ' ', content)
# Supprimer les sauts de ligne multiples
content = re.sub(r'\n{3,}', '\n\n', content)
# Décoder 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 tabulations
content = content.replace('\t', ' ')
# Ajouter les images préservées à la fin
if image_urls:
content += "\n\n"
for url in image_urls:
content += f"![Image]({url})\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>![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)