mn3040/pii-detection-and-redaction
GitHub: mn3040/pii-detection-and-redaction
一个透明且可解释的 PII 隐私检测与脱敏流水线,结合正则规则与 spaCy NER 模型扫描文本文件,并内置针对 Microsoft Presidio 的基准评估体系。
Stars: 0 | Forks: 0
# PII 检测与脱敏 Pipeline
[](https://github.com/mn3040/pii-detection-and-redaction/actions/workflows/tests.yml)
大多数 PII 检测工具都是构建在“黑盒”之上的配置文件。你将它们指向数据,获取返回的标记,却完全不清楚是什么触发了检测或为什么触发。我想了解这些规则到底是如何运作的,所以我从头构建了每一个检测器——然后将其与 Microsoft Presidio 进行基准测试,以探究专用 pipeline 在哪里占优,又在哪里失利。
本项目使用两层方法扫描 `.txt`、`.csv` 和 `.json` 文件以查找个人身份信息(PII):针对结构化格式(SSN、电子邮件、电话、信用卡、IP、地址、出生日期)的严格 regex 规则,以及针对非结构化文本(姓名、地点、组织)的 spaCy NER 模型。它会报告精确的字符跨度,编写脱敏副本,针对合成标记数据评估精确率和召回率,并包含一个对抗性测试套件,用于探测基于 regex 的检测无法处理的规避技术。
**在线体验:** https://mn3040.github.io/pii-detection-and-redaction/
## 它的功能
- 使用专用的 regex 检测器检测 SSN、电子邮件、电话号码、信用卡、IP 地址、街道地址和出生日期。
- 使用 spaCy NER 检测姓名、位置和组织,并通过后置过滤器拒绝未通过合法性检查的检测结果。
- 编写调查报告(JSON 或 CSV),包含实体类型、匹配文本、字符跨度、置信度分数、源文件和行号。
- 编写带有 `[REDACTED_TYPE]` 占位符的脱敏副本,在掩码前解决重叠问题,从而确保偏移量永远不会损坏。
- 针对合成测试集、对抗性边缘案例套件以及 CoNLL-2003 新闻语料库进行评估,以衡量领域迁移效果。
- 在相同的测试集上直接与 Microsoft Presidio 进行比较。
## 为什么构建这个项目
生产级 PII 工具是为合规工作流设计的。它们将检测逻辑隐藏在策略文件和仪表板之后。
我想要理解这些规则,而不是盲目信任它们。本项目专注于透明度:每一个 regex 都带有注释,每一个置信度分数都来源于有据可查的启发式方法,并且评估 pipeline 清晰地展示了检测器在哪里成功以及在哪里存在不足。从头开始构建,然后与生产级库进行基准测试,是了解实际权衡的唯一诚实方法。
## 架构
每个检测器都遵循相同的契约:
```
detect(text) -> List[PartialDetection]
```
检测器返回 `PartialDetection` 对象——匹配的文本、跨度、实体类型、置信度分数。引擎添加源文件上下文,并将其提升为完整的 `Detection`。脱敏组件要么编写报告,要么直接替换选定的跨度。
```
Input files
└── PIIEngine
├── RegexDetector × 7 → PartialDetection list
└── SpacyNERDetector → PartialDetection list
│
promote to Detection (source_file, line_number)
│
Redactor
├── write_report() → JSON / CSV
└── write_masked_directory() → redacted copies
```
这个契约使得添加新的 PII 类型变得轻而易举:添加一个检测器类,注册它,CLI、报告、掩码和评估代码将继续保持不变地工作。
检测器被定义为 `Protocol` 而不是基类。这意味着任何具有正确 `detect` 方法签名的对象都是一个有效的检测器——无需子类化,无需继承样板代码。Python 的结构化类型检查可确保满足接口要求,而无需强制执行类层次结构。
## 项目布局
```
pii_scan.py CLI entry point
engine.py Orchestrates detectors over input files
redactor.py Report writer and mask writer
detectors/
base.py Detection dataclasses and Detector protocol
regex_detectors.py Structured PII detectors
ner_detector.py spaCy NER wrapper with legitimacy post-filter
data_gen/
generate_test_data.py Synthetic labeled test set generator
eval/
evaluate.py Precision / recall / F1 with threshold sweep
adversarial_cases.py Hand-crafted edge cases and evasion tests
conll_eval.py CoNLL-2003 domain-transfer evaluation
presidio_compare.py Side-by-side comparison vs. Microsoft Presidio
plots.py Generates all charts from live eval results
test_dataset/ Generated labels and text
docs/
assets/ Charts generated by eval/plots.py
index.html / app.js / ... GitHub Pages browser demo
tests/ pytest unit tests
sample_data/ Example input file
requirements.txt
```
## 设置
```
python -m venv venv
source venv/Scripts/activate # Windows Git Bash
pip install -r requirements.txt
python -m spacy download en_core_web_sm
```
在 Windows PowerShell 上:
```
venv\Scripts\Activate.ps1
```
## 运行 CLI
在不触碰源文件的情况下编写调查报告:
```
python pii_scan.py --input ./sample_data --mode report --output results.json
```
将脱敏副本写入单独的目录:
```
python pii_scan.py --input ./sample_data --mode mask --output ./redacted
```
实用选项:
| 标志 | 用途 |
|---|---|
| `--no-ner` | 仅运行 regex 检测器——速度更快,NER 零误报 |
| `--spacy-model en_core_web_trf` | 当准确性比速度更重要时,使用更大的 spaCy 模型 |
## 示例
输入:
```
Hi, my name is John Smith and I live in Chicago.
You can reach me at john.smith@example.com or call (312) 555-0198.
My SSN is 412-34-5678 and my card number is 4532015112830366.
```
掩码后的输出:
```
Hi, my name is [REDACTED_PERSON] and I live in [REDACTED_LOCATION].
You can reach me at [REDACTED_EMAIL] or call [REDACTED_PHONE].
My SSN is [REDACTED_SSN] and my card number is [REDACTED_CREDIT_CARD].
```
报告条目:
```
{
"entity_type": "SSN",
"text": "412-34-5678",
"start": 10,
"end": 21,
"confidence": 1.0,
"source_file": "sample_data/sample.txt",
"line_number": 3,
"detector": "regex"
}
```
## 检测器的工作原理
### Regex 检测器 (`detectors/regex_detectors.py`)
每个 regex 检测器都针对一种 PII 格式。所有 regex 匹配的置信度均为 `1.0`——如果模式触发,根据定义,该格式就是精确的。
**SSN** —— 匹配 `NNN-NN-NNNN` 并拒绝美国社会安全局从未签发过的 SSN:区号 `000`、`666` 和 `900`–`999`;组码 `00`;序列码 `0000`。这些排除项被编写为负向先行断言,因此它们可以在不消耗字符的情况下进行过滤。
**Email** —— 匹配本地部分、`@`、域名和 TLD。支持子域名和加号寻址(`jane+work@sub.example.co.uk`)。
**Phone** —— 匹配带有破折号、点、括号或空格的美国格式,带有可选的 `+1` 国家代码。先行断言和后行断言可防止模式匹配较长数字字符串的一部分,因此信用卡号码永远不会产生电话匹配。
**Credit card** —— 匹配带有可选空格或破折号分隔符的 13-19 位数字符串,然后在接受匹配之前运行 Luhn 校验和。Luhn 算法是每个主要卡网络用来捕获转录错误的校验和。从右到左遍历数字字符串:每隔一个数字乘以 2,从 9 以上的任何结果中减去 9,然后将所有结果相加。有效的卡号总和将是 10 的倍数。这意味着检测器永远不会标记随机的 16 位数字——只有结构上有效的卡号才能通过。
**IP address** —— 匹配四个以点分隔的八位字节,并通过枚举有效范围(`25[0-5]`、`2[0-4]\d`、`1\d{2}`、`[1-9]\d`、`\d`)而不是匹配任意三位数字序列,将每个八位字节针对 `0`–`255` 进行验证。
**Street address** —— 匹配街道编号,后跟大写名称和可识别的后缀词。后缀列表涵盖 60 多种 USPS 街道后缀类型,包括完整单词(`Street`、`Avenue`、`Trail`)和缩写(`St`、`Ave`、`Trl`),以及 Faker 生成的较不常见类型(`Loop`、`Cove`、`Forest`、`Mews`)。置信度为 `0.85`。
**Date of birth** —— 匹配 `MM/DD/YYYY`、`YYYY-MM-DD` 和 `MM-DD-YYYY`,年份范围限制在 `1900`–`2019`。排除近十年的日期以避免标记近期的时间戳。置信度为 `0.8`。
### NER 检测器 (`detectors/ner_detector.py`)
spaCy 检测器运行 `en_core_web_sm` 并将实体标签映射到 pipeline 的实体类型。在 spaCy 返回结果后,`_is_valid_org` 后置过滤器会筛选每一个 ORGANIZATION 检测:
1. **黑名单检查** —— 像 `SSN`、`Card`、`Email` 这样的单个 token 会被立即拒绝。这些是小型 NER 模型在结构化 PII 文本上最常见的误报。
2. **合法性检查** —— 只有当检测包含可识别的公司后缀(`Inc`、`LLC`、`Corp`、`University` 等)或长度至少为两个单词时,才会被接受。
此过滤器将 ORGANIZATION 的精确率从 0.227 提升到了 0.800——这就是一个充满噪音的检测器和一个有用的检测器之间的差别。
置信度分数是启发式的,而不是经过校准的概率:
| spaCy 标签 | 映射到 | 置信度 |
|---|---|---:|
| PERSON | PERSON | 0.75 |
| GPE | LOCATION | 0.70 |
| ORG | ORGANIZATION | 0.65 |
| DATE | DATE | 0.60 |
这些分数的存在是为了让脱敏组件在 regex 匹配和 NER 匹配重叠时解决冲突——置信度高的胜出。
### 重叠解决 (`redactor.py`)
在替换任何跨度之前,`_resolve_overlaps` 会按置信度降序对所有检测进行排序(然后以跨度长度作为决胜条件),进行迭代,并跳过任何跨度触及已被声明的检测的结果。替换是从右到左应用的,因此先前的偏移量在整个过程中保持有效。
## 评估
### 测试集证明了什么以及未证明什么
主要的测试集是合成的:`data_gen/generate_test_data.py` 使用 Faker 生成虚假的 PII,并将真实参考跨度写入 `eval/test_dataset/labels.json`,因此代码库绝不包含真实的个人数据。这是有意为之的——但它产生了一个值得说明的循环依赖。Faker 生成的 SSN 带有破折号,电子邮件采用标准形式,电话号码采用美国格式。regex 检测器在编写时正好考虑了这些格式,因此在 Faker 数据上接近完美的 regex 分数部分上是自我实现的。
有三件事打破了这种循环依赖:涵盖规避技术的纯手工对抗性套件、针对 CoNLL-2003 新闻语料库的领域迁移检查,以及在相同测试集上与 Microsoft Presidio 的直接比较。
运行所有内容:
```
python data_gen/generate_test_data.py # regenerate synthetic test set
python eval/evaluate.py # main eval with threshold sweep
python eval/adversarial_cases.py # edge cases and evasion analysis
python eval/conll_eval.py # domain-transfer (requires: pip install datasets)
python eval/presidio_compare.py # vs. Microsoft Presidio
python eval/plots.py # regenerate all charts from live results
```
### 合成测试集结果

| 检测器集合 | 精确率 | 召回率 | F1 |
|---|---:|---:|---:|
| 仅 Regex | 1.000 | 0.577 | 0.732 |
| 仅 NER | 0.443 | 0.361 | 0.398 |
| 组合(阈值 ≥ 0.65) | 0.919 | 0.938 | 0.929 |
推荐的工作点是置信度阈值 ≥ 0.65。在此阈值下,包含 PERSON、LOCATION 和 ORGANIZATION;排除 NER DATE 检测。spaCy 的 `DATE` 实体会匹配所有日期——会议日期、发布日期、时间戳——而不仅仅是出生日期。在阈值 ≥ 0.60 时包含它会使整体精确率从 0.919 下降到 0.674,而召回率没有任何提升,因为相对于我们的 `DATE_OF_BIRTH` 真实参考数据,每个 NER 检测到的日期都是误报。
### 按实体类型划分的精确率-召回率

| 实体类型 | 精确率 | 召回率 | F1 | 检测器 |
|---|---:|---:|---:|---|
| SSN | 1.000 | 1.000 | 1.000 | regex |
| EMAIL | 1.000 | 1.000 | 1.000 | regex |
| PHONE | 1.000 | 1.000 | 1.000 | regex |
| CREDIT_CARD | 1.000 | 1.000 | 1.000 | regex |
| IP_ADDRESS | 1.000 | 1.000 | 1.000 | regex |
| DATE_OF_BIRTH | 1.000 | 1.000 | 1.000 | regex |
| STREET_ADDRESS | 1.000 | 1.000 | 1.000 | regex |
| PERSON | 0.808 | 0.875 | 0.840 | NER |
| LOCATION | 0.833 | 0.909 | 0.870 | NER |
| ORGANIZATION | 0.800 | 0.667 | 0.727 | NER + 过滤器 |
在将后缀列表从 10 种扩展到 60 多种 USPS 后缀类型后,STREET_ADDRESS 的 F1 从 0.333 提升到了 1.000。在添加了 `_is_valid_org` 后置过滤器后,ORGANIZATION 的 F1 从 0.357 提升到了 0.727。这两项都是通过检查实际的 Faker 输出来诊断的,而不是假设模式是正确的。
### 置信度阈值扫描

| 阈值 | 包含内容 | 精确率 | 召回率 | F1 |
|---|---|---:|---:|---:|
| ≥ 1.00 | 仅 Regex | 1.000 | 0.505 | 0.671 |
| ≥ 0.75 | + PERSON | 0.939 | 0.794 | 0.860 |
| ≥ 0.70 | + LOCATION | 0.926 | 0.897 | 0.911 |
| ≥ 0.65 | + ORGANIZATION | 0.919 | 0.938 | **0.929 |
| ≥ 0.60 | + NER DATE(不推荐) | 0.674 | 0.938 | 0.784 |
### 与 Microsoft Presidio 的比较
Presidio 是 Microsoft 的生产级开源 PII 检测库。在相同的测试集上运行这两个系统,可以确切地展示专用 pipeline 在哪里获胜,在哪里失利。
| 实体类型 | Presidio F1 | 本项目 F1 | 获胜者 |
|---|---:|---:|---|
| DATE_OF_BIRTH | 0.103 | 1.000 | **本项目** |
| PHONE | 0.857 | 1.000 | **本项目** |
| LOCATION | 0.800 | 0.870 | **本项目** |
| PERSON | 0.863 | 0.840 | Presidio |
| EMAIL | 1.000 | 1.000 | 平局 |
| CREDIT_CARD | 1.000 | 1.000 | 平局 |
| SSN | 1.000 | 1.000 | 平局 |
| IP_ADDRESS | 1.000 | 1.000 | 平局 |
运行 `python eval/presidio_compare.py` 进行重现。DATE_OF_BIRTH 的差距是方法论上的差异:Presidio 检测所有日期并将所有内容报告为 `DATE_TIME`,这导致我们的仅限出生日期的真实参考数据出现大量误报。我们的年份范围过滤器(`1900`–`2019`)是造成这种差异的原因。PHONE 和 LOCATION 的胜利反映了真实的模式差异。STREET_ADDRESS 被排除在此比较之外——Presidio 没有专门的街道地址识别器。
### 对抗鲁棒性
基于 regex 的 PII 检测极其容易被规避。对抗性套件明确记录了这一点,而不是假装情况并非如此。

**对该 pipeline 有效的规避技术:**
- **空格**:`j o h n @ e x a m p l e . c o m` —— 插入空格会破坏每一个 regex 模式。对于任何带空格的 PII,召回率降至零。
- **Unicode 同形异义字**:用全角 `@` (U+FF20) 替换 `@` 或用西里尔字母 `а` (U+0430) 替换拉丁字母 `a` 会产生对人类来说看起来相同但与 ASCII 模式不匹配的文本。
- **单词形式**:写下 `"four one two dash three four dash five six seven eight"` 而不是 `412-34-5678`,需要模式匹配无法提供的语义理解。
这些不是 bug——它们是该方法的根本局限。生产系统通过规范化预处理(同形异义字)、更大的 NER 模型或 LLM(单词形式)来解决这些问题,并承认有动机的对手总是可以规避基于规则的系统。记录这些技术的目的是为了明确这种边界。
### 领域迁移:CoNLL-2003
CoNLL-2003 是 1996 年的路透社新闻语料,标记有 PERSON、ORG 和 LOC。它没有结构化的 PII,因此这里只能比较 NER 检测器。运行 `eval/conll_eval.py` 可以展示系统在为其未设计的文本上的性能。分数低于在 Faker 测试集上的分数——这是领域转移带来的真实代价,而不是校准问题。
### 生产建议
| 实体类型 | 推荐用于自动脱敏? |
|---|---|
| SSN、EMAIL、PHONE、CREDIT_CARD、IP_ADDRESS、DATE_OF_BIRTH | 是 —— regex,精确率 1.0 |
| STREET_ADDRESS | 是 —— 精确率 1.0,在标准美国格式上召回率 1.0 |
| PERSON、LOCATION | 是 —— NER,精确率 0.81–0.83 |
| ORGANIZATION | 需人工复核 —— 过滤后精确率 0.80,但召回率为 0.67 |
| NER DATE | 否 —— 改用 DATE_OF_BIRTH regex |
使用置信度阈值 ≥ 0.65 作为默认工作点。对于精确率比召回率更重要的仅结构化扫描,请使用 `--no-ner`。
## GitHub Pages 演示
`docs/` 目录是 regex 检测器层的客户端重新实现——没有 Python 后端,也没有任何内容发送到服务器。它在 JavaScript 中运行相同的七个检测器,将检测结果渲染为带有悬停工具提示的颜色编码内联高亮,并并排显示掩码输出和检测表。
JS 移植版直接镜像了 Python 检测器的逻辑。Luhn 算法、SSN 排除先行断言、电话后行断言——所有这些都在 `docs/detectors.js` 中,可读且可在浏览器控制台中测试。
部署由 `.github/workflows/pages.yml` 处理,每当 `main` 发生变化时,它就会将 `docs/` 发布到 GitHub Pages。
## 测试
```
pytest tests/
```
`tests/test_detectors.py` 涵盖了所有七个 regex 检测器和重叠解决程序——总共 42 个测试。SSN 测试包括无效区号排除和跨度准确性;信用卡测试直接调用 `_luhn_valid` 并涵盖空格和破折号分隔的格式;重叠测试验证置信度高的胜出、在平局时跨度长的胜出、非重叠检测是否都被保留,以及三方重叠是否解决为单一的最佳检测。
## 范围之外
此版本有意排除了 OCR 和图像检测、多语言模型以及生产访问控制。其目标是构建一个具有透明行为和可衡量准确性的专注于文本的 pipeline。对于生产部署,请参阅 Microsoft Presidio 或 AWS Comprehend——本项目的价值在于其可解释性和记录在案的权衡,而不是取代那些工具。
标签:spaCy, 安全规则引擎, 对抗攻击, 敏感信息检测, 数据合规, 数据脱敏, 文本处理, 逆向工具, 隐私工程