mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-15 22:06:50 +01:00
503 lines
21 KiB
Python
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"")
|
|
|
|
# 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"")
|
|
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"")
|
|
|
|
# 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"")
|
|
|
|
# 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(' ', ' ')
|
|
content = content.replace('<', '<')
|
|
content = content.replace('>', '>')
|
|
content = content.replace('&', '&')
|
|
content = content.replace('"', '"')
|
|
|
|
# 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"\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) |