Aller au contenu
Documentation technique
Sur cette page

Référence Détecteurs

Module : piighost.components.detector

Un détecteur est l'étage de détection d'un pipeline. Il lit un texte et renvoie les données confidentielles qu'il y trouve. Tout détecteur satisfait le port AnyDetector et renvoie une liste de Detection, quel que soit le backend qu'il enveloppe.

from piighost.components.detector import (
    ChunkedDetector,
    CompositeDetector,
    ExactMatchDetector,
    LLMDetector,
    RegexDetector,
)
from piighost.components.detector.ner import (
    BridgeDetector,
    Gliner2Detector,
    Gliner2PiiDetector,
    PresidioDetector,
    SpacyDetector,
    TransformersDetector,
)

Chaque détecteur NER a besoin de son propre extra (gliner2, spacy, transformers, presidio). LLMDetector a besoin de l'extra llm et d'un paquet fournisseur.


AnyDetector (protocole)

C'est le port que tout détecteur implémente. Sa seule méthode est asynchrone, donc une implémentation peut attendre une I/O comme un serveur de modèle ou une API LLM sans bloquer le pipeline.

@runtime_checkable
class AnyDetector(Protocol):
    async def detect(self, text: str) -> list[Detection]: ...

detect renvoie les détections dans un ordre quelconque. Les chevauchements et les doublons sont résolus par les étages suivants du pipeline, pas par le détecteur.

Detection

Chaque détecteur renvoie une liste de Detection, un dataclass gelé qui porte l'emplacement de la correspondance, le texte trouvé, son label et sa confiance.

AttributTypeDescription
spanSpanL'emplacement de la détection, en intervalle semi-ouvert
textstrLa sous-chaîne trouvée
labelstrLa catégorie de la valeur détectée, par exemple PERSON ou EMAIL
confidencefloatLa confiance du détecteur, dans l'intervalle fermé 0 à 1

RegexDetector

Trouve les données confidentielles en appliquant un pattern regex par label. Chaque pattern est compilé une fois à la construction, sous re.ASCII, donc \d et les autres classes de forme ne correspondent qu'à l'ASCII. Un caractère Unicode ressemblant à un chiffre, comme un chiffre arabo-indien, ne correspond pas, car les formats que le détecteur cible utilisent des chiffres ASCII. detect émet une détection par correspondance sans chevauchement, à une confiance fixe de 1.0. Chaque espace Unicode du texte est lue comme une espace ordinaire, voir Espaces Unicode.

Il ne porte aucun validateur de somme de contrôle. Il reconnaît donc une valeur sur sa forme seule. Une valeur structurée abîmée par un OCR est conservée plutôt que rejetée, car rejeter une vraie valeur reviendrait à la laisser fuiter.

Constructeur

RegexDetector(patterns: dict[str, str])
ParamètreTypeDescription
patternsdict[str, str]Correspondance d'un label vers le pattern regex à appliquer (requis)
from piighost.components.detector import RegexDetector

detector = RegexDetector({"EMAIL": r"[\w.+-]+@[\w.-]+\.\w{2,}"})
detections = await detector.detect("write to alice@example.com")
# [Detection(span=Span(9, 26), text="alice@example.com", label="EMAIL", confidence=1.0)]

from_catalog

RegexDetector.from_catalog(ref: str, *, catalog: str | None = None) -> RegexDetector

Construit un détecteur à partir des regex que porte une référence du catalogue piighost. Le catalogue est un registre de regex de dé-identification testées, adressées par namespace/name et un sélecteur optionnel, soit un tag, soit les huit caractères hexadécimaux d'un commit.

ParamètreTypeDescription
refstrUne référence, namespace/name avec un :selector optionnel et un préfixe catalog: optionnel. Sans sélecteur, elle résout vers latest (requis)
catalogstr | NoneOrigine du catalogue à interroger. Par défaut PIIGHOST_CATALOG_URL, puis le catalogue public
from piighost.components.detector import RegexDetector

detector = RegexDetector.from_catalog("piighost/logs:fd79aec6")
detections = await detector.detect("mail me at a@b.co from 10.0.0.1")

Une référence épinglée sur un commit est immuable. Sa réponse est donc mise en cache sous ~/.cache/piighost/catalog et relue depuis le disque aux appels suivants. Une référence qui pointe vers un tag ou vers latest peut changer. Elle est donc récupérée à chaque fois, parce qu'une version périmée détecterait sans le dire moins que ce que l'appelant a demandé.

L'appel lève une sous-classe de CatalogError (piighost.catalog) si la référence ne se parse pas, si le catalogue est injoignable, ou si la référence résout vers autre chose qu'un détecteur regex simple. Ce dernier cas couvre une référence qui porte un détecteur modèle. Ses regex seules détecteraient moins que ce que la référence promet, donc l'appel échoue plutôt que d'en rendre la moitié.

from_catalog n'utilise que la bibliothèque standard, donc l'installation de base n'a besoin d'aucun extra.

Le code écrit pour la 1.x tourne toujours. RegexDetector.from_hub(ref, hub=...) construit le même détecteur que from_catalog, un préfixe hub: se lit comme catalog:, PIIGHOST_HUB_URL est lue quand PIIGHOST_CATALOG_URL n'est pas posée, et piighost.hub réexporte piighost.catalog sous ses noms de la 1.x.


CompositeDetector

Fait tourner plusieurs détecteurs sur le même texte et fusionne leurs détections. Il est lui-même un AnyDetector, donc il se compose avec le pipeline sans changement. Il exécute chaque enfant en parallèle et concatène leurs résultats dans l'ordre des enfants. Il ne déduplique pas. Chevauchements et doublons passent à l'étage de résolution de spans.

Constructeur

CompositeDetector(detectors: list[AnyDetector])
ParamètreTypeDescription
detectorslist[AnyDetector]Les détecteurs enfants à exécuter, dans l'ordre (requis)
from piighost.components.detector import CompositeDetector, RegexDetector
from piighost.components.detector.ner import Gliner2Detector

email_detector = RegexDetector({"EMAIL": r"[\w.+-]+@[\w.-]+\.\w{2,}"})
person_detector = Gliner2Detector(model="fastino/gliner2-multi-v1", labels=["PERSON"])
detector = CompositeDetector([email_detector, person_detector])

ExactMatchDetector

Trouve les occurrences en mot entier de valeurs littérales configurées. Il parcourt le texte pour chaque valeur et émet une détection par occurrence à une confiance de 1.0. La correspondance se fait sur des frontières de mot, donc une valeur ne se déclenche pas à l'intérieur d'un mot plus long (Ann ne correspond pas dans Anne). La correspondance est insensible à la casse par défaut. Une valeur correspond donc quelle que soit sa casse, et la détection garde le texte tel qu'il apparaît. Une espace dans une valeur correspond à n'importe quelle suite d'espaces, voir Espaces Unicode. Une valeur faite uniquement d'espaces est refusée. Il ne porte aucun modèle et aucune dépendance optionnelle. C'est donc le détecteur de choix pour exercer le pipeline dans les tests.

Constructeur

ExactMatchDetector(values: dict[str, str], case_sensitive: bool = False)
ParamètreTypeDescription
valuesdict[str, str]Correspondance d'une valeur littérale vers le label à émettre pour elle (requis)
case_sensitiveboolSi la correspondance respecte la casse. False par défaut
from piighost.components.detector import ExactMatchDetector

detector = ExactMatchDetector({"Patrick": "PERSON", "Lyon": "LOCATION"})
detections = await detector.detect("Patrick lives in Lyon")

ChunkedDetector

Fait tourner un détecteur enveloppé sur chaque morceau d'un texte long. C'est un décorateur, lui-même un AnyDetector. Il découpe le texte en morceaux qui se chevauchent, exécute le détecteur enveloppé sur chacun, et reprojette chaque détection sur le texte original. Les détections strictement identiques produites par le chevauchement sont supprimées. Conflits de label et confiances différentes passent à l'étage de résolution de spans.

Constructeur

ChunkedDetector(detector: AnyDetector, splitter: AnySplitter | None = None)
ParamètreTypeDescription
detectorAnyDetectorLe détecteur exécuté sur chaque morceau (requis)
splitterAnySplitter | NoneLe splitter, ou None pour un RecursiveCharacterTextSplitter par défaut
from piighost.components.detector import ChunkedDetector
from piighost.components.detector.ner import SpacyDetector

spacy_detector = SpacyDetector(model="en_core_web_sm")
detector = ChunkedDetector(spacy_detector)

LLMDetector

Détecte les PII avec un modèle de chat LangChain via une sortie structurée. A besoin de l'extra llm et d'un paquet fournisseur. On demande au modèle d'extraire des paires (text, label) selon un schéma. Le champ label de ce schéma n'accepte que les labels configurés. Chaque valeur extraite est ensuite localisée dans le texte source par recherche sur frontière de mot, donc une valeur inventée par le modèle mais absente du texte ne donne rien. labels est requis, puisque le schéma est construit à partir de ces labels. Le texte source est enveloppé dans des balises <text_to_analyze>. Le prompt système ordonne au modèle de traiter le contenu balisé comme des données, jamais comme des instructions. Une tentative d'injection de prompt dans le texte ne peut donc pas orienter l'extraction.

Constructeur

LLMDetector(
    model: BaseChatModel | str,
    labels: list[str] | dict[str, str],
    prompt: str | None = None,
    provider: str | None = None,
    confidence: float = 1.0,
    fail_open: bool = False,
)
ParamètreTypeDescription
modelBaseChatModel | strUn modèle de chat chargé, ou un nom chargé avec init_chat_model (requis)
labelslist[str] | dict[str, str]Les labels à extraire, liste ou map {emitted: internal} (requis)
promptstr | NoneUn prompt système personnalisé, ou None pour celui par défaut
providerstr | NoneLe fournisseur passé à init_chat_model quand model est un nom
confidencefloatConfiance portée sur chaque détection, 1.0 par défaut, pour qu'un détecteur LLM puisse être départagé face à un détecteur NER à la résolution des chevauchements
fail_openboolSi une sortie que le détecteur ne sait pas lire passe comme zéro détection, False par défaut

Un prompt personnalisé doit contenir un placeholder {labels}. Il doit aussi doubler toute autre accolade littérale en {{ ou }}, selon le format f-string de LangChain.

Une sortie que le détecteur ne sait pas lire, un JSON cassé ou un résultat sans son champ entities, lève UnreadableOutputError. Un modèle en panne refuse donc le message au lieu de l'envoyer sans détection. L'erreur nomme le type de la sortie, jamais son texte. Avec fail_open=True, le message part sans détection et un avertissement est journalisé, pour un déploiement qui fait passer la disponibilité avant la protection.

from piighost.components.detector import LLMDetector

detector = LLMDetector(
    model="gpt-5.6-terra",
    labels=["PERSON", "EMAIL"],
    provider="openai",
)

Détecteurs NER

Les détecteurs adossés à un modèle étendent BaseNERDetector, qui gère la correspondance et le filtrage des labels (voir plus bas). Chacun a besoin de son propre extra et prend un modèle chargé ou un nom de modèle à charger, sauf PresidioDetector, qui prend un AnalyzerEngine construit.

Gliner2Detector

Un modèle GLiNER2 zero-shot. A besoin de l'extra gliner2. labels est requis, car GLiNER2 est interrogé avec les labels internes. Un model en str est chargé avec GLiNER2.from_pretrained.

Gliner2Detector(
    model: GLiNER2 | str,
    labels: list[str] | dict[str, str],
    threshold: float = 0.5,
    max_concurrency: int | None = None,
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
ParamètreTypeDescription
modelGLiNER2 | strUn modèle chargé, ou un nom chargé avec from_pretrained (requis)
labelslist[str] | dict[str, str]Les labels à interroger, liste ou map {emitted: internal} (requis)
thresholdfloatLa confiance à partir de laquelle une entité est conservée
max_concurrencyint | NonePlafond d'inférences concurrentes, ou None pour sans limite
max_charsint | NoneLimite en caractères vue par une seule inférence, ou None pour aucune limite
auto_chunkboolSi un texte plus long que max_chars est découpé et reprojeté, sinon lève TextTooLongError

Gliner2PiiDetector

Un Gliner2Detector prêt à l'emploi sur le modèle GLiNER2 de fastino affiné pour les PII. Le modèle et la map de labels sont préréglés, donc ni identifiant de modèle ni argument labels n'est requis. Le préréglage couvre la taxonomie du modèle, des noms et coordonnées aux identifiants, données de paiement, identité numérique, secrets et dates sensibles. Passez labels pour restreindre ou étendre l'ensemble, ou model pour injecter une instance chargée, par exemple dans un test, afin qu'aucun poids ne soit téléchargé.

Gliner2PiiDetector(
    model: GLiNER2 | str | None = None,
    labels: list[str] | dict[str, str] | None = None,
    threshold: float = 0.5,
    max_concurrency: int | None = None,
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
ParamètreTypeDescription
modelGLiNER2 | str | NoneUn modèle chargé ou un nom, ou None pour le modèle PII préréglé
labelslist[str] | dict[str, str] | NoneLes labels à interroger, ou None pour la map de labels PII préréglée
thresholdfloatLa confiance à partir de laquelle une entité est conservée
max_concurrencyint | NonePlafond d'inférences concurrentes, ou None pour sans limite
max_charsint | NoneLimite en caractères vue par une seule inférence, ou None pour aucune limite
auto_chunkboolSi un texte plus long que max_chars est découpé et reprojeté, sinon lève TextTooLongError

SpacyDetector

Un modèle NER spaCy. A besoin de l'extra spacy. labels est optionnel. Omis, chaque entité produite par spaCy est conservée avec son label spaCy. Un model en str est chargé avec spacy.load.

SpacyDetector(
    model: Language | str,
    labels: list[str] | dict[str, str] | None = None,
    max_concurrency: int | None = None,
)
ParamètreTypeDescription
modelLanguage | strUn modèle chargé, ou un nom chargé avec spacy.load (requis)
labelslist[str] | dict[str, str] | NoneLes labels à mapper et filtrer, ou None pour garder chaque label natif
max_concurrencyint | NonePlafond d'inférences concurrentes, ou None pour sans limite

TransformersDetector

Un pipeline de classification de tokens Hugging Face. A besoin de l'extra transformers. labels est optionnel. Omis, chaque label natif est gardé. Un pipeline en str est chargé comme un pipeline ner. Une entité qui score sous threshold est rejetée.

TransformersDetector(
    pipeline: TokenClassificationPipeline | str,
    labels: list[str] | dict[str, str] | None = None,
    threshold: float = 0.0,
    max_concurrency: int | None = None,
    aggregation_strategy: str = "simple",
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
ParamètreTypeDescription
pipelineTokenClassificationPipeline | strUn pipeline construit, ou un nom de modèle chargé comme pipeline ner (requis)
labelslist[str] | dict[str, str] | NoneLes labels à mapper et filtrer, ou None pour garder chaque label natif
thresholdfloatLe score sous lequel une entité détectée est rejetée
max_concurrencyint | NonePlafond d'inférences concurrentes, ou None pour sans limite
aggregation_strategystrComment les sous-tokens sont regroupés en entités entières, appliqué seulement à la construction depuis un nom de modèle. Un pipeline injecté garde la sienne. "simple" par défaut
max_charsint | NoneLimite en caractères vue par une seule inférence, ou None pour aucune limite
auto_chunkboolSi un texte plus long que max_chars est découpé et reprojeté, sinon lève TextTooLongError

PresidioDetector

Enveloppe un AnalyzerEngine de Presidio pour réutiliser ses recognizers. A besoin de l'extra presidio. L'analyzer est injecté, car un moteur est assemblé d'un moteur NLP et d'un registre de recognizers, pas chargé depuis un nom. labels est optionnel. Omis, chaque type natif est gardé. Une entité scorant sous threshold est écartée.

PresidioDetector(
    analyzer: AnalyzerEngine,
    labels: list[str] | dict[str, str] | None = None,
    language: str = "en",
    threshold: float = 0.0,
    max_concurrency: int | None = None,
)
ParamètreTypeDescription
analyzerAnalyzerEngineUn analyzer Presidio construit (requis)
labelslist[str] | dict[str, str] | NoneLes labels à mapper et filtrer, ou None pour garder chaque type natif
languagestrLe code de langue passé à analyze
thresholdfloatLe score sous lequel une entité est écartée
max_concurrencyint | NonePlafond d'inférences concurrentes, ou None pour sans limite

Depuis une config, le type de détecteur presidio construit l'AnalyzerEngine anglais par défaut de Presidio. Pour une autre langue ou des recognizers custom, construisez le moteur vous-même et utilisez PresidioDetector directement.

BridgeDetector

Délègue l'inférence à un exécuteur injecté et convertit sa réponse en détections. Il ne porte aucun modèle et ne demande aucun extra. Il existe pour un environnement où aucune pile NER n'est installable. Le cas courant est le navigateur. Le modèle y tourne dans l'environnement d'exécution JavaScript de l'hôte, et Python l'attend via le FFI de Pyodide. La même forme sert n'importe quel exécuteur hors du processus, un sous-processus ou un side-car.

labels est obligatoire, puisque l'exécuteur est interrogé avec les labels internes et qu'un span dont le label n'est pas mappé est écarté, comme pour tout adaptateur NER. offset_unit est obligatoire aussi, puisque rien dans une réponse ne dit si ses décalages comptent des points de code ou des unités UTF-16.

BridgeDetector(
    runner: AnySpanRunner,
    labels: list[str] | dict[str, str],
    *,
    offset_unit: OffsetUnit,
    threshold: float = 0.5,
    max_chars: int | None = None,
    auto_chunk: bool = True,
)
ParamètreTypeDescription
runnerAnySpanRunnerL'appelable attendu pour chaque texte, qui porte le modèle (obligatoire)
labelslist[str] | dict[str, str]Les labels à mapper et filtrer (obligatoire)
offset_unitOffsetUnitCe que l'exécuteur compte dans ses décalages, CODE_POINT ou UTF16 (obligatoire)
thresholdfloatLa confiance à partir de laquelle un span est retenu, transmise à l'exécuteur puis appliquée à nouveau sur sa réponse
max_charsint | NoneBorne au-delà de laquelle le texte est découpé, ou None pour aucune borne
auto_chunkboolSi un texte au-delà de max_chars est découpé plutôt que refusé

L'exécuteur est un appelable asynchrone qui prend le texte, les labels internes et le seuil, et rend une séquence de mappings portant start, end, label et score. Les décalages sont des positions dans le texte transmis, en intervalle semi-ouvert, comptées dans l'unité que nomme offset_unit.

OffsetUnitCompteExécuteur
CODE_POINTdes caractères, comme une str Python et Spanécrit en Python
UTF16des unités UTF-16, où un emoji ou un idéogramme rare en prend deuxécrit en JavaScript, dans un navigateur ou dans Node

Sur un texte sans emoji ni idéogramme rare, les deux unités concordent. Après chacun de ces caractères, elles s'écartent d'une unité. Un décalage JavaScript lu comme un point de code tombe un caractère trop loin, et la première lettre de la valeur reste en clair. Un exécuteur JavaScript déclare donc UTF16, et le détecteur convertit.

from piighost.components.detector.ner import BridgeDetector, OffsetUnit


async def runner(text: str, labels: list[str], threshold: float):
    return [{"start": 0, "end": 10, "label": "person", "score": 0.92}]


detector = BridgeDetector(
    runner, {"PERSON": "person"}, offset_unit=OffsetUnit.CODE_POINT, threshold=0.4
)
await detector.detect("Emma Rossi works at Acme.")
# [Detection(span=Span(0, 10), text="Emma Rossi", label="PERSON", confidence=0.92)]

Un exécuteur est du code étranger, souvent atteint au travers d'une frontière de langage, donc sa réponse est vérifiée plutôt que crue.

  • Le text que l'exécuteur rend est ignoré et relu depuis la source, donc un exécuteur qui abîme la sous-chaîne trouvée ne peut pas désynchroniser le remplacement.
  • Un span auquel il manque un champ, ou qui porte un décalage qui n'est pas un entier, un flottant comme 8.9 ou 8.0 compris, lève BridgePayloadError. Tronquer un tel décalage déplacerait le span.
  • Un span qui déborde du texte, ou un décalage UTF-16 qui tombe entre les deux moitiés d'un caractère, lève BridgeSpanRangeError. Rogner le span découperait une sous-chaîne plus courte que ce que l'exécuteur visait, et laisserait une partie de la valeur en clair.
  • Un span noté sous threshold est écarté, même quand l'exécuteur a ignoré le seuil qu'il a reçu.
  • Un résultat portant une méthode to_py, comme le fait un JsProxy de Pyodide, est converti d'abord.

Ce détecteur n'a pas de modèle de configuration. Son exécuteur est un appelable. Un fichier TOML ou JSON ne peut pas nommer un appelable sans un registre d'appelables, et ce registre ferait dépendre le cœur de ce qui le configure. Un appelant qui construit ce détecteur le construit dans le code.

Gestion des textes longs

Gliner2Detector, TransformersDetector et BridgeDetector prennent max_chars avec auto_chunk (défaut True). Un texte plus long que max_chars est découpé en morceaux qui se chevauchent, scannés séparément, puis reprojetés sur le texte original. Avec auto_chunk désactivé, un texte au-delà de la limite lève TextTooLongError à la place. max_chars vaut None par défaut, donc il n'y a pas de limite et le texte entier est scanné en une passe. SpacyDetector et PresidioDetector n'exposent pas ces deux paramètres.

Garanties communes à tous les détecteurs NER

BaseNERDetector applique une même passe à ce que rend n'importe quel modèle, si bien que tous les adaptateurs se comportent pareil, quel que soit leur backend.

  • Le texte d'une détection est la tranche de la source que couvre son span, jamais la chaîne que le modèle a rendue. La fusion des chevauchements, le regroupement et la restauration supposent tous que le texte correspond aux caractères qu'il remplace.
  • Une détection notée sous threshold est écartée, même quand le modèle a reçu le seuil et laissé passer une détection plus faible.
  • Les labels sont mappés et filtrés, comme le décrit la section suivante.

Correspondance des labels

BaseNERDetector normalise l'argument labels en une map externe vers interne, puis mappe et filtre les détections produites par le modèle. Il distingue le label qu'un modèle utilise nativement du label émis dans Detection.label.

  • Une liste, ["PERSON", "LOCATION"], mappe chaque label vers lui-même.
  • Une map, {"PERSON": "PER"}, prend le label émis comme clé et le label natif du modèle comme valeur. Une détection que le modèle étiquette PER est donc émise en PERSON. Un label natif absent des valeurs de la map est rejeté.
  • None ou une map vide n'applique aucune correspondance, donc chaque détection est gardée avec le label donné par le modèle.

Deux labels externes mappant vers un même label interne lèvent LabelMappingError, car la recherche inverse serait ambiguë.

from piighost.components.detector.ner import TransformersDetector

detector = TransformersDetector(
    pipeline="dslim/bert-base-NER",
    labels={"PERSON": "PER", "LOCATION": "LOC"},
)

Groupes du catalogue

Ensembles de patterns regex réutilisables pour RegexDetector, publiés sous forme de groupes sur le catalogue piighost. Chaque groupe associe un label de PII à un pattern regex. Les patterns correspondent sur la forme seule, sans validation de somme de contrôle.

GroupeRéférenceLabels
Génériquecatalog:piighost/genericEMAIL, URL, IPV4, CREDIT_CARD
UScatalog:piighost/usUS_PHONE, US_ZIP, US_ITIN, US_SSN
EUcatalog:piighost/euIBAN
Francecatalog:piighost/frFR_PHONE, FR_IBAN, FR_NIR, FR_SIRET, FR_SIREN
Secretscatalog:piighost/secretsOPENAI_API_KEY, AWS_ACCESS_KEY, GITHUB_TOKEN, STRIPE_KEY

Construisez un détecteur à partir d'un groupe avec from_catalog. pull (piighost.catalog) renvoie un groupe sous forme de dict[str, str] dans l'ordre du registre. Plusieurs groupes se fusionnent donc comme des dict, et pour un même label, l'entrée de droite l'emporte.

from piighost.catalog import pull
from piighost.components.detector import RegexDetector

detector = RegexDetector.from_catalog("catalog:piighost/generic")
merged = RegexDetector(
    {**pull("catalog:piighost/generic"), **pull("catalog:piighost/fr")}
)

Une référence épinglée sur un commit se termine par les huit caractères hexadécimaux du commit, après le dernier deux-points, comme catalog:piighost/generic:fab51b33. Elle est récupérée à la première construction d'un détecteur, puis relue depuis le cache sur disque, même hors ligne. Une référence non épinglée, catalog:piighost/generic ou catalog:piighost/generic:latest, est récupérée à chaque construction.

Le catalogue teste chaque pattern qu'il publie contre le backtracking catastrophique, de sorte qu'une entrée adverse ne peut pas transformer un scan en déni de service.

Les labels du groupe générique ne dépendent d'aucun pays. Les autres sont préfixés (US_, FR_) pour ne pas se confondre quand les groupes sont fusionnés. Le groupe EU porte l'IBAN ISO 13616 partagé entre les États membres. Pour des numéros propres à un pays, utilisez un groupe par pays.

Tirer les groupes depuis une config

Une config de détecteur regex tire les groupes du catalogue via catalogs. Une entrée est une référence du catalogue écrite catalog:namespace/name avec un :selector optionnel. Une référence écrite hub:namespace/name, comme en 1.x, est toujours acceptée. Les groupes fusionnent dans l'ordre, puis les patterns en ligne s'y ajoutent. Un pattern en ligne l'emporte donc sur un pattern de groupe pour le même label. Une config de détecteur regex a besoin d'au moins un pattern en ligne ou une référence du catalogue.

[detector]
type = "regex"
catalogs = ["catalog:piighost/generic", "catalog:piighost/fr"]

[detector.patterns]
INTERNAL_ID = "EMP-\\d{6}"

Une référence du catalogue nomme un groupe relu au lieu d'en porter une copie. La config reste donc courte, et les patterns restent auditables à leur source. Un groupe est récupéré à la construction de la config, pas à sa lecture. Définissez PIIGHOST_CATALOG_URL pour interroger un registre privé.

Une entrée qui n'est pas une référence du catalogue échoue au chargement plutôt que sous forme d'URL invalide plus tard. Les noms generic, us, eu et fr, qui désignaient avant la 2.0 des ensembles de motifs livrés dans la librairie, sont refusés, et le message d'erreur donne la référence qui les remplace.

the built-in catalog 'generic' was removed in piighost 2.0: name the catalog group instead, catalog:piighost/generic

Espaces Unicode

Une valeur est souvent tapée avec une espace qui n'est pas l'espace ASCII. Word place une espace insécable (U+00A0) ou une espace fine insécable (U+202F) dans un numéro de téléphone ou un IBAN, l'extraction d'un PDF produit des espaces fines et des espaces de chiffre, et un texte d'Asie de l'Est utilise l'espace idéographique (U+3000). piighost lit chaque séparateur d'espace Unicode (catégorie Zs) comme une espace ordinaire, et chaque séparateur de ligne (U+0085, U+2028, U+2029) comme un retour à la ligne. Cette règle s'applique à trois étapes.

ÉtapeComposantsCe qui est garanti
DétectionRegexDetectorUn pattern écrit avec une espace ou avec \s reconnaît une valeur tapée avec n'importe quelle espace Unicode. Les patterns s'appliquent à une copie du texte de même longueur, donc les positions restent justes et le texte détecté garde ses espaces telles qu'elles sont écrites.
RechercheExactMatchDetector, LLMDetector, WordBoundaryExpanderUne espace dans une valeur cherchée correspond à n'importe quelle suite d'espaces, retour à la ligne compris, donc Paul Martin est retrouvé à travers une espace insécable, deux espaces ou un retour à la ligne.
IdentitéExactEntityLinker, overrides, mémoire de conversation, FuzzyEntityResolverDeux valeurs sont la même quand elles ont les mêmes mots, quelles que soient les espaces qui les séparent et leur casse, donc elles partagent un jeton.

La règle vaut pour tous les patterns, ceux du catalogue compris, donc un pattern n'a pas à prévoir ces caractères. Un pattern qui cherche exprès une espace insécable n'en trouve plus, car la copie sur laquelle il s'applique porte des espaces ordinaires à la place. Les caractères de largeur nulle (U+200B, U+2060, U+FEFF) ne sont pas des espaces et restent tels quels.

Les deux fonctions de cette règle, normalize_spaces et value_key, sont publiques dans piighost.text, pour un détecteur ou un linker personnalisé qui doit suivre la même règle.

from piighost.text import normalize_spaces, value_key

normalize_spaces("06\u00a012\u202f34")  # "06 12 34", même longueur
value_key("Paul\u00a0Martin") == value_key("paul  MARTIN")  # True, la même valeur

Recherche par mot entier

ExactMatchDetector, LLMDetector et WordBoundaryExpander ne trouvent une valeur que là où elle forme un mot entier. Le caractère qui la précède et celui qui la suit ne doivent donc pas appartenir à un mot.

CaractèreRôleExemple
lettre, chiffre, tiret basdans un motJean n'est pas trouvé dans Jeanne
trait d'union, n'importe lequeldans un motJean n'est pas trouvé dans Jean-Paul, quel que soit le trait d'union qui les relie
tiret, demi-cadratin ou cadratinborne un motParis est trouvé dans Paris–Lyon
apostrophe, droite ou courbeborne un motAnne est trouvé dans d'Anne, Jean dans Jean's
espace, n'importe laquelleborne un motvoir Espaces Unicode

Les traits d'union sont celui de l'ASCII, le trait d'union et le trait d'union insécable que Word écrit à sa place, le trait d'union conditionnel, le maqaf hébreu, et toute autre ponctuation de tiret que Unicode nomme trait d'union. Ils forment WORD_JOIN_CHARS, dans piighost.text.boundaries.

L'apostrophe borne un mot dans toutes les langues, puisqu'elle termine un mot aussi souvent qu'elle se trouve à l'intérieur. En contrepartie, Brien est aussi trouvé dans O'Brien. Le masque couvre alors plus que demandé, mais ne laisse rien en clair.

La règle suppose des espaces entre les mots, elle ne trouve donc rien en chinois, en japonais ou en thaï, voir Limites.


Voir aussi