Content Filtering › Implementation: Rule-Based PII Filtering
Language AI HandbookAll chapters Content Filtering › Implementation: Rule-Based PII FilteringOne section · Sections in this chapter7 / 14 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's also implement a rule-based filter for detecting and redacting personally identifiable information (PII) in model outputs. This is a common practical need where the regularity of the patterns makes regex a better choice than ML.
import re
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class PIIMatch:
category: str
matched_text: str
start: int
end: int
replacement: str
# PII pattern registry: maps category name to (regex_pattern, replacement_text)
PII_PATTERNS = {
"email": (
r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b",
"[EMAIL REDACTED]",
),
"phone_us": (
r"\b(?:\+?1[\s.\-]?)?\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]\d{4}\b",
"[PHONE REDACTED]",
),
"credit_card": (
r"\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b",
"[CARD REDACTED]",
),
"ssn": (r"\b\d{3}-\d{2}-\d{4}\b", "[SSN REDACTED]"),
}
def detect_and_redact_pii(text: str) -> Tuple[str, List[PIIMatch]]:
"""
Detect PII using regex patterns and return the redacted text plus a list of matches.
Handles overlapping matches by sorting and applying replacements in order.
"""
# Collect all matches with their span and category
raw_matches = []
for category, (pattern, replacement) in PII_PATTERNS.items():
for m in re.finditer(pattern, text):
raw_matches.append(
(m.start(), m.end(), category, m.group(), replacement)
)
# Sort by start position to apply replacements left to right
raw_matches.sort(key=lambda x: x[0])
matches = []
redacted = text
offset = 0
for start, end, category, matched_text, replacement in raw_matches:
adj_start = start + offset
adj_end = end + offset
matches.append(
PIIMatch(
category=category,
matched_text=matched_text,
start=start,
end=end,
replacement=replacement,
)
)
redacted = redacted[:adj_start] + replacement + redacted[adj_end:]
offset += len(replacement) - (end - start)
return redacted, matches# Test PII redaction on model outputs
test_outputs = [
"You can reach me at john.doe@example.com or call 555-867-5309.",
"Please charge card number 4111-1111-1111-1111 for the order.",
"My SSN is 123-45-6789 and my email is alice@corp.net.",
"No sensitive information here, just a normal response.",
]
pii_results = []
for text in test_outputs:
redacted, matches = detect_and_redact_pii(text)
pii_results.append((text, redacted, matches))Original: You can reach me at john.doe@example.com or call 555-867-5309. Redacted: You can reach me at [EMAIL REDACTED] or call [PHONE REDACTED]. Detected: email: 'john.doe@example.com', phone_us: '555-867-5309' Original: Please charge card number 4111-1111-1111-1111 for the order. Redacted: Please charge card number [CARD REDACTED] for the order. Detected: credit_card: '4111-1111-1111-1111' Original: My SSN is 123-45-6789 and my email is alice@corp.net. Redacted: My SSN is [SSN REDACTED] and my email is [EMAIL REDACTED]. Detected: ssn: '123-45-6789', email: 'alice@corp.net' Original: No sensitive information here, just a normal response. Redacted: No sensitive information here, just a normal response. Detected: none
The rule-based PII filter reliably catches structured patterns. Notice how combining both ML classification and regex-based rule filters covers complementary failure modes: the ML classifier handles context-dependent semantic content (hate, threats, harmful instructions) while regex handles structured format violations (PII, card numbers, credentials) with higher precision.