客户数据脱敏 / Redact PII Before Sharing
你现在运行 redact-pii 技能。目标:丢一文件夹混合输入(TXT / CSV / PDF / 图片),就地脱敏出一个结构完全一致的 redacted/——所有姓名、邮箱、手机号、身份证、信用卡、地址、医保号都替换成稳定占位符;外加一份 diff.html 左右对照页和一份 report.json 审计明细。全程本地,零 LLM,零远端 API,零商业兜底转发。
底层是 Microsoft Presidio 的四个官方 pip 包(MIT 协议,Microsoft 维护,PyPI 稳定发布)。本技能不再包一层 wrapper —— 直接调用上游 4 个引擎;课程的价值是 agent-ready SOP + zh-CN 自定义 recognizer + 统一占位符 / 报告契约。
这门课做什么(边界写在第一屏)
- ✅ 做:把一文件夹混合的
.txt / .md / .json / .csv / .tsv / .pdf / .png / .jpg一次脱敏,输出同名同结构的redacted/;同一字面值跨文件保留同一占位符 id,方便下游分析关联。 - ✅ 覆盖语种:默认同时加载 spaCy
en_core_web_lg与zh_core_web_lg,对中文环境额外加载本课的 zh-CN custom recognizer(大陆手机号 / 身份证 / 统一社会信用代码 / 中文姓名 + 上下文)。 - ❌ 不做:扫描件之外的复杂文档结构还原(多栏 PDF、含公式、图表抽提)—— 那是
parse-docs课程的领域。我们只负责把扫描件 PNG/JPG 里被 OCR 出来的 PII 文本盖上黑框。 - ❌ 不做:任何调用 LLM / 商业 API 的兜底。Presidio 的可选 LLM recognizer 增强仅在本文末尾「Optional 增强」小节里描述,默认关闭,需要用户显式 opt-in 才能开。
- 🔒 铁律:SOP、参考脚本
redact.py、popularTasks 默认执行路径不联网,不允许调任何商业 LLM SDK、任何 LLM 兜底端点、任何远端推理服务、任何兼容性转发层;本地 LLM HTTP 网关也禁用。具体被禁用的产品名见本文末尾 Optional 小节(默认 off)。
前置条件
- Python ≥ 3.10(
python3 --version检查) - 本地
tesseractOCR + 中英语言包(图片 / 扫描件路径需要)- macOS:
brew install tesseract tesseract-lang - Ubuntu/Debian:
sudo apt-get install tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim - Windows:官方 installer + 勾选中文语言包
- macOS:
- 首次跑会拉两份 spaCy 模型,合计约 1.2 GB,下载一次即可,全程本地、无需任何 API key
安装(一次到位)
强烈建议在 venv 里装,避免和系统 Python 打架(Ubuntu 24+ 的 PEP 668 会拒绝直接安装):
python3 -m venv .pii_venv
source .pii_venv/bin/activate
pip install --upgrade pip
pip install presidio-analyzer presidio-anonymizer presidio-image-redactor presidio-structured
pip install pandas pypdf Pillow click
# `click` is a transitive of spaCy that some pip resolvers skip in mixed
# installs — install it explicitly so `python -m spacy download …` below
# doesn't crash with ModuleNotFoundError on a fresh venv.
python -m spacy download en_core_web_lg
python -m spacy download zh_core_web_lg
不做 wrapper 的理由:四个包都是 Microsoft 官方维护、MIT、PyPI 稳定发布;组合用法是 Presidio docs 第一章示例代码。课程价值 = 「agent-ready SOP + zh-CN recognizer YAML + 统一报告格式」,而不是再包一层 npm/pip。
redact.py是参考编排脚本(≈ 180 行),随技能一起发布在public/skills/redact-pii/redact.py,agent 把它当模板复用 / 改写即可。
工作流程(agent 执行步骤)
1. 注册 zh-CN custom recognizer
Presidio 默认对中文 PII 识别率一般:zh_core_web_lg 的 NER 会把人名和地名 / 机构名混在一起,且没有大陆手机号 / 身份证 / 统一社会信用代码的内置 recognizer。本课提供一份可复制的 zh_cn_recognizers.yaml(位于 public/skills/redact-pii/zh_cn_recognizers.yaml):
# zh_cn_recognizers.yaml — paste into SOP verbatim
supported_languages: [zh, en]
recognizers:
- name: CN_MOBILE_PHONE
supported_language: zh
supported_entity: CN_MOBILE_PHONE
patterns:
- name: cn_mobile
regex: "(?<![0-9])1[3-9]\\d{9}(?![0-9])"
score: 0.95 # > 0.85 so we win overlap ties against built-in DATE_TIME
- name: CN_ID_CARD
supported_language: zh
supported_entity: CN_ID_CARD
patterns:
- name: cn_id_18
regex: "(?<![0-9])[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx](?![0-9Xx])"
score: 0.95
context: [身份证, 身分证]
- name: CN_USCC
supported_language: zh
supported_entity: CN_USCC
patterns:
- name: cn_uscc
regex: "(?<![A-Z0-9])[0-9A-HJ-NPQRTUWXY]{2}\\d{6}(?=[0-9A-HJ-NPQRTUWXY]*[A-HJ-NPQRTUWXY])[0-9A-HJ-NPQRTUWXY]{10}(?![A-Z0-9])"
score: 0.9
- name: CN_PERSON_NAME
supported_language: zh
supported_entity: PERSON
patterns:
- name: cn_name_context
regex: "[\\u4e00-\\u9fa5]{2,4}"
score: 0.35
context: [姓名, 客户, 联系人, 收件人, 申请人, 先生, 女士]
为什么这套设计:
CN_PERSON_NAME基础 score = 0.35(低于默认 0.5 阈值),单凭 regex 不会自动替换;命中后由 spaCy NER 或 context 关键词把分数抬上去,才进 anonymizer 这一步。这样既覆盖「客户:张三」「收件人 李雷」也避免把「今天天气真好」误标。CN_USCCregex 显式要求 9 位组织机构代码段至少含一个字母,避免和纯数字 18 位身份证撞车(身份证由CN_ID_CARD以更高 score 接住)。score_threshold=0.5是默认阈值;低于阈值的命中仍然写进report.json并在diff.html用红色标low-confidence, review needed,但不自动替换 —— 避免误脱敏。
注册方式两选一(参考脚本走 Python;YAML 路径见 Presidio 官方文档 AnalyzerEngineProvider):
from presidio_analyzer import AnalyzerEngine, RecognizerRegistry, PatternRecognizer, Pattern
from presidio_analyzer.nlp_engine import NlpEngineProvider
config = {"nlp_engine_name": "spacy", "models": [
{"lang_code": "en", "model_name": "en_core_web_lg"},
{"lang_code": "zh", "model_name": "zh_core_web_lg"},
]}
nlp_engine = NlpEngineProvider(nlp_configuration=config).create_engine()
registry = RecognizerRegistry(supported_languages=["en", "zh"])
registry.load_predefined_recognizers(languages=["en", "zh"])
# add the four zh-CN recognizers from zh_cn_recognizers.yaml (see redact.py for the Python form)
analyzer = AnalyzerEngine(nlp_engine=nlp_engine, registry=registry,
supported_languages=["en", "zh"])
2. 用四入口分发文件
把输入文件按后缀路由:
| 扩展名 | 引擎 | 备注 |
|---|---|---|
.txt / .md / .json |
presidio-analyzer + presidio-anonymizer |
自动检测 CJK → 选 zh,否则 en |
.csv / .tsv |
presidio-structured (PandasAnalysisBuilder) |
列级类型推断 + 同字面值跨行同 id |
.pdf (文本层) |
analyzer + anonymizer + pypdf 重写 content stream |
保留原排版、字体、坐标 |
.pdf (扫描件) |
先 pypdf.PdfReader 判断有无文本层;无 → 按页 rasterize 后走 image-redactor |
复杂多栏 / 含公式 PDF 改用 parse-docs |
.png / .jpg / .jpeg / .tif |
presidio-image-redactor |
tesseract OCR + 黑框覆盖 |
3. 统一占位符策略
用 OperatorConfig("replace", {"new_value": f"<{entity_type}_{stable_id:03d}>"}),相同字面值(不论出现在哪个文件、哪一列)复用同一 stable_id:
import hashlib
ids = {} # {(entity_type, sha16(value)): "<ENT_NNN>"}
counters = {} # {entity_type: int}
def allocate(entity_type: str, value: str) -> str:
h = hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]
key = (entity_type, h)
if key in ids:
return ids[key]
counters[entity_type] = counters.get(entity_type, 0) + 1
ids[key] = f"<{entity_type}_{counters[entity_type]:03d}>"
return ids[key]
这样下游分析(外包团队 / 研究伙伴)即便看不到原 PII,也能用 replacement_id 做关联分析(同一客户的多笔工单依然能聚合)。
4. 一键跑参考脚本
python redact.py --input ./customer-export \
--output ./redacted \
--report ./redacted/report.json
redact.py在public/skills/redact-pii/redact.py,本地把它拷到工作目录或直接python /path/to/skill/redact.py。
5. 自检(务必跑)
report.json.summary.total_findings > 0,否则说明 recognizer 没装上(最常见原因:忘了把CN_*recognizer 注册进 registry,或没下载zh_core_web_lg)。- 抽样 5 条
low_confidence: true的命中人工二次确认 —— 这些是 score < 0.5 没被自动替换的,是真 PII 还是误报? - 用一份只含中文姓名 + 大陆手机号 + 大陆身份证的小
.txt跑一次 sanity check —— 必须命中至少 3 类不同的 entity_type,否则 zh-CN recognizer 没生效。
产出物(用户拿到的就是这些)
redacted/
├── <same-name>.<same-ext> # 每个原文件一份脱敏副本(同名同后缀)
├── diff.html # 自包含单文件,左右对照 + entity 计数 chip
└── report.json # auditable findings(含 score / offset / replacement_id)
report.json schema(节选):
{
"summary": {
"total_files": 4,
"total_findings": 677,
"by_entity": {"PERSON": 156, "EMAIL_ADDRESS": 78, "CN_MOBILE_PHONE": 15, "...": "..."},
"low_confidence_review_needed": 15,
"score_threshold": 0.5
},
"findings": [
{"file": "support-chat.txt", "entity_type": "CN_MOBILE_PHONE",
"score": 0.85, "start": 142, "end": 153,
"original_hash": "sha256:…", "replacement_id": "<CN_MOBILE_PHONE_001>",
"low_confidence": false}
]
}
diff.html 自包含(inline CSS + 内嵌 base64 image,无 CDN)、左右对照、顶部 entity 类型计数 chips、底部 attribution。浏览器双击可直接打开。
铁律 / 反模式
- 不要写入用户系统 Python:始终在 venv(或 pipx)里安装。
- 不要默认开 LLM recognizer。Presidio 支持外挂语义抽取做更细的实体识别,但那是 BYO key、增加成本、增加风险面;本课程的承诺是「零 LLM、零远端 API」。如果用户显式要开,让他在 SOP 之外另起一份脚本(具体怎么挂见本文末尾 Optional 小节),且要让他完全清楚 key 走哪儿、数据被发到哪儿。
- 不要假设默认 recognizer 已经够好。中文环境下若没注册 zh-CN custom recognizer,大陆手机号 / 身份证 / 统一社会信用代码会零命中 —— 演示翻车第一名。
- 不要伪造内容:脱敏不是改写。如果某份文件转出来零命中,把那份文件名列给用户让他自己看,不要替它编 PII。
- 不要把
redact.py当 wrapper 二次打包再发布到 npm / PyPI。它是参考脚本,用户随手改 ≤ 50 行就能改成自己业务需要的形态。
Optional · LLM recognizer 增强(默认 off)
Presidio 0.7+ 官方支持挂 Azure OpenAI / Anthropic LangExtract 做更细的实体抽取(适合「医生病历摘要里识别出'家庭住址'类高语义实体」这种 regex 抓不到的场景)。如果用户明确知道自己在做什么、能承担 BYO key 成本、且数据可以离开本地,可以这样起一份独立脚本:
# Optional — only if the user explicitly opts in. NOT part of the default SOP.
pip install presidio-analyzer[llm]
# Configure provider via env vars per Presidio docs; do not ship secrets in the SOP.
注意几点(这一节是写给用户读的,不是写给 agent 自动开的):
- 这条路径会把数据发去第三方 LLM provider,与本课「零 LLM、零远端」的默认承诺不一致。
- BYO key 路径由用户全权管理;课程不提供远端中转、不提供 Clawvard SDK 兜底、不替用户管 key。
- 如果你的数据出于合规原因不能离开本地,不要启用这条路径。
学习完成后
告诉用户:
我已经学会了 redact-pii。给我一个混合 CSV / TXT / PDF / 图片的文件夹,我在本地用 Microsoft Presidio 一次脱敏成同名同结构的
redacted/,附左右对照diff.html和审计明细report.json,每条命中含 entity 类型、score、原始位置、稳定占位符 id。全程本地、零 LLM、零外部 key、不联网。
课程主页与更多示例:clawvard.school