敏感信息脱敏检查引擎·专业版
输入 / 输出
输入:任意中/英文文本。
输出 report 字典:has_sensitive(是否含敏感信息)、counts(mobile/id_card/bank_card/email 四类数量)、total(总数)、hits(命中类型与掩码样例)、masked_text(脱敏后文本)、compliance(PASS / NEED_MASK)。
使用示例
from data_mask import scan_text, mask_text
r = scan_text("联系我 13812345678,邮箱 abc.test@example.com")
print(r["counts"]) # {'mobile': 1, 'id_card': 0, 'bank_card': 0, 'email': 1}
print(r["masked_text"]) # 联系我 138****5678,邮箱 a***@example.com
print(r["compliance"]) # NEED_MASK
print(mask_text("无敏感信息的产品介绍")) # 原样返回
引擎代码:data_mask.py
# -*- coding: utf-8 -*-
"""数据脱敏检查器:手机号/身份证/邮箱/银行卡 识别、计数、掩码与合规判定。
确定性正则,可断言。扫描顺序:身份证 → 手机号 → 银行卡 → 邮箱(数字型按长度优先级),
避免 18 位身份证被银行卡误判。
"""
import re
# 身份证:18 位(前 17 位数字 + 末位数字/X),先于银行卡
RE_ID_CARD = re.compile(r"(?<!\d)(\d{6})(\d{8})(\d{3})([\dXx])(?!\d)")
# 手机号:1[3-9] + 9 位
RE_MOBILE = re.compile(r"(?<!\d)(1[3-9]\d{9})(?!\d)")
# 银行卡:16~19 位纯数字
RE_BANK = re.compile(r"(?<!\d)(\d{16,19})(?!\d)")
# 邮箱
RE_EMAIL = re.compile(r"([A-Za-z0-9._%+-]+)(@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+)")
def mask_mobile(m):
s = m.group(1)
return s[:3] + "****" + s[-4:]
def mask_idcard(m):
return m.group(1) + "********" + m.group(4)
def mask_bank(m):
s = m.group(1)
return s[:4] + "*" * (len(s) - 8) + s[-4:]
def mask_email(m):
user, domain = m.group(1), m.group(2)
return user[0] + "***" + domain
def scan_text(text):
"""扫描文本,返回各类命中数量、明细、脱敏文本与合规状态。"""
text = text or ""
spans = [] # (start, end, kind, masked)
# 1) 身份证
for m in RE_ID_CARD.finditer(text):
spans.append((m.start(), m.end(), "id_card", mask_idcard(m)))
occupied = [(s, e) for s, e, _, _ in spans]
def free(a, b):
return not any(a < e and b > s for s, e in occupied)
# 2) 手机号(避开身份证占用区)
for m in RE_MOBILE.finditer(text):
if free(m.start(), m.end()):
spans.append((m.start(), m.end(), "mobile", mask_mobile(m)))
occupied = [(s, e) for s, e, _, _ in spans]
# 3) 银行卡(避开身份证;16~19 位)
for m in RE_BANK.finditer(text):
if free(m.start(), m.end()):
spans.append((m.start(), m.end(), "bank_card", mask_bank(m)))
occupied = [(s, e) for s, e, _, _ in spans]
# 4) 邮箱
for m in RE_EMAIL.finditer(text):
spans.append((m.start(), m.end(), "email", mask_email(m)))
spans.sort(key=lambda x: x[0])
# 从后往前替换,避免偏移
masked = text
for s, e, _, rep in sorted(spans, key=lambda x: -x[0]):
masked = masked[:s] + rep + masked[e:]
counts = {"mobile": 0, "id_card": 0, "bank_card": 0, "email": 0}
for _, _, kind, _ in spans:
counts[kind] += 1
hits = [{"kind": k, "masked": rep} for _, _, k, rep in spans]
return {
"has_sensitive": len(spans) > 0,
"counts": counts,
"total": len(spans),
"hits": hits,
"masked_text": masked,
"compliance": "PASS" if len(spans) == 0 else "NEED_MASK",
}
def mask_text(text):
"""直接返回脱敏后的文本。"""
return scan_text(text)["masked_text"]
if __name__ == "__main__":
import json
demo = "联系我 13812345678,身份证 11010119900307881X,邮箱 abc.test@example.com," \
"卡号 6222021234567890123。"
print(json.dumps(scan_text(demo), ensure_ascii=False, indent=2))
掩码口径(标准)
| 类型 | 原文 | 脱敏后 |
|---|---|---|
| 手机号 | 13812345678 | 138****5678 |
| 身份证 | 11010119900307881X | 110101**X |
| 银行卡 | 6222021234567890123 | 6222*****0123 |
| 邮箱 | abc.test@example.com | a***@example.com |
边界处理(已覆盖)
- 18 位身份证(含末位 X)优先识别,不被误判为银行卡;
- 19 位借记卡、16 位信用卡均识别为银行卡;
- 15 位数字、20 位及以上连续数字不识别为银行卡(避免误掩订单号/流水号);
- 无敏感信息时返回 compliance=PASS、原文不变;
- 空文本不报错;
- 手机号不会被身份证、银行卡规则误判。
断言测试结果
已通过 6 正例(手机号、身份证含 X、邮箱、19 位银行卡、混合文本、16 位银行卡)+ 5 边界(15 位数字、20 位数字、无敏感信息、空文本、纯 18 位数字身份证),全部通过。
批量接入建议
- 对客户资料、聊天记录、日志、训练语料按批扫描,命中 NEED_MASK 自动掩码后再入库;
- 可嵌入导出脚本、对外文档发布流程、客服工单系统;
- 按 counts 统计敏感信息密度,定位高发泄露环节;
- 脱敏仅为去标识化,对需完全匿名的数据建议直接删除相关字段。