step-1: add inspect_pdf.py
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).
This commit is contained in:
@@ -25,3 +25,5 @@ Thumbs.db
|
||||
|
||||
# Report generati dagli script
|
||||
step-0/*_step0_report.txt
|
||||
step-1/*_step1_report.txt
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user