AI 数据合规检查:GDPR 和个保法的字段级自动扫描方案
数据合规这件事,很多团队的处理方式是"法务出一份清单,工程师手动打标签"。问题是:数据仓库有 200 张表、5000 个字段,手动标注的覆盖率可能只有 30%,而且标注后字段改名了、新增了,标签又不跟上了。等到审计时才发现漏标了 50 个敏感字段,罚款可能比整改成本高 10 倍。
AI 数据合规检查的思路是:用 NLP 模型自动识别字段含义,判断是否符合 GDPR 和个保法的合规要求。它不是替代法务判断,而是替代"工程师手动扫描 5000 个字段"这个劳力密集环节。
一、合规扫描的三个层次
数据合规检查不是"有没有身份证号"这么简单。法规要求覆盖三个层次:
层次1:直接标识字段
姓名、身份证号、手机号、邮箱——这些字段一眼就能看出是个人信息。但数据仓库里的命名可能不是这么直白:cust_name、id_card_no、phone_num、email_addr。缩写、英文、业务自定义命名让直接标识也变得不那么"直接"。
层次2:间接标识字段( quasi-identifier)
年龄、性别、城市、职业——单独看都不是个人信息,但组合起来可以唯一识别一个人。GDPR 把这种"组合可识别"也定义为个人信息。比如"35岁+女性+北京+律师"可能只有一个人符合,这四个字段合在一起就是个人信息。
层次3:推断敏感字段
消费偏好、健康数据、政治倾向——这些字段本身不包含标识信息,但法规认为它们属于"敏感个人信息"(个保法第28条)。你的数据仓库里可能有 purchase_category、health_checkup_result、political_survey_answer 这类字段,它们需要更严格的处理(比如必须明示同意才能收集)。
graph TB
A[数据仓库5000字段] –> B[层次1扫描<br/>直接标识字段]
A –> C[层次2扫描<br/>间接标识组合]
A –> D[层次3扫描<br/>推断敏感字段]
B –> E[GDPR Art.4<br/>个人信息]
C –> E
D –> F[个保法§28<br/>敏感个人信息]
E –> G[合规建议:<br/>脱敏/加密/最小化]
F –> H[合规建议:<br/>明示同意+单独存储]
style B fill:#e1f5fe
style C fill:#fff3e0
style D fill:#fce4ec
三个层次的扫描逻辑不同:层次1靠字段名和样例数据匹配,层次2靠字段组合的 k-匿名性评估,层次3靠语义推断。AI 在这三个层次都能发挥作用,但发挥的方式不一样。
二、层次1:字段名+样例数据的自动识别
字段名识别是合规扫描的基础。我们用 NLP 模型做字段名的语义分类:
from sentence_transformers import SentenceTransformer
import numpy as np
# 加载中文语义模型——为什么用 text2vec 而不是规则匹配?
# 因为规则匹配只能覆盖已知命名(比如 id_card_no)
# 但数据仓库里可能有 50 种不同的身份证号命名方式
# 语义模型能理解"id_number"、"身份证号码"、"cert_id"都是同一个含义
# 规则列表永远追不上业务的命名创意
model = SentenceTransformer("shibing624/text2vec-large-chinese")
# 定义合规类别的参考向量——每个类别用多个典型字段名编码
# 为什么用多个而不是单个?
# 因为单个参考词的向量可能不够代表性
# 多个参考词的均值向量更稳定,覆盖更多表达方式
PII_categories = {
"直接标识-姓名": ["姓名", "顾客名称", "客户名", "customer_name", "cust_name", "user_name"],
"直接标识-身份证": ["身份证号", "证件号", "id_card", "cert_no", "identity_number", "id_number"],
"直接标识-手机号": ["手机号", "联系电话", "phone", "mobile", "phone_number", "contact_number"],
"直接标识-邮箱": ["电子邮箱", "email", "email_address", "mail", "email_addr"],
"间接标识-年龄": ["年龄", "age", "birth_year", "年龄段", "age_group"],
"间接标识-地址": ["住址", "地址", "address", "home_address", "居住地", "residence"],
"敏感-健康": ["体检结果", "health_status", "疾病记录", "medical_record", "健康数据"],
"敏感-偏好": ["消费偏好", "purchase_category", "购物习惯", "brand_preference"],
"非敏感-业务": ["订单号", "order_id", "交易金额", "amount", "注册日期", "signup_date"],
}
# 计算每个类别的参考向量(类别内所有参考词的均值)
category_vectors = {}
for cat, refs in PII_categories.items():
vecs = model.encode(refs)
category_vectors[cat] = np.mean(vecs, axis=0) # 均值向量更稳定
def classify_field(field_name, field_sample=None, threshold=0.75):
"""分类单个字段——threshold=0.75 是什么意思?
0.75 表示语义相似度≥75%才归入该类别
为什么不设更高?因为字段名可能用缩写或英文,和参考词的相似度会被稀释
比如 "cust_name" 和 "客户名" 的相似度可能在 0.78 左右
0.85 的阈值会漏掉这类缩写命名
为什么不设更低?因为 0.7 以下会误分类
比如 "amount" 和 "address" 相似度可能在 0.65,但含义完全不同
"""
field_vec = model.encode([field_name])[0]
# 计算与每个类别的相似度
scores = {}
for cat, cat_vec in category_vectors.items():
# 内积(向量已归一化后等同于余弦相似度)
sim = np.dot(field_vec, cat_vec) / (
np.linalg.norm(field_vec) * np.linalg.norm(cat_vec)
)
scores[cat] = sim
# 取最高相似度的类别
best_cat = max(scores, key=scores.get)
best_score = scores[best_cat]
if best_score >= threshold:
return best_cat, best_score
# 如果字段名匹配不到,检查样例数据
# 为什么样例数据是补充而不是主要判断依据?
# 因为样例数据可能被脱敏过(比如身份证号显示为 110***********1234)
# 脱敏后的样例无法用于识别字段含义
# 但未被脱敏的样例数据是很好的辅助证据
if field_sample:
sample_vec = model.encode([f"{field_name}: {field_sample}"])[0]
sample_scores = {}
for cat, cat_vec in category_vectors.items():
sim = np.dot(sample_vec, cat_vec) / (
np.linalg.norm(sample_vec) * np.linalg.norm(cat_vec)
)
sample_scores[cat] = sim
sample_best = max(sample_scores, key=sample_scores.get)
sample_best_score = sample_scores[sample_best]
if sample_best_score >= threshold:
return sample_best, sample_best_score
return "未分类", best_score
三、层次2:间接标识组合的 k-匿名性评估
间接标识字段单独看可能没问题,但组合起来可能能唯一识别一个人。k-匿名性评估就是检查"任意 k 个间接标识字段的组合,是否能让至少 k 个人匹配"——如果只有 1 个人匹配,这个组合就泄露了个人信息。
import pandas as pd
def evaluate_k_anonymity(df, quasi_id_columns, k=5):
"""评估 k-匿名性——k=5 是什么意思?
k=5 表示任何间接标识组合至少对应 5 个人
为什么 k=5 而不是 k=3?
因为 GDPR 推荐的最低标准是 k=5
k=3 在小样本下可能不够安全(3个人里可能只有1个女性,组合仍然可识别)
k=5 提供更大的安全缓冲
"""
# 按间接标识列组合分组,计算每组的人数
# 为什么用 groupby + size 而不是 value_counts?
# 因为 groupby 可以处理多列组合,value_counts 只能处理单列
group_sizes = df.groupby(quasi_id_columns).size()
# 统计有多少组合的 k 值低于阈值
# 低于阈值意味着这个组合能唯一识别少于 k 个人
unsafe_combos = group_sizes[group_sizes < k]
# 计算不安全比例——为什么关注比例而不是绝对数?
# 因为数据量不同时绝对数没有可比性
# 比如 100 万条数据里有 500 个不安全组合(0.05%)
# 和 1 万条数据里有 500 个不安全组合(5%),风险级别完全不同
unsafe_ratio = len(unsafe_combos) / len(group_sizes)
return {
"quasi_ids": quasi_id_columns,
"total_combos": len(group_sizes),
"unsafe_combos": len(unsafe_combos),
"unsafe_ratio": unsafe_ratio,
"min_k": group_sizes.min(), # 最小的 k 值——有人只靠这些字段就能被唯一识别
"mean_k": group_sizes.mean(), # 平均 k 值——整体匿名性水平
}
# 实际使用:扫描所有可能的间接标识组合
# 为什么不只检查"全部间接标识列的组合"?
# 因为攻击者可能只掌握部分信息(比如只知道年龄和城市)
# 需要检查每个子组合的 k 值,不只是完整组合
quasi_ids = ["age_group", "gender", "city", "occupation"]
# 检查所有 2 列组合——为什么先检查 2 列?
# 因为 2 列组合是最常见的攻击场景(攻击者通常只知道 2-3 个属性)
# 如果 2 列组合就低于 k=5,说明数据匿名性严重不足
from itertools import combinations
results = []
for r in range(2, len(quasi_ids) + 1):
for combo in combinations(quasi_ids, r):
result = evaluate_k_anonymity(df, list(combo), k=5)
results.append(result)
# 输出风险最高的组合——unsafe_ratio 最高意味着最容易泄露个人信息
worst = max(results, key=lambda x: x["unsafe_ratio"])
print(f"风险最高的组合: {worst['quasi_ids']}, 不安全比例: {worst['unsafe_ratio']:.2%}")
如果发现 age_group + city 的组合 k 值只有 2(每对年龄-城市组合平均只有 2 个人),合规建议是:对这两个字段做泛化处理(年龄从具体值泛化到年龄段,城市泛化到省份)。
四、层次3:推断敏感字段的语义推断
有些字段名不含敏感关键词,但数据内容涉及敏感信息。比如 tag_list 字段存的是"运动达人、母婴关注、保健品购买"——这些标签组合可能推断出健康状况和育儿状态。
def infer_sensitive_from_content(df, field_name, sensitive_keywords):
"""从字段内容推断敏感类别——为什么用关键词而不是模型?
因为敏感推断需要精确匹配法规定义的敏感类别
模型推断可能有幻觉(把普通购物标签误判为健康敏感)
关键词匹配更可控,误判率更低
生产环境可以先用关键词做初筛,再用模型做深度判断
"""
# 统计字段内容中包含敏感关键词的比例
# 为什么关注比例而不是存在与否?
# 因为一个字段可能有 90% 的普通数据和 10% 的敏感数据
# 如果只判断"是否存在",所有多值字段都会被标为敏感
# 比例判断更精确:只有超过阈值(比如 5%)才标为敏感
content = df[field_name].astype(str)
hit_count = 0
for kw in sensitive_keywords:
hit_count += content.str.contains(kw, case=False).sum()
hit_ratio = hit_count / len(df)
# 阈值 5%——为什么这么低?
# 因为个保法对敏感数据的定义是"一旦泄露就可能造成歧视或侵害"
# 5% 的人受影响已经构成风险,不能忽视
if hit_ratio > 0.05:
return True, hit_ratio
return False, hit_ratio
# 个保法定义的敏感数据类别关键词
sensitive_keywords = {
"健康": ["疾病", "体检", "处方", "手术", "慢性病", "药品", "健康数据"],
"生物识别": ["指纹", "人脸", "虹膜", "声纹", "基因"],
"金融": ["收入", "负债", "征信", "贷款", "信用评分"],
"未成年": ["儿童", "学生", "未成年", "中小学"],
"政治": ["选举", "投票", "政治倾向", "党派"],
}
# 扫描所有文本类字段——为什么只扫文本类?
# 因为数字类字段(amount、count)通常不含语义信息
# 敏感推断主要适用于标签、描述、备注等文本内容
text_fields = [col for col in df.columns if df[col].dtype == "object"]
for field in text_fields:
for category, keywords in sensitive_keywords.items():
is_sensitive, ratio = infer_sensitive_from_content(df, field, keywords)
if is_sensitive:
print(f"⚠ {field} 可能包含{category}敏感数据,命中比例: {ratio:.1%}")
五、合规建议的自动生成
扫描完成后,AI 可以根据分类结果自动生成合规建议:
def generate_compliance_report(scan_results):
"""自动生成合规建议报告——为什么用规则模板而不是 LLM?
因为合规建议必须精确匹配法规条文,不能有歧义
LLM 生成的建议可能含糊(比如"建议适当处理")
规则模板直接映射到法规条文,每条建议都有法律依据
"""
report = []
for result in scan_results:
field = result["field"]
category = result["category"]
severity = result["severity"]
if category.startswith("直接标识"):
# GDPR Art.4 + 个保法 §4:个人信息必须最小化收集
report.append(f"- {field}: 直接标识字段,建议脱敏处理(哈希/掩码),仅保留必要字段")
if severity == "高":
report.append(f" → 法规依据: GDPR Art.5(1)(c) 数据最小化原则")
elif category.startswith("间接标识"):
# k-匿名性不足的间接标识字段需要泛化
report.append(f"- {field}: 间接标识字段,建议泛化处理(如城市→省份、年龄→年龄段)")
report.append(f" → 法规依据: GDPR Art.25 数据保护设计")
elif category.startswith("敏感"):
# 个保法 §28:敏感个人信息需要明示同意+单独存储
report.append(f"- {field}: 敏感个人信息,需要明示同意+加密存储+访问审计")
report.append(f" → 法规依据: 个保法第28条,GDPR Art.9")
return "\\n".join(report)
五、总结
AI 数据合规检查分三个层次:直接标识字段靠语义模型匹配字段名和样例数据,间接标识组合靠 k-匿名性评估检测信息泄露风险,推断敏感字段靠关键词+语义推断识别隐藏的敏感数据。
落地时注意三点:
合规扫描的终极目标是:让 5000 个字段的合规标注从"手工考古"变成"自动巡航",覆盖率从 30% 提升到 95%,漏标风险从"审计时才发现"变成"扫描时就预警"。

