Vai al contenuto

Utente:Wiccio/Tool:RipWikiquote

Da Wikiquote, aforismi e citazioni in libertà.

Allineamento date biografiche da Wikidata

[modifica]

Questo strumento consiste in uno script Python progettato per allineare le date di nascita e di morte presenti nelle intestazioni delle voci di it.wikiquote.org con i dati contenuti in Wikidata.

Lo script è destinato alle voci relative a esseri umani e interviene esclusivamente sulla prima intestazione della pagina, senza modificare il resto del contenuto.

Obiettivo

[modifica]

Lo scopo principale dello strumento è:

  • migliorare la coerenza tra Wikiquote e Wikidata;
  • ridurre discrepanze nelle informazioni biografiche;
  • semplificare il lavoro di manutenzione delle voci;
  • evitare aggiornamenti manuali ripetitivi.

Lo script adotta un approccio conservativo: modifica una voce solo quando i dati presenti risultano effettivamente diversi da quelli di Wikidata.

Ambito di intervento

[modifica]

Lo script agisce solo sull’intestazione iniziale, tipicamente nella forma:

Nome Cognome (anno di nascita – anno di morte), descrizione…

Sono gestite anche intestazioni più complesse, ad esempio:

  • pseudonimi o alter ego;
  • nomi estesi o titoli;
  • qualificazioni linguistiche o storiche.

Non vengono mai modificati:

  • citazioni;
  • testo discorsivo;
  • immagini e didascalie;
  • template;
  • note e riferimenti;
  • collegamenti wiki.

Fonte dei dati

[modifica]

Le informazioni vengono lette da Wikidata, utilizzando le proprietà:

  • P569 – data di nascita
  • P570 – data di morte

La selezione dei dati segue queste priorità:

  • affermazioni con rank “preferred”;
  • in assenza di preferred, affermazione con il maggior numero di riferimenti;
  • in ulteriore assenza, la prima affermazione disponibile.

Gestione delle date

[modifica]

Lo script è in grado di interpretare e riportare correttamente:

  • anni precisi;
  • secoli (es. “VII secolo”, “VI secolo a.C.”);
  • date approssimative (“circa”);
  • decenni (es. “1100 circa”);
  • date avanti Cristo;
  • casi in cui è nota solo la data di morte;
  • persone viventi (con la dicitura “vivente”).

Il formato finale è adattato allo stile comunemente utilizzato su it.wikiquote.org.

Criteri di modifica

[modifica]

Una voce viene modificata solo se almeno una delle seguenti condizioni è vera:

  • la data di nascita su Wikiquote è diversa da quella su Wikidata;
  • la data di morte su Wikiquote è diversa da quella su Wikidata;
  • su Wikiquote è presente una data che non risulta supportata da Wikidata.

Se le informazioni coincidono, la voce viene lasciata invariata.

Sicurezza e protezioni

[modifica]

Per evitare modifiche indesiderate, lo script:

  • esclude automaticamente contenuti tra [[ ]], {{ }}, <ref> e tag HTML;
  • limita la modifica alla prima occorrenza dell’intestazione;
  • interrompe l’elaborazione se il formato della voce non è riconosciuto.

Utilizzo e raccomandazioni

[modifica]

Questo strumento è pensato come supporto alla manutenzione, non come sostituto del controllo umano.

Si raccomanda di:

  • utilizzarlo su un numero limitato di voci per sessione;
  • verificare manualmente i casi storici o ambigui;
  • usare sempre un riassunto di modifica chiaro;
  • rispettare le linee guida sull’uso di strumenti automatici.

Limitazioni note

[modifica]
  • Non gestisce voci non riconducibili a esseri umani.
  • Non interviene su più intestazioni nella stessa pagina.
  • Dipende dalla qualità e dalla precisione dei dati presenti in Wikidata.

Licenza e riutilizzo

[modifica]

Il codice sorgente dello script è liberamente utilizzabile, condivisibile e modificabile.

Chiunque può:

  • usarlo per scopi personali di manutenzione;
  • adattarlo alle proprie esigenze;
  • migliorarne o estenderne le funzionalità;
  • redistribuirlo, anche in forma modificata.

L’unica raccomandazione è quella di rispettare le linee guida dei progetti Wikimedia e di utilizzare lo strumento in modo responsabile, specialmente in caso di modifiche automatiche su larga scala.

Codice sorgente

[modifica]
# ===============================================================
# SCRIPT DI SINCRONIZZAZIONE DATE NASCITA/MORTE
# da Wikidata a it.wikiquote.org
#
# Obiettivo:
# - leggere date di nascita (P569) e morte (P570) da Wikidata
# - confrontarle con l'intestazione della voce su Wikiquote
# - aggiornare l'intestazione solo se necessario
#
# Principi adottati:
# - Wikidata ha priorità su Wikiquote
# - si preserva sempre la struttura testuale originale
# - nessuna modifica a wikilink, template, ref o HTML
# ===============================================================

import pywikibot
import re
from SPARQLWrapper import SPARQLWrapper, JSON

# QID iniziale
START_QID = "Q1"

# ---------------------------------------------------------------
# DEFINIZIONE DEI TRATTINI
#
# In Wikiquote potrebbero essere usati molti tipi di trattino
# (–, -, —, −, ecc.). Li normalizzo in un'unica regex
# per separare nascita e morte in modo canonico.
# ---------------------------------------------------------------

TRATTINI = r"–\-˗‒—―−ꟷ"
REGEX_TRATTINI = f"[{TRATTINI}]"

# ---------------------------------------------------------------
# PATTERN DELL'INTESTAZIONE
#
# Cattura:
# 1) tutto ciò che precede la parentesi con le date (incipit)
# 2) il contenuto della parentesi che contiene cifre o "secolo"
#
# Questo permette di gestire:
# - nomi semplici
# - pseudonimi / alter ego
# - titoli onorifici
# - testo descrittivo prima delle date
# ---------------------------------------------------------------

pattern = (
    r"(.+?)"
    r"\s*\("
    r"([^()]*?(?:\d|secolo|\?)[^()]*)"
    r"\)"
)

# ---------------------------------------------------------------
# ESTRAZIONE E FORMATTAZIONE DELLE DATE DA WIKIDATA
#
# La funzione converte il TimeValue Wikidata in una stringa
# compatibile con lo stile di it.wikiquote.org
#
# Precisioni gestite:
# - 7  → secolo (es. "VII secolo", "VII secolo a.C.")
# - 8  → decennio → reso come "anno circa" (es. "1100 circa")
# - ≥9 → anno preciso
#
# Eventuali qualificatori "circa" (P1480 = Q5727902)
# vengono sempre rispettati.
# ---------------------------------------------------------------

def estrai_anno(statement):
    if not statement:
        return None

    data = statement.getTarget()
    precision = getattr(data, "precision", None)

    try:
        year = data.year
    except Exception:
        return None

    # --- SECOLO ---
    if precision == 7:
        if year < 0:
            secolo = ((abs(year) - 1) // 100) + 1
            risultato = f"{int_to_roman(secolo)} secolo a.C."
        else:
            secolo = ((year - 1) // 100) + 1
            risultato = f"{int_to_roman(secolo)} secolo"

    # --- DECENNIO ---
    elif precision == 8:
        risultato = f"{abs(year)} a.C. circa" if year < 0 else f"{year} circa"

    # --- ANNO ---
    elif precision >= 9:
        risultato = f"{abs(year)} a.C." if year < 0 else str(year)

    else:
        return None

    # --- QUALIFICATORE "CIRCA" ---
    qualifiers = getattr(statement, "qualifiers", {})
    if "P1480" in qualifiers:
        for qual in qualifiers["P1480"]:
            if getattr(qual.getTarget(), "id", None) == "Q5727902":
                risultato += " circa"
                break

    return risultato

# ---------------------------------------------------------------
# CONVERSIONE NUMERI ROMANI
# Usata esclusivamente per i secoli
# ---------------------------------------------------------------

def int_to_roman(num):
    val = [1000,900,500,400,100,90,50,40,10,9,5,4,1]
    syms = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
    roman = ""
    i = 0
    while num > 0:
        for _ in range(num // val[i]):
            roman += syms[i]
            num -= val[i]
        i += 1
    return roman

# ---------------------------------------------------------------
# SCELTA DELLO STATEMENT PIÙ AFFIDABILE
#
# Ordine di priorità:
# 1) statement con rank "preferred"
# 2) statement con più riferimenti
# 3) primo statement disponibile
# ---------------------------------------------------------------

def prendi_statement_preferito(statements):
    if not statements:
        return None

    preferiti = [s for s in statements if s.getRank() == "preferred"]
    if preferiti:
        return preferiti[0]

    return sorted(
        statements,
        key=lambda s: len(getattr(s, "sources", [])),
        reverse=True
    )[0]

# ---------------------------------------------------------------
# PROTEZIONE DELLE SEZIONI NON MODIFICABILI
#
# Prima di operare sul testo copio e mantengo:
# - wikilink [[...]]
# - template {{...}}
# - ref <ref>...</ref>
# - tag HTML
#
# vengono sostituiti con segnaposto temporanei
# per evitare modifiche indesiderate.
# ---------------------------------------------------------------

def proteggi_blocchi(testo):
    segnaposti = []

    pattern_unico = re.compile(
        r"(\[\[.*?\]\]|\{\{.*?\}\}|<ref\b[^>]*?>.*?</ref>|<[^>]+>)",
        re.DOTALL
    )

    def salva(match):
        segnaposti.append(match.group(0))
        return f"@@PROT{len(segnaposti)-1}@@"

    testo = pattern_unico.sub(salva, testo)
    return testo, segnaposti

def ripristina_blocchi(testo, segnaposti):
    return re.sub(
        r"@@PROT(\d+)@@",
        lambda m: segnaposti[int(m.group(1))],
        testo
    )

# ---------------------------------------------------------------
# FUNZIONE PRINCIPALE
#
# - interroga Wikidata
# - itera su tutte le persone (Q5) con sitelink su it.wikiquote
# - confronta e aggiorna l'intestazione se necessario
# ---------------------------------------------------------------

def main():
    site_wd = pywikibot.Site("wikidata", "wikidata")
    repo = site_wd.data_repository()

    query = """
    SELECT ?item ?title WHERE {
      ?item wdt:P31 wd:Q5 .
      ?sitelink schema:about ?item ;
                schema:isPartOf <https://it.wikiquote.org/> ;
                schema:name ?title .
    }
    """

    sparql = SPARQLWrapper("https://query.wikidata.org/sparql")
    sparql.setQuery(query)
    sparql.setReturnFormat(JSON)
    results = sparql.query().convert()

    print(f"Trovati {len(results['results']['bindings'])} elementi da processare.\n")

    for row in results["results"]["bindings"]:
        qid = row["item"]["value"].split("/")[-1]
        title = row["title"]["value"]

        print(f"\n--- PROCESSO {qid} : {title} ---")

        item = pywikibot.ItemPage(repo, qid)
        item.get()

        site_wq = pywikibot.Site("it", "wikiquote")
        page = pywikibot.Page(site_wq, title)
        testo = page.text

        # P31 = Q5
        if "P31" not in item.claims:
            print(" → Nessuna P31, salto.")
            continue
        
        if not any(c.getTarget().id == "Q5" for c in item.claims["P31"]):
            print(" → Non è un essere umano (Q5), salto.")
            continue

        nas = prendi_statement_preferito(item.claims.get("P569", []))
        mor = prendi_statement_preferito(item.claims.get("P570", []))

        anno_nascita = estrai_anno(nas)
        anno_morte   = estrai_anno(mor)

        if not anno_nascita and not anno_morte:
            print(" → Nascita e morte non disponibili su Wikidata, salto.")
            continue

        # Proteggo sezioni non modificabili
        testo_protetto, segnaposti = proteggi_blocchi(testo)

        match = re.search(pattern, testo_protetto, re.DOTALL)
        if not match:
            print(" → Formato non riconosciuto, salto.")
            continue

        incipit = match.group(1).rstrip()
        incipit = re.sub(r"\s*\($", "", incipit)
        contenuto = match.group(2).strip()

        # Estrazione nascita/morte
        if re.search(REGEX_TRATTINI, contenuto):
            parti = re.split(rf"\s*{REGEX_TRATTINI}\s*", contenuto)
            nascita_wq = parti[0].strip()
            morte_wq   = parti[1].strip() if len(parti) > 1 else None

            if morte_wq and morte_wq.lower() in ("vivente", "", " "):
                morte_wq = None
                
        # ---------------------------------------------------------
        # NORMALIZZAZIONE DEI VALORI "IGNOTI" SU WIKIQUOTE
        #
        # Sequenze come "????" non rappresentano una data reale,
        # ma l'assenza di informazione. Vanno quindi trattate
        # come None per permettere a Wikidata di avere priorità.
        # ---------------------------------------------------------

        if nascita_wq and re.fullmatch(r"\?+", nascita_wq):
            nascita_wq = None
        if "?" in contenuto:
            modifica = True

        modifica = (
            anno_nascita != nascita_wq
            or (anno_morte and anno_morte != morte_wq)
            or (not anno_morte and morte_wq is not None)
        )

        if not modifica:
            print(" → Nessuna modifica necessaria.")
            continue

        # -------------------------------------------------------------
        # LOGICA DEFINITIVA PER NASCITA/MORTE
        # -------------------------------------------------------------

        if anno_nascita and anno_morte:
            # Caso normale: entrambe presenti su Wikidata
            nuova_parentesi = f"({anno_nascita} – {anno_morte})"

        elif anno_nascita and not anno_morte:
            # Wikidata ha solo la nascita → vivente
            nuova_parentesi = f"({anno_nascita} – vivente)"

        elif not anno_nascita and anno_morte:
            # Wikidata ha solo la morte → caso raro, ma gestito
            nuova_parentesi = f"(... – {anno_morte})"

        else:
            # Caso impossibile per Q5, ma mettiamo una sicurezza
            nuova_parentesi = ""
            
        # Ricostruzione della stringa finale preservando grassetto
        start, end = match.span()
        testo_mod = (
            testo_protetto[:start]
            + incipit
            + " "
            + nuova_parentesi
            + testo_protetto[end:]
        )

        # Ripristina sezioni protette
        testo_finale = ripristina_blocchi(testo_mod, segnaposti)
        page.text = testo_finale

        try:
            page.save(summary="Anno nascita/morte da Wikidata")
            print(" ✓ Modificato correttamente!")
        except Exception as e:
            print(f" ✗ Errore nel salvataggio: {e}")
            continue

if __name__ == "__main__":
    main()