"""Presidio analyzers + PatternRecognizer regex per gare d'appalto italiane.""" import re as _re from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer from presidio_analyzer.nlp_engine import NlpEngineProvider from utils import ensure_spacy_model # --------------------------------------------------------------------------- # Spacy model (solo per tokenizzazione; NER è sul transformer) # --------------------------------------------------------------------------- ensure_spacy_model("it_core_news_lg") _nlp_config = { "nlp_engine_name": "transformers", "models": [{ "lang_code": "it", "model_name": { "spacy": "it_core_news_lg", "transformers": "DeepMount00/Italian_NER_XXL_v2", }, }], "ner_model_configuration": { "model_to_presidio_entity_mapping": { # Anagrafica "NOME": "PERSON", "COGNOME": "PERSON", "DATA_NASCITA": "DATE_TIME", "DATA_MORTE": "DATE_TIME", "ETA": "ETA", "PROFESSIONE": "PROFESSIONE", "STATO_CIVILE": "STATO_CIVILE", "CODICE_FISCALE": "CODICE_FISCALE", # Contatti "INDIRIZZO": "INDIRIZZO", "NUMERO_TELEFONO": "PHONE_NUMBER", "EMAIL": "EMAIL_ADDRESS", "CODICE_POSTALE": "CAP", # Finanziari "VALUTA": "VALUTA", "IMPORTO": "IMPORTO_GARA", "NUMERO_CARTA": "CREDIT_CARD", "CVV": "CVV", "NUMERO_CONTO": "NUMERO_CONTO", "IBAN": "IBAN_CODE", "BIC": "BIC_SWIFT", "P_IVA": "PARTITA_IVA", "BANCA": "BANCA", "TASSO_MUTUO": "TASSO_MUTUO", "NUM_ASSEGNO_BANCARIO": "NUM_ASSEGNO", # Legali "RAGIONE_SOCIALE": "ORGANIZATION", "TRIBUNALE": "TRIBUNALE", "LEGGE": "LEGGE", "N_SENTENZA": "N_SENTENZA", "N_LICENZA": "N_LICENZA", "AVV_NOTAIO": "AVV_NOTAIO", "REGIME_PATRIMONIALE": "REGIME_PATRIMONIALE", # Medico "CARTELLA_CLINICA": "CARTELLA_CLINICA", "MALATTIA": "MALATTIA", "MEDICINA": "MEDICINA", "STORIA_CLINICA": "STORIA_CLINICA", "STRENGTH": "STRENGTH", "FREQUENZA": "FREQUENZA", "DURATION": "DURATION", "DOSAGGIO": "DOSAGGIO", "FORM": "FORM", # Tecnico "IP": "IP_ADDRESS", "IPV6_1": "IP_ADDRESS", "MAC": "MAC", "USER_AGENT": "USER_AGENT", "IMEI": "IMEI", # Geo / temporale "STATO": "LOCATION", "LUOGO": "LOCATION", "DATA": "DATE_TIME", "ORARIO": "DATE_TIME", # Documenti / veicoli / catasto "NUMERO_DOCUMENTO": "NUMERO_DOCUMENTO", "TARGA_VEICOLO": "TARGA_VEICOLO", "FOGLIO": "FOGLIO_CATASTO", "PARTICELLA": "PARTICELLA_CATASTO", "MAPPALE": "MAPPALE_CATASTO", "SUBALTERNO": "SUBALTERNO_CATASTO", # Web / sicurezza "URL": "URL", "PASSWORD": "PASSWORD", "PIN": "PIN", "BRAND": "ORGANIZATION", }, "low_confidence_score_multiplier": 0.4, "low_score_entity_names": [], }, } _nlp_engine = NlpEngineProvider(nlp_configuration=_nlp_config).create_engine() analyzer_ner_only = AnalyzerEngine(nlp_engine=_nlp_engine, supported_languages=["it"]) analyzer_full = AnalyzerEngine(nlp_engine=_nlp_engine, supported_languages=["it"]) # --------------------------------------------------------------------------- # Helper # --------------------------------------------------------------------------- def _r(entity: str, name: str, regex: str, score: float, context: list[str]) -> PatternRecognizer: return PatternRecognizer( supported_entity=entity, supported_language="it", patterns=[Pattern(name, regex, score)], context=context, ) # --------------------------------------------------------------------------- # PatternRecognizer – gare d'appalto # --------------------------------------------------------------------------- _RECOGNIZERS: list[PatternRecognizer] = [ # CIG – Ordinario (\d{7}[0-9A-F]{3}) · Unico/Smart ([A-Z][0-9A-F]{9}) _r("CIG", "cig", r"\b(?:\d{7}[0-9A-F]{3}|[A-Z][0-9A-F]{9})\b", 0.70, ["CIG", "cig", "Cig", "CIG:", "cig:", "CIG :", "cig :", "codice CIG", "codice cig", "Codice CIG", "codice identificativo gara", "Codice Identificativo Gara", "codice identificativo della gara", "identificativo di gara", "id gara", "ID gara", "ID Gara", "smartcig", "SmartCIG", "smart CIG", "Smart CIG", "lotto CIG", "lotto cig"]), # CUP – struttura CIPE ufficiale (15 char) _r("CUP", "cup", r"\b[A-Z]\d{2}[A-Z][A-Z0-9]{2}\d{6}[A-Z0-9]{3}\b", 0.90, ["CUP", "cup", "Cup", "CUP:", "cup:", "CUP :", "cup :", "codice CUP", "Codice CUP", "codice unico di progetto", "Codice Unico di Progetto", "codice unico progetto", "codice progetto", "monitoraggio investimenti pubblici", "investimento pubblico", "progetto pubblico"]), # REA – sigla provincia + separatore + 4-7 cifre _r("REA", "rea", r"\b[A-Z]{2}[\s\-/\.]\d{4,7}\b", 0.85, ["REA", "rea", "Rea", "REA:", "rea:", "REA :", "REA n.", "n. REA", "n. rea", "n.REA", "registro imprese", "Registro Imprese", "registro delle imprese", "registro economico amministrativo", "CCIAA", "cciaa", "camera di commercio", "Camera di Commercio"]), # Numero Gara ANAC – score basso (richiede context boost) _r("NUMERO_GARA_ANAC", "num_gara", r"\b\d{7,8}\b", 0.60, ["ANAC","ANAC:","ANAC :","anac:" "anac", "numero gara", "Numero Gara", "n. gara", "N. gara", "N.Gara", "gara ANAC", "Gara ANAC:" "gara n.", "gara n°", "SIMOG", "simog", "identificativo ANAC", "codice ANAC"]), # PEC – domini certificati italiani _r("PEC", "pec", r"\b[\w.\-]+@[\w.\-]*(?:pec|postacert|legalmail|pecmail|" r"sicurezzapostale|registerpec|ticertifica)[\w.\-]*\.\w+\b", 0.90, ["PEC", "pec", "Pec", "PEC:", "pec:", "indirizzo PEC", "posta certificata", "Posta Certificata", "posta elettronica certificata", "p.e.c.", "P.E.C."]), # Matricola INPS – score basso (richiede context boost) _r("MATRICOLA_INPS", "inps", r"(? PARTITA_IVA (0.70) → vince in presenza di contesto C.F. _r("CODICE_FISCALE", "cf_azienda", r"\b\d{11}\b", 0.75, ["C.F.", "c.f.", "CF", "cf", "C.F.:", "c.f.:", "CF:", "codice fiscale", "Codice Fiscale", "codice fiscale azienda", "codice fiscale impresa", "codice fiscale società"]), # Forme societarie – nome (1-4 parole capitalizzate) + sigla giuridica _r("SOCIETA", "societa", r"\b(?:[A-Z][\w&\-']*\s+){1,4}" r"(?:" r"S\.?\s*r\.?\s*l\.?(?:\s*[Ss]\.?)?|" # S.r.l., Srl, S.r.l.s. r"S\.?\s*p\.?\s*[Aa]\.?|" # S.p.A., SpA r"S\.?\s*a\.?\s*p\.?\s*[Aa]\.?|" # S.a.p.A. r"S\.?\s*n\.?\s*c\.?|" # S.n.c., Snc r"S\.?\s*a\.?\s*s\.?|" # S.a.s., Sas r"S\.?\s*s\.?|" # S.s. r"S\.?\s*c\.?\s*a\.?\s*r\.?\s*l\.?|" # S.c.a.r.l. r"Soc(?:\.|ietà)?\s+[Cc]oop(?:\.|erativa)?(?:\s+a\s+r\.?\s*l\.?)?|" r"Cooperativa(?:\s+Sociale)?|" r"Onlus|ONLUS|" r"E\.?\s*T\.?\s*S\.?|" # ETS r"A\.?\s*P\.?\s*S\.?|" # APS r"O\.?\s*N\.?\s*G\.?|" # ONG r"Lda\.?|Ltd\.?|GmbH|S\.?\s*A\.?|Inc\.?" # estere comuni r")\b\.?", 0.85, ["società", "Società", "SOCIETÀ", "impresa", "Impresa", "ditta", "Ditta", "azienda", "Azienda", "ragione sociale", "Ragione Sociale", "denominazione", "Denominazione", "operatore economico", "Operatore Economico", "subappaltatore", "appaltatore", "aggiudicatario", "banca", "Banca", "istituto", "compagnia", "Compagnia", "fornitore", "Fornitore", "concessionario"]), # Iscrizione Albo professionale _r("ISCRIZIONE_ALBO", "albo", r"(?:[Aa]lbo\s+(?:degli?\s+)?\w+(?:\s+(?:di|della|del)\s+\w+)?" r"(?:\s+(?:di|della|del)\s+\w+)?)\s*(?:al|n\.?)?\s*\d+(?:/[A-Z])?", 0.80, ["albo", "Albo", "iscrizione", "Iscrizione", "iscritto", "iscritta", "albo professionale", "Albo Professionale", "ordine professionale"]), ] for _rec in _RECOGNIZERS: analyzer_full.registry.add_recognizer(_rec) # --------------------------------------------------------------------------- # POST_BOOST_PATTERNS – secondo passaggio regex (score +0.30 se fullmatch) # --------------------------------------------------------------------------- POST_BOOST_PATTERNS: dict[str, _re.Pattern] = { "NUMERIC": _re.compile(r"^\d{4,7}$"), "CIG": _re.compile(r"^(?:\d{7}[0-9A-F]{3}|[A-Z][0-9A-F]{9})$"), "CUP": _re.compile(r"^[A-Z]\d{2}[A-Z][A-Z0-9]{2}\d{6}[A-Z0-9]{3}$"), "REA": _re.compile(r"^[A-Z]{2}[\s\-/\.]\d{4,7}$"), "CPV": _re.compile(r"^\d{8}-\d$"), "NUTS": _re.compile(r"^IT[A-Z0-9]{1,4}$"), "ATECO": _re.compile(r"^\d{2}\.\d{2}(?:\.\d{1,2})?$"), "CODICE_ANAC": _re.compile(r"^IT-\d{11}$"), "SIOGG": _re.compile(r"^SOGG-?[A-Z0-9]{8,12}$"), "LOTTO_GARA": _re.compile(r"^(?:lotto|lot\.?)\s*(?:n\.?)?\s*\d{1,3}[a-z]?$", _re.IGNORECASE), "POLIZZA_ASSICURATIVA": _re.compile(r"^[A-Z]{2,5}-\d{4}-[A-Z]{2}-\d{6,12}$"), "PEC": _re.compile( r"^[\w.\-]+@[\w.\-]*(?:pec|postacert|legalmail|pecmail|" r"sicurezzapostale|registerpec|ticertifica)[\w.\-]*\.\w+$", _re.IGNORECASE), "CODICE_FISCALE": _re.compile(r"^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$", _re.IGNORECASE), "PARTITA_IVA": _re.compile(r"^(IT\s*)?\d{11}$", _re.IGNORECASE), "IBAN_CODE": _re.compile(r"^IT\d{2}[A-Z]\d{10}[A-Z0-9]{12}$", _re.IGNORECASE), "BIC_SWIFT": _re.compile( r"^[A-Z]{4}(?:IT|DE|FR|ES|GB|CH|NL|BE|AT|PT|GR|LU|IE|FI|SE|DK|NO|PL)" r"[A-Z0-9]{2}(?:[A-Z0-9]{3})?$"), "CREDIT_CARD": _re.compile( r"^(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))" r"[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{3,4}$"), "IP_ADDRESS": _re.compile( r"^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$"), "MAC": _re.compile(r"^(?:[0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}$"), "TARGA_VEICOLO": _re.compile( r"^(?:[A-Z]{2}\d{3}[A-Z]{2}|[A-Z]{2}\d{4}[A-Z]|" r"(?:CC|EI|EE|RM|VF|CRI|SCC|CD)\s*\d{3,5})$", _re.IGNORECASE), "NUMERO_PROTOCOLLO": _re.compile( r"^prot(?:ocollo)?\.?\s*(?:n\.?)?\s*\d{4,8}(?:[/\-]\d{2,4})?$", _re.IGNORECASE), }