""" content_cleaner.py — Pulisce e struttura il testo per il modello. W-NAV: aggiunto extract_with_trafilatura() come primary extractor. Strategia: 1. trafilatura.extract() — algoritmo Readability-quality, puro Python 2. Fallback: strip HTML tags + clean_and_structure() esistente (regex-based) se trafilatura non installato o restituisce < 200 chars Problematiche gestite: - trafilatura su pagine non-article (login, 404, SPA vuota) → None → fallback - ImportError: trafilatura non installato → fallback silenzioso - Fallback con HTML grezzo: strip tag prima di passare a clean_and_structure (evita che HTML residuo finisca nel testo inviato all'LLM) - favor_recall=True: preferisce recall a precision (meglio più testo che meno) - include_tables=True: tabelle importanti per dati tecnici/finanziari - deduplicate=True: rimuove paragrafi boilerplate duplicati (footer, nav) """ import re from typing import Optional NOISE_PATTERNS = [ "cookie", "accept all cookies", "privacy policy", "terms of service", "all rights reserved", "subscribe", "follow us on", "share this article", ] def remove_noise(text: str) -> str: lines = text.split("\n") cleaned = [] for line in lines: ll = line.lower().strip() if len(ll) < 4: continue if any(p in ll for p in NOISE_PATTERNS): continue if line.count("|") > 5: continue cleaned.append(line) return "\n".join(cleaned) def extract_key_paragraphs(text: str, query: str, max_paragraphs: int = 6) -> list[str]: q_words = set(w.lower() for w in re.split(r"\W+", query) if len(w) > 3) pars = [p.strip() for p in re.split(r"\n{2,}", text) if len(p.strip()) > 60] def rel(p: str) -> float: pl = p.lower() return sum(1 for w in q_words if w in pl) / max(1, len(q_words)) return sorted(pars, key=rel, reverse=True)[:max_paragraphs] def _strip_html(html: str) -> str: """Strip HTML tags — usato nel fallback di extract_with_trafilatura.""" text = re.sub(r"<(script|style|nav|footer|header|noscript|aside)[^>]*>.*?", " ", html, flags=re.S | re.I) text = re.sub(r"<[^>]+>", " ", text) text = re.sub(r"[ \t]+", " ", text) text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() def clean_and_structure(text: str, query: Optional[str] = None, max_chars: int = 4000) -> dict: text = remove_noise(text) pars = extract_key_paragraphs(text, query) if query else [text] structured = "\n\n".join(pars) if len(structured) > max_chars: structured = structured[:max_chars] + "\n[troncato]" return { "content": structured, "chars": len(structured), "words": len(structured.split()), "query_used": query, "extractor": "regex", } def extract_with_trafilatura( html: str, url: str = "", query: Optional[str] = None, max_chars: int = 5000, ) -> dict: """ Primary extractor: trafilatura (Readability-quality, puro Python). Fallback a clean_and_structure() con strip HTML preventivo se trafilatura non disponibile o restituisce contenuto insufficiente. Parametri: html — HTML grezzo della pagina (non testo pre-pulito) url — URL originale (aiuta trafilatura a contestualizzare il dominio) query — query opzionale per filtrare paragrafi rilevanti max_chars — limite caratteri output """ try: import trafilatura # type: ignore[import-untyped] extracted = trafilatura.extract( html, url=url or None, include_comments=False, include_tables=True, include_images=False, deduplicate=True, favor_recall=True, ) if extracted and len(extracted.strip()) > 200: # Filtra per rilevanza solo su testi lunghi (>1000 chars) # Su testi brevi il ranking riduce troppo il contenuto utile if query and len(extracted) > 1000: pars = extract_key_paragraphs(extracted, query, max_paragraphs=8) structured = "\n\n".join(pars) else: structured = extracted if len(structured) > max_chars: structured = structured[:max_chars] + "\n[troncato]" return { "content": structured, "chars": len(structured), "words": len(structured.split()), "query_used": query, "extractor": "trafilatura", } except ImportError: pass # trafilatura non installato except Exception: pass # HTML malformato o altro errore inatteso # Fallback: strip HTML poi regex-based cleaner # CRITICO: strip tag prima di passare a clean_and_structure — # altrimenti i tag HTML finiscono nel testo inviato all'LLM stripped = _strip_html(html) result = clean_and_structure(text=stripped, query=query, max_chars=max_chars) result["extractor"] = "regex" return result