""" Prétraitement du texte pour la classification de produits Rakuten. Ce module fournit des fonctions de nettoyage et préparation du texte, basées sur le pipeline NLP développé par l'équipe dans build_features_nlp.py. Étapes de prétraitement: 1. Nettoyage HTML (unescape, suppression tags) 2. Normalisation (lowercase, suppression caractères spéciaux) 3. Gestion des nombres 4. Concaténation designation + description """ import re import html from typing import Optional, Tuple import unicodedata def clean_text(text: str) -> str: """ Nettoie un texte brut pour la classification. Applique les transformations suivantes: - Décodage des entités HTML - Suppression des balises HTML - Conversion en minuscules - Normalisation des espaces - Suppression des caractères spéciaux excessifs Args: text: Texte brut à nettoyer Returns: Texte nettoyé """ if not text or not isinstance(text, str): return "" # Décoder les entités HTML (& -> &, etc.) text = html.unescape(text) # Supprimer les balises HTML text = _remove_html_tags(text) # Convertir en minuscules text = text.lower() # Normaliser les espaces text = _normalize_whitespace(text) # Supprimer les accents problématiques (optionnel, configurable) # text = _remove_accents(text) return text.strip() def _remove_html_tags(text: str) -> str: """ Supprime les balises HTML d'un texte. Args: text: Texte avec potentiellement des balises HTML Returns: Texte sans balises HTML """ # Pattern pour matcher les balises HTML html_pattern = re.compile(r'<[^>]+>') text = html_pattern.sub(' ', text) # Supprimer les commentaires HTML comment_pattern = re.compile(r'', re.DOTALL) text = comment_pattern.sub(' ', text) return text def _normalize_whitespace(text: str) -> str: """ Normalise les espaces dans un texte. Args: text: Texte avec espaces irréguliers Returns: Texte avec espaces normalisés """ # Remplacer les caractères de contrôle par des espaces text = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', text) # Normaliser tous les types d'espaces text = re.sub(r'\s+', ' ', text) return text def _remove_accents(text: str) -> str: """ Supprime les accents d'un texte. Note: Cette fonction est optionnelle et peut affecter la qualité de la classification pour le français. Args: text: Texte avec accents Returns: Texte sans accents """ nfkd_form = unicodedata.normalize('NFKD', text) return ''.join(c for c in nfkd_form if not unicodedata.combining(c)) def preprocess_product_text( designation: str, description: Optional[str] = None, remove_numbers: bool = False ) -> str: """ Prétraite le texte complet d'un produit Rakuten. Combine et nettoie la désignation et la description du produit selon le pipeline établi par l'équipe NLP. Args: designation: Titre/nom du produit (obligatoire) description: Description détaillée (optionnel) remove_numbers: Si True, supprime les tokens contenant des chiffres Returns: Texte nettoyé et combiné prêt pour la vectorisation Example: >>> text = preprocess_product_text( ... "Livre Harry Potter", ... "Roman fantastique pour enfants" ... ) >>> print(text) "livre harry potter . -//- roman fantastique pour enfants" """ # Nettoyer la désignation clean_designation = clean_text(designation or "") # Nettoyer la description clean_description = clean_text(description or "") # Appliquer les remplacements spécifiques du pipeline Rakuten clean_designation = _apply_rakuten_replacements(clean_designation) clean_description = _apply_rakuten_replacements(clean_description) # Supprimer les mots avec chiffres si demandé if remove_numbers: clean_designation = _remove_words_with_numbers(clean_designation) clean_description = _remove_words_with_numbers(clean_description) # Combiner avec le séparateur du pipeline original if clean_description: combined = f"{clean_designation} . -//- {clean_description}" else: combined = clean_designation return combined.strip() def _apply_rakuten_replacements(text: str) -> str: """ Applique les remplacements spécifiques au pipeline Rakuten. Basé sur le code de build_features_nlp.py. Args: text: Texte à transformer Returns: Texte avec remplacements appliqués """ # Remplacer n° par numéro text = re.sub(r'n°', ' numéro ', text) # Ajouter des espaces autour de la ponctuation # (sauf pour certains caractères spéciaux) text = re.sub(r"[^\d\w\s¿?'\-]", r' \g<0> ', text) # Normaliser les espaces après les remplacements text = _normalize_whitespace(text) return text def _remove_words_with_numbers(text: str) -> str: """ Supprime les mots contenant des chiffres. Args: text: Texte à traiter Returns: Texte sans les mots contenant des chiffres """ pattern = r'\b\S*[0-9]+\S*\b' return re.sub(pattern, '', text) def detect_language_simple(text: str) -> str: """ Détection simplifiée de la langue (heuristique). Pour une détection plus précise, utiliser langid. Cette fonction est un fallback rapide. Args: text: Texte à analyser Returns: Code langue probable ('fr', 'en', 'de', etc.) ou 'unknown' """ if not text or len(text) < 10: return "unknown" text_lower = text.lower() # Mots indicateurs par langue french_indicators = [ 'le', 'la', 'les', 'de', 'du', 'des', 'un', 'une', 'et', 'est', 'pour', 'avec', 'dans', 'sur', 'par', 'qui', 'que', 'ce', 'cette' ] english_indicators = [ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with' ] german_indicators = [ 'der', 'die', 'das', 'ein', 'eine', 'und', 'ist', 'sind', 'für', 'mit', 'auf', 'bei', 'nach', 'von', 'zu' ] words = set(re.findall(r'\b\w+\b', text_lower)) fr_score = len(words & set(french_indicators)) en_score = len(words & set(english_indicators)) de_score = len(words & set(german_indicators)) scores = {'fr': fr_score, 'en': en_score, 'de': de_score} best_lang = max(scores, key=scores.get) if scores[best_lang] >= 2: return best_lang return "unknown" def validate_text_input( designation: str, description: Optional[str] = None, min_length: int = 3, max_length: int = 5000 ) -> Tuple[bool, str]: """ Valide les entrées texte d'un produit. Args: designation: Titre du produit description: Description du produit min_length: Longueur minimale de la désignation max_length: Longueur maximale totale Returns: Tuple (is_valid, error_message) """ if not designation or not designation.strip(): return False, "La désignation du produit est obligatoire" if len(designation.strip()) < min_length: return False, f"La désignation doit contenir au moins {min_length} caractères" total_length = len(designation) + len(description or "") if total_length > max_length: return False, f"Le texte total ne doit pas dépasser {max_length} caractères" return True, ""