File size: 5,138 Bytes
28a08e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""
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)[^>]*>.*?</\1>",
                  " ", 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