Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

## [unreleased]

### Added

- Added Taiwan National ID (`TW_NATIONAL_ID`) predefined recognizer in `presidio-analyzer`.
- Added Taiwan phone number (`TW_PHONE_NUMBER`) predefined recognizer in `presidio-analyzer`.

### Anonymizer
### General
#### Fixed
Expand Down
2 changes: 2 additions & 0 deletions docs/supported_entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ For more information, refer to the [adding new recognizers documentation](analyz
| FieldType | Description | Detection Method |
|------------|---------------------------------------------------------------------------------------------------------|------------------------------------------|
| TH_TNIN | The Thai National ID Number (TNIN) is a unique 13-digit number issued to all Thai residents. | Pattern match, context and custom logic. |
| TW_NATIONAL_ID | Taiwan National Identification Number (國民身分證統一編號 / 身分證字號): 1 leading letter followed by 9 digits, with the second digit indicating holder category and a public checksum rule. | Pattern match, context and checksum |
| TW_PHONE_NUMBER | Taiwan phone number (電話號碼 / 手機號碼): validated by `python-phonenumbers` with the Taiwan (`TW`) region for mobile and landline formats. | Pattern match, context and region-aware phone validation |
Comment on lines +149 to +150

### Turkey

Expand Down
14 changes: 14 additions & 0 deletions presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,20 @@ recognizers:
enabled: false
country_code: th

- name: TwNationalIdRecognizer
supported_languages:
- zh
type: predefined
enabled: false
country_code: tw

- name: TwPhoneNumberRecognizer
supported_languages:
- zh
type: predefined
enabled: false
country_code: tw

- name: TrNationalIdRecognizer
supported_languages:
- tr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
)
from .country_specific.sweden.se_personnummer_recognizer import SePersonnummerRecognizer

# Taiwan recognizers
from .country_specific.taiwan.tw_national_id_recognizer import TwNationalIdRecognizer
from .country_specific.taiwan.tw_phone_number_recognizer import TwPhoneNumberRecognizer
Comment on lines +104 to +106

# Thai recognizers
from .country_specific.thai.th_tnin_recognizer import ThTninRecognizer

Expand Down Expand Up @@ -245,6 +249,8 @@
"KrFrnRecognizer",
"SeOrganisationsnummerRecognizer",
"ThTninRecognizer",
"TwNationalIdRecognizer",
"TwPhoneNumberRecognizer",
"TrLicensePlateRecognizer",
"TrNationalIdRecognizer",
"SePersonnummerRecognizer",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from typing import List, Optional, Tuple, Union

from presidio_analyzer import EntityRecognizer, Pattern, PatternRecognizer


class TwNationalIdRecognizer(PatternRecognizer):
"""
Recognize Taiwan National Identification Numbers.

Taiwan national IDs use one leading letter followed by 9 digits.
The second digit is typically 1 or 2, and the full value follows
a public checksum rule defined by Taiwan's National Identification Card
numbering scheme.

Public reference for format and checksum background:
https://www.ris.gov.tw/app/portal/3053

:param patterns: List of patterns to be used by this recognizer
:param context: List of context words to increase confidence in detection
:param supported_language: Language this recognizer supports
:param supported_entity: The entity this recognizer can detect
:param replacement_pairs: List of tuples with potential replacement values
for different strings to be used during pattern matching.
"""

COUNTRY_CODE = "tw"

PATTERNS = [
Pattern(
"TW_NATIONAL_ID",
r"\b[A-Z][12]\d{8}\b",
0.3,
),
]

CONTEXT = [
"身分證",
"身分證字號",
"身份證",
"身份證字號",
"國民身分證",
"national id",
"id number",
]

LETTER_MAPPING = {
"A": 10,
"B": 11,
"C": 12,
"D": 13,
"E": 14,
"F": 15,
"G": 16,
"H": 17,
"I": 34,
"J": 18,
"K": 19,
"L": 20,
"M": 21,
"N": 22,
"O": 35,
"P": 23,
"Q": 24,
"R": 25,
"S": 26,
"T": 27,
"U": 28,
"V": 29,
"W": 32,
"X": 30,
"Y": 31,
"Z": 33,
}

WEIGHTS = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1]

def __init__(
self,
patterns: Optional[List[Pattern]] = None,
context: Optional[List[str]] = None,
supported_language: str = "zh",
supported_entity: str = "TW_NATIONAL_ID",
replacement_pairs: Optional[List[Tuple[str, str]]] = None,
name: Optional[str] = None,
):
self.replacement_pairs = replacement_pairs if replacement_pairs else []

patterns = patterns if patterns else self.PATTERNS
context = context if context else self.CONTEXT
super().__init__(
supported_entity=supported_entity,
patterns=patterns,
context=context,
supported_language=supported_language,
name=name,
)

def validate_result(self, pattern_text: str) -> Union[bool, None]:
"""
Validate the pattern logic by running Taiwan ID checksum verification.

:param pattern_text: the text to validated.
Only the part in text that was detected by the regex engine
:return: A bool or None, indicating whether the validation was successful.
"""
sanitized_value = EntityRecognizer.sanitize_value(
pattern_text, self.replacement_pairs
)

if len(sanitized_value) != 10:
return False

if not sanitized_value[0].isalpha() or not sanitized_value[1:].isdigit():
return False

sanitized_value = sanitized_value.upper()

if sanitized_value[0] not in self.LETTER_MAPPING:
return False

if sanitized_value[1] not in {"1", "2"}:
return False

return self._validate_checksum(sanitized_value)

def _validate_checksum(self, national_id: str) -> bool:
"""Validate Taiwan National ID using the public checksum algorithm."""
mapped_prefix = str(self.LETTER_MAPPING[national_id[0]])
digits = [int(digit) for digit in mapped_prefix + national_id[1:]]
checksum = sum(digit * weight for digit, weight in zip(digits, self.WEIGHTS))
return checksum % 10 == 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import List, Optional

from presidio_analyzer.predefined_recognizers.generic.phone_recognizer import (
PhoneRecognizer,
)


class TwPhoneNumberRecognizer(PhoneRecognizer):
"""
Recognize Taiwan phone numbers using python-phonenumbers with TW region hints.

This recognizer intentionally delegates parsing and validation to the
generic PhoneRecognizer with the supported region restricted to Taiwan.

Public reference for numbering background:
https://www.itu.int/itudoc/itu-t/ob-lists/icc/e164_886.html

:param context: Base context words for enhancing the assurance scores.
:param supported_language: Language this recognizer supports
:param supported_entity: The entity this recognizer can detect
:param name: Optional recognizer name override
"""

COUNTRY_CODE = "tw"

CONTEXT = [
"電話",
"電話號碼",
"手機",
"手機號碼",
"行動電話",
"聯絡電話",
"市話",
"office phone",
"phone",
"phone number",
"mobile",
"cell",
"call",
"contact",
]

def __init__(
self,
context: Optional[List[str]] = None,
supported_language: str = "zh",
supported_entity: str = "TW_PHONE_NUMBER",
name: Optional[str] = None,
):
super().__init__(
context=context if context else self.CONTEXT,
supported_language=supported_language,
supported_entity=supported_entity,
supported_regions=["TW"],
name=name,
)
23 changes: 23 additions & 0 deletions presidio-analyzer/tests/test_recognizer_registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

import pytest
import yaml
import regex as re
from presidio_analyzer import (
AnalyzerEngine,
Expand All @@ -10,6 +11,7 @@
RecognizerRegistry,
)
from presidio_analyzer.predefined_recognizers import SpacyRecognizer, UsSsnRecognizer
from presidio_analyzer.recognizer_registry.recognizers_loader_utils import RecognizerListLoader


def create_mock_pattern_recognizer(lang, entity, name):
Expand Down Expand Up @@ -617,3 +619,24 @@ def test_load_predefined_recognizers_validates_countries_input():
# ---------------------------------------------------------------------------
# YAML ``country_code`` cross-validation
# ---------------------------------------------------------------------------


def test_default_yaml_defines_tw_recognizers_and_loader_can_resolve_classes():
"""Default YAML should define Taiwan recognizers with matching loader classes."""
config_path = Path(__file__).resolve().parents[1] / "presidio_analyzer" / "conf" / "default_recognizers.yaml"
config = yaml.safe_load(config_path.read_text())

recognizer_entries = {
entry["name"]: entry for entry in config["recognizers"] if isinstance(entry, dict)
}

tw_national_id = recognizer_entries["TwNationalIdRecognizer"]
tw_phone = recognizer_entries["TwPhoneNumberRecognizer"]

assert tw_national_id["supported_languages"] == ["zh"]
assert tw_national_id["country_code"] == "tw"
assert tw_phone["supported_languages"] == ["zh"]
assert tw_phone["country_code"] == "tw"

assert RecognizerListLoader.get_existing_recognizer_cls("TwNationalIdRecognizer").__name__ == "TwNationalIdRecognizer"
assert RecognizerListLoader.get_existing_recognizer_cls("TwPhoneNumberRecognizer").__name__ == "TwPhoneNumberRecognizer"
Loading