3d9ed0141c
Analisi automatica pagina per pagina: score 0–100, sillabazioni, layout a colonne, Unicode anomali, intestazioni/piè ripetitivi. Report salvato in step-1/<stem>_step1_report.txt (escluso da git).
200 lines
7.5 KiB
Python
200 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Step 1 — Ispezione automatica PDF
|
||
|
||
Analizza il PDF pagina per pagina e produce un report con score (0–100)
|
||
e lista dei problemi per pagina. Serve per capire la qualità del documento
|
||
e mappare i problemi prima della revisione manuale (step 4).
|
||
|
||
Uso:
|
||
python step1/inspect.py
|
||
|
||
Output:
|
||
step1/<nome_pdf>_step1_report.txt
|
||
"""
|
||
|
||
import re
|
||
import sys
|
||
import statistics
|
||
from collections import Counter
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
|
||
# ── Penalità per il calcolo dello score ───────────────────────────────────
|
||
SYLLABIF_PENALTY = 0.3 # per occorrenza di sillabazione
|
||
COLUMN_PENALTY = 3.0 # per pagina con layout a colonne
|
||
UNICODE_PENALTY = 1.5 # per pagina con caratteri anomali
|
||
EMPTY_PENALTY = 1.0 # per pagina vuota
|
||
HEADER_FOOTER_PEN = 5.0 # fisso se intestazioni/piè ripetitivi rilevati
|
||
|
||
|
||
def inspect_pdf(pdf_path: str, save: bool = True) -> None:
|
||
try:
|
||
import pdfplumber
|
||
except ImportError:
|
||
print("Errore: pdfplumber non è installato.")
|
||
print(" pip install pdfplumber")
|
||
sys.exit(1)
|
||
|
||
path = Path(pdf_path)
|
||
if not path.exists():
|
||
print(f"Errore: file non trovato — {pdf_path}")
|
||
sys.exit(1)
|
||
|
||
lines = []
|
||
|
||
def out(text=""):
|
||
lines.append(text)
|
||
print(text)
|
||
|
||
out("Step 1 — Ispezione automatica PDF")
|
||
out(f"File: {path.name}")
|
||
out(f"Data: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||
out("=" * 50)
|
||
|
||
# ── Lettura pagine ─────────────────────────────────────────────────────
|
||
with pdfplumber.open(path) as pdf:
|
||
n_pages = len(pdf.pages)
|
||
pages_text = [page.extract_text() or "" for page in pdf.pages]
|
||
|
||
# ── Analisi per pagina ─────────────────────────────────────────────────
|
||
issues = [] # (page_num, descrizione) — page_num=0 → problema globale
|
||
deductions = 0.0
|
||
|
||
first_lines = [] # prima riga significativa di ogni pagina (per header)
|
||
last_lines = [] # ultima riga significativa di ogni pagina (per footer)
|
||
|
||
for i, text in enumerate(pages_text):
|
||
page_num = i + 1
|
||
stripped = text.strip()
|
||
|
||
# 1. Pagina vuota
|
||
if len(stripped) < 50:
|
||
issues.append((page_num, "pagina vuota"))
|
||
deductions += EMPTY_PENALTY
|
||
continue
|
||
|
||
page_lines = text.splitlines()
|
||
nonempty = [l.strip() for l in page_lines if l.strip()]
|
||
|
||
# Raccogli prima/ultima riga per il controllo header/footer
|
||
if nonempty:
|
||
first_lines.append(nonempty[0])
|
||
last_lines.append(nonempty[-1])
|
||
|
||
# 2. Sillabazione a fine riga (es. "estra-" + a capo)
|
||
syllabif = sum(
|
||
1 for line in page_lines
|
||
if re.search(r'\b\w{2,}-$', line.rstrip())
|
||
)
|
||
if syllabif:
|
||
label = "occorrenza" if syllabif == 1 else "occorrenze"
|
||
issues.append((page_num, f"sillabazione rilevata ({syllabif} {label})"))
|
||
deductions += syllabif * SYLLABIF_PENALTY
|
||
|
||
# 3. Layout a colonne (righe molto corte e numerose)
|
||
if len(nonempty) >= 10:
|
||
median_len = statistics.median(len(l) for l in nonempty)
|
||
short_ratio = sum(1 for l in nonempty if len(l) < median_len * 0.4) / len(nonempty)
|
||
if short_ratio > 0.35:
|
||
issues.append((page_num, f"possibile layout a colonne ({short_ratio:.0%} righe corte)"))
|
||
deductions += COLUMN_PENALTY
|
||
|
||
# 4. Caratteri Unicode anomali
|
||
# (control chars esclusi \n \t \r, replacement char, PUA block)
|
||
anomalies = re.findall(
|
||
r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\ufffd\ue000-\uf8ff]', text
|
||
)
|
||
if anomalies:
|
||
issues.append((page_num, f"caratteri Unicode anomali ({len(anomalies)} trovati)"))
|
||
deductions += UNICODE_PENALTY
|
||
|
||
# ── Intestazioni e piè di pagina ripetitivi ────────────────────────────
|
||
def _check_repetition(line_list: list, label: str) -> None:
|
||
nonlocal deductions
|
||
if not line_list:
|
||
return
|
||
threshold = max(3, len(line_list) * 0.25)
|
||
repeated = [
|
||
(txt, cnt) for txt, cnt in Counter(line_list).items()
|
||
if cnt >= threshold and len(txt) > 3
|
||
]
|
||
if repeated:
|
||
deductions += HEADER_FOOTER_PEN
|
||
for txt, cnt in repeated[:3]:
|
||
issues.append((0, f"{label} ripetitivo: \"{txt[:45]}\" ({cnt} volte)"))
|
||
|
||
_check_repetition(first_lines, "intestazione")
|
||
_check_repetition(last_lines, "piè di pagina")
|
||
|
||
# ── Score ──────────────────────────────────────────────────────────────
|
||
score = max(0, round(100 - deductions))
|
||
|
||
# ── Riepilogo ──────────────────────────────────────────────────────────
|
||
pages_with_issues = len({p for p, _ in issues if p > 0})
|
||
out()
|
||
out(f"Score: {score}/100")
|
||
out(f"Pagine totali: {n_pages}")
|
||
out(f"Pagine con problemi: {pages_with_issues}")
|
||
out()
|
||
|
||
if issues:
|
||
global_issues = [(p, d) for p, d in issues if p == 0]
|
||
page_issues = sorted([(p, d) for p, d in issues if p > 0])
|
||
for _, desc in global_issues:
|
||
out(f" ⚠️ {desc}")
|
||
for page_num, desc in page_issues:
|
||
out(f" Pagina {page_num:>4}: {desc}")
|
||
else:
|
||
out(" Nessun problema rilevato.")
|
||
|
||
out()
|
||
|
||
# ── Prossimi passi ─────────────────────────────────────────────────────
|
||
out("PROSSIMI PASSI:")
|
||
if score >= 70:
|
||
out(" → conversione con marker funzionerà bene")
|
||
elif score >= 40:
|
||
out(" → conversione possibile, attendi più errori nella revisione")
|
||
else:
|
||
out(" → qualità bassa — valuta una fonte PDF migliore")
|
||
|
||
attention_pages = sorted({p for p, _ in issues if p > 0})
|
||
if attention_pages:
|
||
sample = ", ".join(str(p) for p in attention_pages[:10])
|
||
if len(attention_pages) > 10:
|
||
sample += f" … e altre {len(attention_pages) - 10}"
|
||
out(f" → attenzione alle pagine {sample} nella revisione manuale")
|
||
out()
|
||
|
||
_maybe_save(lines, path, save)
|
||
|
||
|
||
def _maybe_save(lines: list, pdf_path: Path, save: bool) -> None:
|
||
if not save:
|
||
return
|
||
script_dir = Path(__file__).parent
|
||
out_file = script_dir / f"{pdf_path.stem}_step1_report.txt"
|
||
out_file.write_text("\n".join(lines), encoding="utf-8")
|
||
print(f"Report salvato in: {out_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
project_root = Path(__file__).parent.parent
|
||
sources_dir = project_root / "sources"
|
||
|
||
if not sources_dir.exists():
|
||
print(f"Errore: cartella sources/ non trovata in {project_root}")
|
||
sys.exit(1)
|
||
|
||
pdfs = sorted(sources_dir.glob("*.pdf"))
|
||
if not pdfs:
|
||
print(f"Errore: nessun PDF trovato in {sources_dir}")
|
||
sys.exit(1)
|
||
|
||
for pdf in pdfs:
|
||
inspect_pdf(str(pdf), save=True)
|
||
if len(pdfs) > 1:
|
||
print("-" * 50)
|