mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-15 21:46:52 +01:00
308 lines
11 KiB
Python
308 lines
11 KiB
Python
"""
|
|
Utilitaires généraux pour l'extraction de tickets.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
import re
|
|
from typing import Dict, Any, List, Optional, Union
|
|
from html import unescape
|
|
from bs4 import BeautifulSoup, Tag
|
|
import html2text
|
|
import unicodedata
|
|
|
|
def setup_logging(level: Union[str, int] = logging.INFO, log_file: Optional[str] = None) -> None:
|
|
"""
|
|
Configure la journalisation avec un format spécifique et éventuellement un fichier de logs.
|
|
|
|
Args:
|
|
level: Niveau de journalisation en tant que chaîne (ex: "INFO", "DEBUG") ou valeur entière (default: logging.INFO)
|
|
log_file: Chemin du fichier de log (default: None)
|
|
"""
|
|
# Convertir le niveau de log si c'est une chaîne
|
|
if isinstance(level, str):
|
|
numeric_level = getattr(logging, level.upper(), None)
|
|
if not isinstance(numeric_level, int):
|
|
raise ValueError(f"Niveau de journalisation invalide: {level}")
|
|
else:
|
|
numeric_level = level
|
|
|
|
logging.basicConfig(
|
|
level=numeric_level,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
|
|
# Ajout d'un gestionnaire de fichier si log_file est spécifié
|
|
if log_file:
|
|
# S'assurer que le répertoire existe
|
|
log_dir = os.path.dirname(log_file)
|
|
if log_dir and not os.path.exists(log_dir):
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
file_handler.setLevel(numeric_level)
|
|
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', '%Y-%m-%d %H:%M:%S')
|
|
file_handler.setFormatter(file_formatter)
|
|
logging.getLogger().addHandler(file_handler)
|
|
|
|
def log_separator(length: int = 60) -> None:
|
|
"""
|
|
Ajoute une ligne de séparation dans les logs.
|
|
|
|
Args:
|
|
length: Longueur de la ligne (default: 60)
|
|
"""
|
|
logging.info("-" * length)
|
|
|
|
def save_json(data: Any, file_path: str) -> bool:
|
|
"""
|
|
Sauvegarde des données au format JSON dans un fichier.
|
|
|
|
Args:
|
|
data: Données à sauvegarder
|
|
file_path: Chemin du fichier
|
|
|
|
Returns:
|
|
True si la sauvegarde a réussi, False sinon
|
|
"""
|
|
try:
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
return True
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de la sauvegarde du fichier JSON {file_path}: {e}")
|
|
return False
|
|
|
|
def save_text(text: str, file_path: str) -> bool:
|
|
"""
|
|
Sauvegarde du texte dans un fichier.
|
|
|
|
Args:
|
|
text: Texte à sauvegarder
|
|
file_path: Chemin du fichier
|
|
|
|
Returns:
|
|
True si la sauvegarde a réussi, False sinon
|
|
"""
|
|
try:
|
|
# S'assurer que le répertoire existe
|
|
directory = os.path.dirname(file_path)
|
|
if directory and not os.path.exists(directory):
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
f.write(text)
|
|
return True
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de la sauvegarde du fichier texte {file_path}: {e}")
|
|
return False
|
|
|
|
def is_important_image(tag: Tag, message_text: str) -> bool:
|
|
"""
|
|
Détermine si une image est importante ou s'il s'agit d'un logo/signature.
|
|
|
|
Args:
|
|
tag: La balise d'image à analyser
|
|
message_text: Le texte complet du message pour contexte
|
|
|
|
Returns:
|
|
True si l'image semble importante, False sinon
|
|
"""
|
|
# Vérifier les attributs de l'image
|
|
src = str(tag.get('src', ''))
|
|
alt = str(tag.get('alt', ''))
|
|
title = str(tag.get('title', ''))
|
|
|
|
# Patterns pour les images inutiles
|
|
useless_img_patterns = [
|
|
'logo', 'signature', 'outlook', 'footer', 'header', 'icon',
|
|
'emoticon', 'emoji', 'cid:', 'pixel', 'spacer', 'vignette',
|
|
'banner', 'separator', 'decoration', 'mail_signature'
|
|
]
|
|
|
|
# Vérifier si c'est une image inutile
|
|
for pattern in useless_img_patterns:
|
|
if (pattern in src.lower() or
|
|
pattern in alt.lower() or
|
|
pattern in title.lower()):
|
|
return False
|
|
|
|
# Vérifier la taille
|
|
width_str = str(tag.get('width', ''))
|
|
height_str = str(tag.get('height', ''))
|
|
|
|
try:
|
|
if width_str.isdigit() and height_str.isdigit():
|
|
width = int(width_str)
|
|
height = int(height_str)
|
|
if width <= 50 and height <= 50:
|
|
return False
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Vérifier si l'image est mentionnée dans le texte
|
|
image_indicators = [
|
|
'capture', 'screenshot', 'image', 'photo', 'illustration',
|
|
'voir', 'regarder', 'ci-joint', 'écran', 'erreur', 'problème',
|
|
'bug', 'pièce jointe', 'attachment', 'veuillez trouver'
|
|
]
|
|
|
|
for indicator in image_indicators:
|
|
if indicator in message_text.lower():
|
|
return True
|
|
|
|
return True
|
|
|
|
def clean_html(html_content: str,
|
|
strategy: str = "html2text",
|
|
preserve_links: bool = False,
|
|
preserve_images: bool = False) -> str:
|
|
"""
|
|
Nettoie le contenu HTML et le convertit en texte selon la stratégie spécifiée.
|
|
|
|
Args:
|
|
html_content: Contenu HTML à nettoyer
|
|
strategy: Stratégie de nettoyage ('strip_tags', 'html2text', 'soup') (default: 'html2text')
|
|
preserve_links: Conserver les liens dans la version texte (default: False)
|
|
preserve_images: Conserver les références aux images (default: False)
|
|
|
|
Returns:
|
|
Texte nettoyé
|
|
"""
|
|
if not html_content:
|
|
return ""
|
|
|
|
# Remplacer les balises br par des sauts de ligne
|
|
html_content = re.sub(r'<br\s*/?>|<BR\s*/?>', '\n', html_content)
|
|
|
|
if strategy == "strip_tags":
|
|
# Solution simple: suppression des balises HTML
|
|
text = re.sub(r'<[^>]+>', '', html_content)
|
|
# Nettoyer les espaces multiples et les lignes vides multiples
|
|
text = re.sub(r'\s+', ' ', text)
|
|
text = re.sub(r'\n\s*\n', '\n\n', text)
|
|
return text.strip()
|
|
|
|
elif strategy == "html2text":
|
|
# Utiliser html2text pour une meilleure conversion
|
|
h = html2text.HTML2Text()
|
|
h.ignore_links = not preserve_links
|
|
h.ignore_images = not preserve_images
|
|
h.body_width = 0 # Ne pas limiter la largeur du texte
|
|
return h.handle(html_content).strip()
|
|
|
|
elif strategy == "soup":
|
|
# Utiliser BeautifulSoup pour un nettoyage plus avancé
|
|
try:
|
|
soup = BeautifulSoup(html_content, 'html.parser')
|
|
|
|
# Préserver les liens si demandé
|
|
if preserve_links:
|
|
for a_tag in soup.find_all('a', href=True):
|
|
if isinstance(a_tag, Tag):
|
|
href = a_tag.get('href', '')
|
|
new_text = f"{a_tag.get_text()} [{href}]"
|
|
new_tag = soup.new_string(new_text)
|
|
a_tag.replace_with(new_tag)
|
|
|
|
# Préserver les images si demandé
|
|
if preserve_images:
|
|
for img_tag in soup.find_all('img'):
|
|
if isinstance(img_tag, Tag):
|
|
src = img_tag.get('src', '')
|
|
alt = img_tag.get('alt', '')
|
|
new_text = f"[Image: {alt} - {src}]"
|
|
new_tag = soup.new_string(new_text)
|
|
img_tag.replace_with(new_tag)
|
|
|
|
# Convertir les listes en texte formaté
|
|
for ul in soup.find_all('ul'):
|
|
if isinstance(ul, Tag):
|
|
for li in ul.find_all('li'):
|
|
if isinstance(li, Tag):
|
|
li_text = li.get_text()
|
|
new_text = f"• {li_text}"
|
|
new_tag = soup.new_string(new_text)
|
|
li.replace_with(new_tag)
|
|
|
|
for ol in soup.find_all('ol'):
|
|
if isinstance(ol, Tag):
|
|
for i, li in enumerate(ol.find_all('li')):
|
|
if isinstance(li, Tag):
|
|
li_text = li.get_text()
|
|
new_text = f"{i+1}. {li_text}"
|
|
new_tag = soup.new_string(new_text)
|
|
li.replace_with(new_tag)
|
|
|
|
text = soup.get_text()
|
|
# Nettoyer les espaces et les lignes vides
|
|
text = re.sub(r'\n\s*\n', '\n\n', text)
|
|
return text.strip()
|
|
except Exception as e:
|
|
logging.warning(f"Erreur lors du nettoyage HTML avec BeautifulSoup: {e}")
|
|
# En cas d'erreur, utiliser une méthode de secours
|
|
return clean_html(html_content, "strip_tags")
|
|
|
|
else:
|
|
# Stratégie par défaut
|
|
logging.warning(f"Stratégie de nettoyage '{strategy}' inconnue, utilisation de 'strip_tags'")
|
|
return clean_html(html_content, "strip_tags")
|
|
|
|
def detect_duplicate_content(messages: List[Dict[str, Any]]) -> List[int]:
|
|
"""
|
|
Détecte les messages avec un contenu dupliqué et retourne leurs indices.
|
|
|
|
Args:
|
|
messages: Liste de messages à analyser
|
|
|
|
Returns:
|
|
Liste des indices des messages dupliqués
|
|
"""
|
|
content_map = {}
|
|
duplicate_indices = []
|
|
|
|
for idx, message in enumerate(messages):
|
|
body = message.get("body", "")
|
|
if not body:
|
|
continue
|
|
|
|
# Nettoyer le contenu HTML pour la comparaison
|
|
cleaned_content = clean_html(body, "strip_tags")
|
|
# Considérer uniquement les messages avec du contenu significatif
|
|
if len(cleaned_content.strip()) < 10:
|
|
continue
|
|
|
|
# Vérifier si le contenu existe déjà
|
|
if cleaned_content in content_map:
|
|
duplicate_indices.append(idx)
|
|
else:
|
|
content_map[cleaned_content] = idx
|
|
|
|
return duplicate_indices
|
|
|
|
def normalize_filename(name: str) -> str:
|
|
"""
|
|
Normalise un nom de fichier en remplaçant les caractères non autorisés.
|
|
|
|
Args:
|
|
name: Nom à normaliser
|
|
|
|
Returns:
|
|
Nom normalisé
|
|
"""
|
|
# Enlever les accents
|
|
name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('ASCII')
|
|
|
|
# Remplacer les caractères non alphanumériques par des underscores
|
|
name = re.sub(r'[^\w\.-]', '_', name)
|
|
|
|
# Limiter la longueur à 255 caractères (limitation commune des systèmes de fichiers)
|
|
# Remplacer les caractères non autorisés par des underscores
|
|
sanitized = re.sub(r'[\\/*?:"<>|]', '_', name)
|
|
# Limiter la longueur du nom à 100 caractères
|
|
if len(sanitized) > 100:
|
|
sanitized = sanitized[:97] + "..."
|
|
return sanitized.strip()
|