feat(pipeline): 10 nuovi transform e metriche residui estese
- 0_br: rimozione tag <br> residui da tabelle PDF - 0_tabsep: rimozione separatori | | e |---| (doppio pass pre/post merge) - 0a2: correzione encoding " → × (moltiplicazione, solo digit-before) - 0a3: correzione encoding ! → µ prima di unità SI - 0a4: rimozione label formule inline [N.M] - 9c: filtro garbage headers — simboli puri, abbreviazioni brevi, prefisso ... - 9d: rimozione sezioni frontmatter (URL, email, copyright, affiliazione) - build_report: tracking esteso br_inline, simboli_encoding, formule_inline
This commit is contained in:
+123
-12
@@ -349,6 +349,12 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
||||
"toc_rimosso": False,
|
||||
"n_immagini_rimosse": 0,
|
||||
"n_accenti_corretti": 0,
|
||||
"n_moltiplicazioni_corrette": 0,
|
||||
"n_micro_corretti": 0,
|
||||
"n_br_rimossi": 0,
|
||||
"n_formule_rimossi": 0,
|
||||
"n_garbage_headers_rimossi": 0,
|
||||
"n_frontmatter_rimossi": 0,
|
||||
"n_dotleader_rimossi": 0,
|
||||
"n_header_concat_fixati": 0,
|
||||
"n_articoli_estratti": 0,
|
||||
@@ -357,12 +363,26 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
||||
"n_header_allcaps": 0,
|
||||
"n_sezioni_numerate": 0,
|
||||
"n_paragrafi_uniti": 0,
|
||||
"n_tabsep_rimossi": 0,
|
||||
}
|
||||
|
||||
# 0. Rimuovi riferimenti immagini (artefatti opendataloader-pdf)
|
||||
stats["n_immagini_rimosse"] = len(re.findall(r"!\[[^\]]*\]\([^)]*\)", text))
|
||||
text = re.sub(r"!\[[^\]]*\]\([^)]*\)\s*", "", text)
|
||||
|
||||
# 0_br. Rimuovi tag <br> residui da tabelle e blocchi formula PDF
|
||||
# Nelle celle di tabella produce spazio; nel testo inline elimina rumore.
|
||||
stats["n_br_rimossi"] = len(re.findall(r"<br>", text, re.IGNORECASE))
|
||||
text = re.sub(r"<br>\s*", " ", text, flags=re.IGNORECASE)
|
||||
|
||||
# 0_tabsep. Rimuovi separatori tabella PDF: "| |" (riga vuota) e "|---|" (separatore).
|
||||
# Nascono da tabelle non strutturate nel PDF. Rimossi PRIMA del merge paragrafi
|
||||
# (step 5) altrimenti "|---|" viene fuso con il paragrafo successivo producendo
|
||||
# righe tipo "|---| Una caratterizzazione analoga...".
|
||||
_pat_tabsep = re.compile(r"(?m)^\|\s*\|\s*$|^\|---\|?\s*$")
|
||||
stats["n_tabsep_rimossi"] = len(_pat_tabsep.findall(text))
|
||||
text = _pat_tabsep.sub("", text)
|
||||
|
||||
# 0a. Fix artefatti backtick da PDF LaTeX: `e→è, e`→è, sar`a→sarà, ecc.
|
||||
# I PDF prodotti da LaTeX estraggono gli accenti gravi come backtick separati
|
||||
# dalla vocale accentata. Esempi: "`e" → "è", "puo`" → "può", "sar`a" → "sarà"
|
||||
@@ -383,6 +403,30 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
||||
text = re.sub(r"`", "", text)
|
||||
stats["n_accenti_corretti"] += n_bt_orfani
|
||||
|
||||
# 0a2. Fix segno di moltiplicazione "→× (encoding font PDF non-standard)
|
||||
# Esempi: 2"107 → 2×107, 2"(10-2 m)3 → 2×(10-2 m)3
|
||||
# Lookbehind SOLO su cifra: evita falsi positivi tipo t1"t0 (→ limite)
|
||||
# o h"hf (→ differenza) dove la lettera prima della " non indica prodotto.
|
||||
_n_cross = len(re.findall(r'(?<=[0-9])"(?=[0-9(])', text))
|
||||
text = re.sub(r'(?<=[0-9])"(?=[0-9(])', '×', text)
|
||||
stats["n_moltiplicazioni_corrette"] = _n_cross
|
||||
|
||||
# 0a3. Fix prefisso micro !→µ prima di unità SI note
|
||||
# "1 !m" → "1 µm", "1 !A" → "1 µA", "3 !s-1" → "3 µs-1"
|
||||
# Pattern stretto: cifra + spazio opzionale + ! + lettera unità SI a scelta ristretta.
|
||||
# Non tocca "4! steradianti" (spazio dopo !) né "mol!K" (non preceduto da cifra).
|
||||
_SI_UNITS_RE = r'[mAsgVWFHTKNJClΩ]'
|
||||
_n_micro = len(re.findall(rf'\d\s*!(?={_SI_UNITS_RE})', text))
|
||||
text = re.sub(rf'(\d)\s*!({_SI_UNITS_RE})', r'\1 µ\2', text)
|
||||
stats["n_micro_corretti"] = _n_micro
|
||||
|
||||
# 0a4. Rimuovi label formule inline [N.M] — es. [3.4], [10.7], [5.25]
|
||||
# Non aggiungono valore semantico per il RAG; restano come rumore numerico.
|
||||
# Preserva [N] senza punto (riferimenti bibliografici/note legittime).
|
||||
n_form_before = len(re.findall(r"\[\d+\.\d+\]", text))
|
||||
text = re.sub(r"\s*\[\d+\.\d+\]\s*", " ", text)
|
||||
stats["n_formule_rimossi"] = n_form_before
|
||||
|
||||
# 0b_pre. Rimuovi righe con dot-leader (voci di indice/sommario)
|
||||
# Esempi: "- 1.1 Alfabeto greco . . . . . . 1", "3.4 Continuità . . . . 205"
|
||||
# Pattern: almeno 3 occorrenze di ". " consecutive nella riga
|
||||
@@ -624,6 +668,9 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
||||
i += 1
|
||||
text = "\n\n".join(merged)
|
||||
|
||||
# Secondo pass: rimuovi prefisso |---| eventualmente rimasto dopo il merge paragrafi
|
||||
text = re.sub(r"(?m)^\|---\|\s*", "", text)
|
||||
|
||||
# 6. Normalizza whitespace multiplo interno alle righe
|
||||
lines = text.split("\n")
|
||||
text = "\n".join(
|
||||
@@ -655,6 +702,61 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
||||
text, n_titoli = _merge_title_headers(text)
|
||||
stats["n_titoli_uniti"] = n_titoli
|
||||
|
||||
# 9c. Rimuovi garbage headers: header ### senza parole reali o con solo
|
||||
# abbreviazioni matematiche. Esempi: "### ( vm)", "### #", "### ! =",
|
||||
# "### (am)", "### 2. Il valore di hf si deter- mina risolvendo mg(h!hf)"
|
||||
# Questi nascono da espressioni matematiche scambiate per titoli di sezione.
|
||||
# Il corpo rimane nel testo e viene accorpato alla sezione precedente.
|
||||
def _is_garbage_header(content: str) -> bool:
|
||||
# Header con prefisso "..." — frammento di formula (es. "...Di", "...vi")
|
||||
if content.lstrip().startswith("..."):
|
||||
return True
|
||||
# Nessuna sequenza alfabetica ≥ 2 char
|
||||
if not re.search(r"[A-Za-zÀ-ÿ]{2,}", content):
|
||||
return True
|
||||
# Abbreviazione corta in parentesi opzionali: "(vm)", "( am)", "(am)"
|
||||
if re.fullmatch(r"\(?\s*[A-Za-z]{1,4}\s*\)?", content.strip()):
|
||||
return True
|
||||
# Header molto lungo (>60ch) con artefatti formula inline
|
||||
if len(content) > 60 and re.search(r"[!%#]\w|\w[!%#]|\b\w+-\s*\w", content):
|
||||
return True
|
||||
return False
|
||||
|
||||
lines = text.split("\n")
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
m = re.match(r"^#{1,6} (.+)$", line)
|
||||
if m and _is_garbage_header(m.group(1)):
|
||||
stats["n_garbage_headers_rimossi"] += 1
|
||||
continue
|
||||
new_lines.append(line)
|
||||
text = "\n".join(new_lines)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
|
||||
# 9d. Rimuovi sezioni frontmatter: header senza numero + corpo corto con
|
||||
# URL, email, affiliazione, copyright, edizione — metadati non-contenuto.
|
||||
_FM_RE = re.compile(
|
||||
r"https?://|www\.|@[A-Za-z]|\bUniversit[àa]\b|\bDipartimento\b|"
|
||||
r"\bCopyright\b|\bLicenza\b|\bEdizione\b|"
|
||||
r"protetto da|tutti i diritti",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
blocks = re.split(r"\n{2,}", text)
|
||||
cleaned = []
|
||||
for i, block in enumerate(blocks):
|
||||
stripped = block.strip()
|
||||
if not re.match(r"^### ", stripped) or re.match(r"^### \d", stripped):
|
||||
cleaned.append(block)
|
||||
continue
|
||||
body = blocks[i + 1].strip() if i + 1 < len(blocks) else ""
|
||||
is_fm_body = len(body) < 250 and _FM_RE.search(body)
|
||||
is_fm_hdr = _FM_RE.search(stripped)
|
||||
if is_fm_body or is_fm_hdr:
|
||||
stats["n_frontmatter_rimossi"] += 1
|
||||
continue
|
||||
cleaned.append(block)
|
||||
text = re.sub(r"\n{3,}", "\n\n", "\n\n".join(cleaned))
|
||||
|
||||
return text, stats
|
||||
|
||||
|
||||
@@ -831,10 +933,13 @@ def build_report(
|
||||
return hits
|
||||
|
||||
residui = {
|
||||
"backtick": _scan(r"`"),
|
||||
"dotleader": _scan(r"(?:\. ){3,}"),
|
||||
"url": _scan(r"^(https?://|www\.)\S+"),
|
||||
"immagini": _scan(r"!\[[^\]]*\]\([^)]*\)"),
|
||||
"backtick": _scan(r"`"),
|
||||
"dotleader": _scan(r"(?:\. ){3,}"),
|
||||
"url": _scan(r"^(https?://|www\.)\S+"),
|
||||
"immagini": _scan(r"!\[[^\]]*\]\([^)]*\)"),
|
||||
"br_inline": _scan(r"<br>"),
|
||||
"simboli_encoding":_scan(r'(?<=[0-9A-Za-z])[!"](?=[0-9A-Za-z])'),
|
||||
"formule_inline": _scan(r"\[\d+\.\d+\]"),
|
||||
}
|
||||
|
||||
# ── Composizione report ───────────────────────────────────────────────
|
||||
@@ -856,14 +961,20 @@ def build_report(
|
||||
"long_sections_list": long_secs,
|
||||
},
|
||||
"residui": {
|
||||
"backtick": len(residui["backtick"]),
|
||||
"dotleader": len(residui["dotleader"]),
|
||||
"url": len(residui["url"]),
|
||||
"immagini": len(residui["immagini"]),
|
||||
"backtick_esempi": residui["backtick"],
|
||||
"dotleader_esempi": residui["dotleader"],
|
||||
"url_esempi": residui["url"],
|
||||
"immagini_esempi": residui["immagini"],
|
||||
"backtick": len(residui["backtick"]),
|
||||
"dotleader": len(residui["dotleader"]),
|
||||
"url": len(residui["url"]),
|
||||
"immagini": len(residui["immagini"]),
|
||||
"br_inline": len(residui["br_inline"]),
|
||||
"simboli_encoding": len(residui["simboli_encoding"]),
|
||||
"formule_inline": len(residui["formule_inline"]),
|
||||
"backtick_esempi": residui["backtick"],
|
||||
"dotleader_esempi": residui["dotleader"],
|
||||
"url_esempi": residui["url"],
|
||||
"immagini_esempi": residui["immagini"],
|
||||
"br_inline_esempi": residui["br_inline"],
|
||||
"simboli_encoding_esempi": residui["simboli_encoding"],
|
||||
"formule_inline_esempi": residui["formule_inline"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user