个人数据检测系统:正则召回、校验位与置信度打分实现敏感数据识别
简介KaiGe个人数据检测系统是一套基于Flask框架的个人数据泄露检测Web应用面向有网络安全自查需求的技术人员、运维开发者和数据隐私关注者。系统支持Q绑、手机号、邮箱等多种查询类型兼顾本地测试与服务器生产双模式部署并内置一键启动、便携版Python支持及安全提醒机制适合快速搭建私人化的数据泄露排查工具。资源包共2000个文件压缩包约24.67MB以py源码、pyc编译文件为主辅以exe可执行程序、dll动态库、配置文件、文档及静态资源目录结构完整既能直接运行也可二次开发。目前已有130人学习下载。通过该资源可获取完整项目源码、依赖清单与启动脚本同时理解Flask应用部署、数据检测业务逻辑、响应式界面设计和自动化环境配置等实践要点对学习Web安全检测工具开发具有参考价值。1. KaiGe个人数据检测系统解决的问题先搞清楚数据里藏了什么一家公司手里最值钱也最烫手的资产就是用户数据可多数团队对自己库里存了多少种个人数据、分布在哪张表哪个字段根本说不清楚。服务器、备份、日志、Excel 导出文件里都可能有手机号和身份证号。KaiGe 个人数据检测系统解决的就是这件事扫描数据源把个人敏感数据按类型识别出来给出字段级标注和置信度报告供合规审计、数据治理和数据迁移前的盘点使用。典型场景是系统迁移前梳理敏感字段、新项目上线前做泄漏自查、日常数据资产盘点。适合后端开发、数据工程师和安全运维读完能自己跑通一次检测也知道源码下载后怎么改成自己团队的工具。2. 个人数据检测系统的核心原理正则召回、校验位与置信度打分2.1 单条正则在真实数据上扛不住误报和漏报从哪来很多人第一反应是写正则手机号1[3-9]\d{9}身份证\d{17}[\dXx]。这在演示环境里没问题丢到真实数据库上就是灾难原因有两层。第一层是误报。18 位数字串不一定是身份证可能是订单号、雪花 ID、时间戳截断以 1 开头的 11 位数字也不一定是手机号可能是连续数字里恰好掐了一段。第二层是漏报。生产数据里身份证常被脱敏成110101********1234手机号可能被写成138 0013 8000或带横杠一条固定正则根本覆盖不了这些变体。所以成熟的检测系统不会用命中就算的单层策略而是拆成三层格式层用正则粗筛把候选串全部召回校验层用算法验证身份证校验码、银行卡 Luhn 都在这一层语境层看字段名和数据上下文给结论加权。这跟垃圾邮件过滤是同一个思路先保召回再逐步提高精确率。2.2 手机号、身份证、银行卡三类敏感数据校验逻辑的差别以国内最常见的三类数据为例校验强度差异很大写规则时必须区别对待。手机号没有校验位能精确验证的只有号段。1[3-9]覆盖现网大部分号段但号段会新增硬编码在正则会过期常见做法是把号段表外置成配置文件更新时只动数据不动代码。身份证号有强校验位前 17 位乘以权重系数[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]求和后对 11 取模余数对应校验码1 0 X 9 8 7 6 5 4 3 2这是国标算法可以放心写死。银行卡号走 Luhn 模 10 校验它是通用算法、不是银行卡专属所以只能当辅助判断。三类数据的校验强度直接决定它们在规则里的置信度权重身份证通过校验加分最高银行卡次之手机号只能靠号段加一点分。这个差异会落到第 3 章的规则配置里先想清楚再写代码能省掉后面大量的误报排查时间。2.3 置信度打分让检测结论可解释、可过滤检测系统不能只输出发现 1280 条手机号要能回答凭什么说它是手机号。常见做法是给每条命中算一个 0 到 1 的置信度分数由三部分叠加命中格式正则给基础分 0.4通过强校验算法加 0.3通过弱校验加 0.2字段名或上下文命中关键词再加 0.2。总分 0.6 以上判为确认命中0.4 到 0.6 判为疑似交给人工复核。这套打分机制的价值在于让报告可操作。高置信度结果自动进整改清单低置信度单独开一栏运维不用逐条肉眼看。后面要调整规则时也只需要改分数权重不用动检测代码。下一章就按这个逻辑给出一个最小可运行版本。3. 从零实现可运行的 KaiGe 检测工具规则文件加引擎主循环3.1 最小目录结构规则和引擎必须分开kaige_detect/ ├── rules.json # 敏感数据规则定义 ├── detector.py # 检测引擎主程序 ├── scanners/ │ ├── __init__.py │ ├── file_scanner.py # 文件扫描入口 │ └── db_scanner.py # 数据库扫描入口 └── report.py # 报告生成这个结构刻意保持精简只依赖 Python 标准库源码下载后不用装重型框架就能跑。把规则文件和引擎分开是有意为之实际使用中规则调整的频次远高于引擎代码。接新数据源、新数据格式时只动规则不动机器出问题的范围就小得多。3.2 用 JSON 定义检测规则正则、校验器与权重{ rules: [ { id: cn_id_card, name: 身份证号, pattern: \\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b, checksum: id_card, base_score: 0.4, checksum_bonus: 0.3, context_keywords: [身份证, id_card, idno] }, { id: cn_mobile, name: 手机号, pattern: \\b1[3-9]\\d{9}\\b, checksum: mobile_prefix, base_score: 0.4, checksum_bonus: 0.2, context_keywords: [手机, mobile, phone] }, { id: bank_card, name: 银行卡号, pattern: \\b[1-9]\\d{12,18}\\b, checksum: luhn, base_score: 0.3, checksum_bonus: 0.3, context_keywords: [银行卡, bank_card, card_no] } ] }参数说明pattern是格式层正则负责候选串召回checksum指向引擎里的校验函数名base_score是命中格式后的底分checksum_bonus是通过校验后追加的分context_keywords用于字段名和上下文加权。身份证正则里(?:19|20)把出生年份限定在 1900 到 2099能排除明显不合法的数字。JSON 里的\\b经 Python 加载后变成正则的单词边界不加它订单号里嵌的 18 位数字就会被误命中。3.3 检测引擎主循环命中、校验、打分三步import json import re class KaiGeDetector: def __init__(self, rules_pathrules.json): with open(rules_path, encodingutf-8) as f: rules json.load(f)[rules] # 预编译正则避免每次扫描重复解析 self.rules [{rule: r, rx: re.compile(r[pattern])} for r in rules] def _checksum(self, algo, v): if algo id_card: return self._verify_id(v) if algo luhn: return self._verify_luhn(v) if algo mobile_prefix: # 手机号无校验位退化为号段检查 return v[:2] in {13, 14, 15, 16, 17, 18, 19} return False staticmethod def _verify_id(v): weights [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] mapping 10X98765432 if len(v) ! 18: return False try: total sum(int(v[i]) * weights[i] for i in range(17)) except ValueError: return False return mapping[total % 11] v[17].upper() staticmethod def _verify_luhn(v): digits [int(c) for c in v if c.isdigit()] if len(digits) 13: return False for i in range(len(digits) - 2, -1, -2): digits[i] digits[i] * 2 - 9 if digits[i] * 2 9 else digits[i] * 2 return sum(digits) % 10 0 def scan_text(self, text, hint): hits [] for item in self.rules: r item[rule] for m in item[rx].finditer(text): v m.group(0) score r[base_score] if self._checksum(r[checksum], v): score r[checksum_bonus] if any(k.lower() in hint.lower() for k in r[context_keywords]): score 0.2 if score 0.6: hits.append({ rule_id: r[id], value: v, score: round(score, 2), pos: m.start() }) return hits if __name__ __main__: d KaiGeDetector() # 一行样本同时覆盖三类敏感数据 print(d.scan_text(张三 110101199003071234 13800138000 6222020200112233445))逻辑说明scan_text按正则召回 → 校验加分 → 语境加权 → 阈值过滤的顺序执行。_checksum是分发器根据规则里的checksum字段路由到具体校验函数新增校验类型只需要在这里加分支。输出每条命中都带规则 ID、原文、得分和位置可以直接对接 CSV 或报告模块。参数说明hint传字段名比如扫描mobile列就传mobile字段名参与语境加权后命中分数更合理。0.6是全局阈值想收紧改成 0.8。生产化有两个便宜好用的优化校验结果按值缓存成字典同一批数据里相同号码会反复出现用re.findall配合手动索引替代finditer减少 match 对象开销。4. 在真实数据上跑检测参数调整、SQL 采样与误报排查4.1 采样率、置信度阈值与扫描深度一组可落地的默认值亿级表全量扫描在 Python 里不可接受必须采样。下面是核心参数和我常用的经验值。参数默认值适用场景调整说明sample_rate0.011%大表普查先采样定位疑似表再对疑似表全量补扫threshold0.6常规扫描调到 0.8 只留强证据用于整改清单max_hits_per_col100防单列刷屏单列命中达到上限即停省 IO 和内存scan_depth5嵌套 JSON 字段嵌套超过 5 层的字段直接跳过提示抽样别用ORDER BY RAND()大表上会触发全表排序慢一个量级。MySQL 里用MOD(id, 100) 0做哈希抽样或者按主键范围等间隔抽效果接近随机且开销可忽略。前提是表里有主键没有的话退回去用LIMIT加偏移量分段抽。4.2 对接 MySQL两段 SQL 加一段扫描循环数据库扫描的标准做法是两段式先查元数据拿到所有字符型字段再对可疑字段抽样本喂给检测引擎。SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA user_db AND DATA_TYPE IN (varchar, char, text, json) ORDER BY TABLE_NAME, ORDINAL_POSITION;SELECT id, user_name, id_card, mobile, address FROM user_profile WHERE MOD(id, 100) 0 LIMIT 5000;逻辑说明第一段 SQL 把扫描范围缩小到字符型字段int、bigint理论上也能存手机号但实践中极少先排除能省掉大量无效 IO。第二段 SQL 用MOD(id, 100) 0抽 1% 样本LIMIT 5000控制单次传输行数。拿到的行数据在 Python 端逐列跑scan_text上下文提示直接传字段名让字段名参与置信度加权。def scan_table(conn, table, columns, detector, limit5000): col_sql , .join(columns) sql fSELECT {col_sql} FROM {table} WHERE MOD(id, 100) 0 LIMIT {limit} cur conn.cursor() cur.execute(sql) for row in cur.fetchall(): for col, val in zip(columns, row): if val and detector.scan_text(str(val), hintcol): yield table, col, val参数说明conn是数据库连接对象columns来自第一段 SQL 的查询结果detector是第 3 章的KaiGeDetector实例。用生成器逐行产出命中避免把几千行样本一次性堆进内存。MOD(id, 100)的模数由sample_rate换算1% 就模 1005% 就模 20别写死。4.3 误报排查先按规则分桶再按置信度分桶跑完一轮最常遇到的问题是结果里混进长得像但根本不是的数据。排查别靠肉眼看按顺序来。先按规则 ID 分组统计。如果cn_mobile的命中量是cn_id_card的几十倍要么数据脏要么正则可疑。随机抽 20 条命中值看全是13000000000这种测试号是数据问题全是12345678901这种递增串是正则边界问题。再看置信度分布。把命中按分数分桶如果 0.6 到 0.7 的桶占了八成以上说明大量命中是靠语境加分勉强过线应该收紧规则而不是继续下调阈值。最常见的元凶是关键词子串误匹配比如tel命中hotel。解决办法是把关键词比对从in换成单词边界正则匹配\b(tel|phone)\b字段名匹配同理。这一步改动通常能把误报率压下一个量级。5. 源码下载与二次开发git 拉取、加规则、验基准5.1 git 源码下载的标准流程和初始化检查获取 KaiGe 检测系统源码用 git 克隆git clone加仓库地址进目录后python -m venv venv建虚拟环境pip install -r requirements.txt装依赖python -m pytest tests/ -q跑测试。要稳定版就先git tag看版本列表git checkout v1.0.0切换。虚拟环境这步别省依赖版本不锁就会出现本地跑通、服务器跑挂的情况。源码要是连tests/目录都没有说明规则没经过验证使用前务必自己构造样本集验一遍。别人的规则文件也别直接抄袭就上字段名和数据分布不同规则必然水土不服。5.2 扩展新数据类型统一社会信用代码的两步改动二次开发最典型的需求是加新格式。以统一社会信用代码为例它 18 位字符集排除I O S V Z五个易混淆字母校验按 GB 32100-2015 加权取模。在现有框架里只需要两步rules.json加一条规则detector.py加一个校验分支。{ id: uscc, name: 统一社会信用代码, pattern: \\b[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}\\b, checksum: uscc, base_score: 0.4, checksum_bonus: 0.3, context_keywords: [信用代码, uscc, credit_code] }逻辑说明checksum: uscc指向引擎里新增的校验分支权重表是[1,3,9,27,19,26,16,17,20,29,25,13,8,24,10,30,28]前 17 位加权求和后对 31 取模映射规则与身份证校验同思路。整个改造只动了一个 JSON 配置和一个函数扫描器、报告、阈值逻辑一行不用改这就是规则与引擎分离的收益。改完别急着上线先跑基准测试集确认召回率和精确率没回退。基准集固定放三类样本正样本脱敏后的真实数据、负样本订单号、流水号、随机字符串、边界样本分段号码、脱敏号码每次改规则都拿它回归。有了这套基准调阈值、改正则时每次都能明确知道是变好还是变差而不是靠看起来差不多的直觉判断。本文还有配套的精品资源点击获取