Files
presidio/presidio-analyzer/presidio_analyzer/recognizer_registry/recognizer_registry.py
Sharon Hart cb0184afa2 Add Ruff linter + Apply Ruff fix (#1379)
* Add Ruff linter + Apply Ruff fix

* Move up linting

* Move up linting

* Move up linting

* docs

* docs
2024-05-12 10:16:47 +03:00

416 lines
13 KiB
Python

import copy
import logging
from typing import Optional, List, Iterable, Union, Type, Dict
import yaml
import regex as re
from pathlib import Path
from presidio_analyzer import EntityRecognizer, PatternRecognizer
from presidio_analyzer.nlp_engine import (
NlpEngine,
SpacyNlpEngine,
StanzaNlpEngine,
TransformersNlpEngine,
)
from presidio_analyzer.predefined_recognizers import (
CreditCardRecognizer,
CryptoRecognizer,
DateRecognizer,
EmailRecognizer,
IbanRecognizer,
IpRecognizer,
MedicalLicenseRecognizer,
NhsRecognizer,
PhoneRecognizer,
UrlRecognizer,
UsBankRecognizer,
UsLicenseRecognizer,
UsItinRecognizer,
UsPassportRecognizer,
UsSsnRecognizer,
SgFinRecognizer,
SpacyRecognizer,
EsNifRecognizer,
EsNieRecognizer,
StanzaRecognizer,
AuAbnRecognizer,
AuAcnRecognizer,
AuTfnRecognizer,
AuMedicareRecognizer,
ItDriverLicenseRecognizer,
ItFiscalCodeRecognizer,
ItVatCodeRecognizer,
TransformersRecognizer,
ItPassportRecognizer,
ItIdentityCardRecognizer,
InPanRecognizer,
PlPeselRecognizer,
InAadhaarRecognizer,
InVehicleRegistrationRecognizer,
InVoterRecognizer,
InPassportRecognizer,
)
logger = logging.getLogger("presidio-analyzer")
class RecognizerRegistry:
"""
Detect, register and hold all recognizers to be used by the analyzer.
:param recognizers: An optional list of recognizers,
that will be available instead of the predefined recognizers
:param global_regex_flags : regex flags to be used in regex matching,
including deny-lists
"""
def __init__(
self,
recognizers: Optional[Iterable[EntityRecognizer]] = None,
global_regex_flags: Optional[int] = re.DOTALL | re.MULTILINE | re.IGNORECASE,
supported_languages: Optional[List[str]] = None,
):
if recognizers:
self.recognizers = recognizers
else:
self.recognizers = []
self.global_regex_flags = global_regex_flags
self.supported_languages = (
supported_languages if supported_languages else ["en"]
)
def _create_nlp_recognizer(
self, nlp_engine: NlpEngine = None, supported_language: str = None
) -> SpacyRecognizer:
nlp_recognizer = self._get_nlp_recognizer(nlp_engine)
if nlp_engine:
return nlp_recognizer(
supported_language=supported_language,
supported_entities=nlp_engine.get_supported_entities(),
)
return nlp_recognizer(supported_language=supported_language)
def add_nlp_recognizer(self, nlp_engine: NlpEngine) -> None:
"""
Adding NLP recognizer in accordance with the nlp engine.
:param nlp_engine: The NLP engine.
:return: None
"""
if not nlp_engine:
supported_languages = self.supported_languages
else:
supported_languages = nlp_engine.get_supported_languages()
self.recognizers.extend(
[
self._create_nlp_recognizer(
nlp_engine=nlp_engine, supported_language=supported_language
)
for supported_language in supported_languages
]
)
def load_predefined_recognizers(
self, languages: Optional[List[str]] = None, nlp_engine: NlpEngine = None
) -> None:
"""
Load the existing recognizers into memory.
:param languages: List of languages for which to load recognizers
:param nlp_engine: The NLP engine to use.
:return: None
"""
if not languages:
languages = ["en"]
recognizers_map = {
"en": [
UsBankRecognizer,
UsLicenseRecognizer,
UsItinRecognizer,
UsPassportRecognizer,
UsSsnRecognizer,
NhsRecognizer,
SgFinRecognizer,
AuAbnRecognizer,
AuAcnRecognizer,
AuTfnRecognizer,
AuMedicareRecognizer,
InPanRecognizer,
InAadhaarRecognizer,
InVehicleRegistrationRecognizer,
InVoterRecognizer,
InPassportRecognizer,
],
"es": [
EsNifRecognizer,
EsNieRecognizer,
],
"it": [
ItDriverLicenseRecognizer,
ItFiscalCodeRecognizer,
ItVatCodeRecognizer,
ItIdentityCardRecognizer,
ItPassportRecognizer,
],
"pl": [PlPeselRecognizer],
"ALL": [
CreditCardRecognizer,
CryptoRecognizer,
DateRecognizer,
EmailRecognizer,
IbanRecognizer,
IpRecognizer,
MedicalLicenseRecognizer,
PhoneRecognizer,
UrlRecognizer,
],
}
for lang in languages:
lang_recognizers = [
self.__instantiate_recognizer(
recognizer_class=rc, supported_language=lang
)
for rc in recognizers_map.get(lang, [])
]
self.recognizers.extend(lang_recognizers)
all_recognizers = [
self.__instantiate_recognizer(
recognizer_class=rc, supported_language=lang
)
for rc in recognizers_map.get("ALL", [])
]
self.recognizers.extend(all_recognizers)
self.add_nlp_recognizer(nlp_engine=nlp_engine)
@staticmethod
def _get_nlp_recognizer(
nlp_engine: NlpEngine,
) -> Type[SpacyRecognizer]:
"""Return the recognizer leveraging the selected NLP Engine."""
if isinstance(nlp_engine, StanzaNlpEngine):
return StanzaRecognizer
if isinstance(nlp_engine, TransformersNlpEngine):
return TransformersRecognizer
if not nlp_engine or isinstance(nlp_engine, SpacyNlpEngine):
return SpacyRecognizer
else:
logger.warning(
"nlp engine should be either SpacyNlpEngine,"
"StanzaNlpEngine or TransformersNlpEngine"
)
# Returning default
return SpacyRecognizer
def get_recognizers(
self,
language: str,
entities: Optional[List[str]] = None,
all_fields: bool = False,
ad_hoc_recognizers: Optional[List[EntityRecognizer]] = None,
) -> List[EntityRecognizer]:
"""
Return a list of recognizers which supports the specified name and language.
:param entities: the requested entities
:param language: the requested language
:param all_fields: a flag to return all fields of a requested language.
:param ad_hoc_recognizers: Additional recognizers provided by the user
as part of the request
:return: A list of the recognizers which supports the supplied entities
and language
"""
if language is None:
raise ValueError("No language provided")
if entities is None and all_fields is False:
raise ValueError("No entities provided")
all_possible_recognizers = copy.copy(self.recognizers)
if ad_hoc_recognizers:
all_possible_recognizers.extend(ad_hoc_recognizers)
# filter out unwanted recognizers
to_return = set()
if all_fields:
to_return = [
rec
for rec in all_possible_recognizers
if language == rec.supported_language
]
else:
for entity in entities:
subset = [
rec
for rec in all_possible_recognizers
if entity in rec.supported_entities
and language == rec.supported_language
]
if not subset:
logger.warning(
"Entity %s doesn't have the corresponding"
" recognizer in language : %s",
entity,
language,
)
else:
to_return.update(set(subset))
logger.debug(
"Returning a total of %s recognizers",
str(len(to_return)),
)
if not to_return:
raise ValueError("No matching recognizers were found to serve the request.")
return list(to_return)
def add_recognizer(self, recognizer: EntityRecognizer) -> None:
"""
Add a new recognizer to the list of recognizers.
:param recognizer: Recognizer to add
"""
if not isinstance(recognizer, EntityRecognizer):
raise ValueError("Input is not of type EntityRecognizer")
self.recognizers.append(recognizer)
def remove_recognizer(
self, recognizer_name: str, language: Optional[str] = None
) -> None:
"""
Remove a recognizer based on its name.
:param recognizer_name: Name of recognizer to remove
:param language: The supported language of the recognizer to be removed,
in case multiple recognizers with the same name are present,
and only one should be removed.
"""
if not language:
new_recognizers = [
rec for rec in self.recognizers if rec.name != recognizer_name
]
logger.info(
"Removed %s recognizers which had the name %s",
str(len(self.recognizers) - len(new_recognizers)),
recognizer_name,
)
else:
new_recognizers = [
rec
for rec in self.recognizers
if rec.name != recognizer_name or rec.supported_language != language
]
logger.info(
"Removed %s recognizers which had the name %s and language %s",
str(len(self.recognizers) - len(new_recognizers)),
recognizer_name,
language,
)
self.recognizers = new_recognizers
def add_pattern_recognizer_from_dict(self, recognizer_dict: Dict) -> None:
"""
Load a pattern recognizer from a Dict into the recognizer registry.
:param recognizer_dict: Dict holding a serialization of an PatternRecognizer
:example:
>>> registry = RecognizerRegistry()
>>> recognizer = { "name": "Titles Recognizer", "supported_language": "de","supported_entity": "TITLE", "deny_list": ["Mr.","Mrs."]}
>>> registry.add_pattern_recognizer_from_dict(recognizer)
""" # noqa: E501
recognizer = PatternRecognizer.from_dict(recognizer_dict)
self.add_recognizer(recognizer)
def add_recognizers_from_yaml(self, yml_path: Union[str, Path]) -> None:
r"""
Read YAML file and load recognizers into the recognizer registry.
See example yaml file here:
https://github.com/microsoft/presidio/blob/main/presidio-analyzer/presidio_analyzer/conf/example_recognizers.yaml
:example:
>>> yaml_file = "recognizers.yaml"
>>> registry = RecognizerRegistry()
>>> registry.add_recognizers_from_yaml(yaml_file)
"""
try:
with open(yml_path, "r") as stream:
yaml_recognizers = yaml.safe_load(stream)
for yaml_recognizer in yaml_recognizers["recognizers"]:
self.add_pattern_recognizer_from_dict(yaml_recognizer)
except IOError as io_error:
print(f"Error reading file {yml_path}")
raise io_error
except yaml.YAMLError as yaml_error:
print(f"Failed to parse file {yml_path}")
raise yaml_error
except TypeError as yaml_error:
print(f"Failed to parse file {yml_path}")
raise yaml_error
def __instantiate_recognizer(
self, recognizer_class: Type[EntityRecognizer], supported_language: str
):
"""
Instantiate a recognizer class given type and input.
:param recognizer_class: Class object of the recognizer
:param supported_language: Language this recognizer should support
"""
inst = recognizer_class(supported_language=supported_language)
if isinstance(inst, PatternRecognizer):
inst.global_regex_flags = self.global_regex_flags
return inst
def _get_supported_languages(self) -> List[str]:
languages = []
for rec in self.recognizers:
languages.append(rec.supported_language)
return list(set(languages))
def get_supported_entities(
self, languages: Optional[List[str]] = None
) -> List[str]:
"""
Return the supported entities by the set of recognizers loaded.
:param languages: The languages to get the supported entities for.
If languages=None, returns all entities for all languages.
"""
if not languages:
languages = self._get_supported_languages()
supported_entities = []
for language in languages:
recognizers = self.get_recognizers(language=language, all_fields=True)
for recognizer in recognizers:
supported_entities.extend(recognizer.get_supported_entities())
return list(set(supported_entities))