ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

日志脱敏的正则陷阱:连续数字规则漏了一半

日志脱敏的正则陷阱:连续数字规则漏了一半

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

一、为什么这条线容易漏

脱敏,指的是在日志落盘、导出或外发之前,把手机号、身份证号、银行卡号这类字段替换成星号或占位符的动作。绝大多数团队做这件事的方式是一样的:写一条正则,把「看起来像手机号」的数字换掉,收工。

问题不在于大家不会写正则。正则,指的是用一串符号描述「什么样的字符串算命中」的规则文本,会写re.sub的人很多。问题在于这条规则从来没有被证伪过——它上线之后只跑正常样本,或者只在「格式整齐」的样本上跑,漏了也没人知道,因为脱敏漏了不会报错。

1.1 第一版脚本几乎都长这样

下面三行是从主代理的本机实测脚本里原样摘出来的,属于已经跑过的写法,不是示意:

importre# 摘自 work/2026-10-22_脱敏实测.py,这三条是实跑过的R1=re.compile(r"1[3-9]\d{9}")# 只认连续 11 位R2=re.compile(r"1[3-9]\d[- ]?\d{4}[- ]?\d{4}")# 允许半角空格与短横R3=re.compile(r"(?:\+?86)?1[3-9]\d{9}")# 允许 +86 前缀

1[3-9]的意思是「第一位是 1、第二位是 3 到 9」,后面跟九个数字。这条规则在纸面上没错,中国内地手机号的号段确实落在这个范围里。但它描述的是号码本身,不是号码在日志里的样子。

1.2 漏的不是规则,是写法分支

同一批手机号,在真实日志里至少有四种写法。我按这四种写法各构造了 5 条样本,一共 15 条,逐个喂给上面三条规则。

写法(各 5 条)日志里的样子用哪条规则原样先归一化
连续 11 位用户 13812345678 提交订单R15/55/5
带半角分隔手机 159 0000 1111 待确认R10/50/5
带半角分隔联系 135 1111 2222R25/55/5
全角或带前缀手机:13812345678R14/55/5

这张表里有一行特别值得看:「全角或带前缀」这一类,R1 原样就能抓到 4/5。原因是这 5 条里只有 1 条真的用了全角数字,另外 4 条是半角数字配中文标点、括号、+86或0086前缀,R1 在字符串中间照样能找到那 11 位。所以「原样 4/5」这个数字会给人一个虚假的安心感——看起来只差一点点,实际上差的是整整一个写法类别。

而「半角分隔」那一行是 0/5,怎么调前缀、怎么加+86都还是 0/5,因为中间的空格和短横不在规则的胃口里。规则写得多花,和覆盖了哪些写法,是两件不相关的事。

判据一:召回先看写法覆盖,不看正则写得多花。动手写规则之前,先把「这套日志里出现过几种写法」列成清单,四种写法就是四行样本,一行都不能省。

1.3 本文的样本与口径

必须先把口径说清楚:样本是构造样本,非真实业务日志,仅用于对比规则写法。6 类敏感字段各 5 条(手机号连续 11 位、手机号带半角分隔符、手机号全角或带前缀、身份证 18 位、银行卡 16–19 位、邮箱),另有 6 条不该被动的正常字段(订单号、13 位毫秒时间戳、运单号、12 位批次号、13 位商品条码、15 位流水号)。

跑测环境是 Python 3.13.14,仅用标准库re,脚本与日志都在work/目录下,任何人可以复跑。所以下面所有分母都是 5,所有数字都能回到日志里逐行对上。

二、六类字段的写法覆盖实测

先把完整的对照表放出来。每一格是「原样匹配 → 先做全角转半角再匹配」,分母都是 5。

规则手机号·连续 11 位手机号·带分隔符手机号·全角或带前缀身份证·18 位银行卡·16~19 位邮箱
R1 连续 11 位5/5 → 5/50/5 → 0/54/5 → 5/55/5 → 5/50/5 → 0/50/5 → 0/5
R2 允许半角分隔5/5 → 5/55/5 → 5/54/5 → 5/55/5 → 5/50/5 → 0/50/5 → 0/5
R3 带 +86 前缀5/5 → 5/50/5 → 0/54/5 → 5/55/5 → 5/50/5 → 0/50/5 → 0/5
R4 身份证 18 位0/5 → 0/50/5 → 0/50/5 → 0/55/5 → 5/52/5 → 2/50/5 → 0/5
R5 银行卡 16~19 位0/5 → 0/50/5 → 0/50/5 → 0/55/5 → 5/53/5 → 3/50/5 → 0/5
R6 邮箱0/5 → 0/50/5 → 0/50/5 → 0/50/5 → 0/50/5 → 0/55/5 → 5/5

2.1 手机号:两个召回台阶,分别归功于不同的动作

召回,指的是「该抓到的里面实际抓到了多少」。把三类手机号写法合起来算 15 条:

  • R1 朴素规则:原样9/15(60.0%),先归一化10/15(66.7%)。
  • R2 允许半角分隔:原样14/15(93.3%),先归一化15/15,全部命中。

两个台阶要分开看。从 9 涨到 10 是归一化带来的,涨的只有全角那 1 条;从 10 涨到 15 是写法覆盖带来的,靠的是规则里多认了[- ]?这两处分隔符。很多人只做了前者(写了个translate),或者只做了后者(把规则写长),所以永远停在 10/15 或 14/15。

标题里说的「漏了一半」,指的就是 9/15 这个起点:15 条样本,朴素规则原样只认了 9 条,剩下 6 条一条都不会报错、一条都不会告警。

2.2 身份证与银行卡:位数不是判据

这张表里最别扭的两行是 R4 和 R5。

R4 是\d{17}[\dXx],在 5 条身份证样本上5/5。这里有一个必须写死的细节:样本里有一条的末位是小写x,只写大写X会漏掉它,必须写[Xx]。漏掉的结果不是报错,是那一条身份证原样落进了日志。

R5 是\d{16,19},在 5 条银行卡样本上只命中3/5:6222 0212 3456 7890带空格、6217-0000-1111-2222带短横的两条,全部漏掉。同时它把 5 条 18 位身份证全部吃了进来(5/5)——因为 18 位纯数字正好落在 16 到 19 位这个区间里。

反过来的方向也一样:R4(身份证规则)在银行卡那一列命中了 2/5,说明这两条规则在互相吃。「多少位数字」只能筛出一个候选集合,不能判定字段类型。这是本文后面所有锚点讨论的起点。

2.3 邮箱:这一条反而最稳

邮箱规则[\w.+-]+@[\w-]+\.[\w.]+在 5 条样本上5/5,本轮没有参与任何一次误判。

原因不神秘:它中间有一个@。@是一个几乎不会出现在订单号、时间戳、运单号里的字符,等于规则自带了一个锚点。这个对比很值钱——凡是自带稀有分隔符的字段,规则都容易写对;凡是「纯数字」的字段,规则都容易写歪,因为纯数字的世界里没有任何天然边界。

三、误判清单:什么被当成了手机号

误判,指的是不该动的字段被动了。6 条正常字段、6 条规则交叉跑下来,一共产生11 次误判。

正常字段值被哪条规则误判为什么会被吃
13 位毫秒时间戳1759999999999R1 / R2 / R313 位里含一段以 1 开头、第二位是 7 的 11 位
运单号SF1381234567890R1 / R2 / R3字母后面那 13 位里含同样形状的 11 位
15 位流水号159000011112222R1 / R2 / R315 位里含一段 1 开头、第二位是 5 的 11 位
18 位订单号202610221234567890R4 / R518 位纯数字,正好满足「17 位 + 1 位」与「16 到 19 位」
12 位批次号202610220001无位数不够 16,也没有 11 位以 1[3-9] 开头的连续段
13 位商品条码6901234567892无位数不够 16,首位是 6 不是 1

3.1 13 位时间戳是重灾区

1759999999999这条同时被三条手机号规则命中,是这轮实测里最典型的失败。它是 13 位毫秒时间戳——日志里最常见的字段之一,几乎每一行都有。

为什么 11 位规则能吃 13 位?因为正则默认是子串搜索,不是整串匹配。1[3-9]\d{9}只要求「存在这么一段」,不要求「这段前后没有别的数字」。时间戳的第 1 位是 1、第 2 位是 7,刚好满足1[3-9],后面再凑 9 个数字,就命中了。规则本身完全没错,错在它没有说「前后不许再挨着数字」。

3.2 订单号同时命中两条长规则

202610221234567890是 8 位日期加 10 位序列,共 18 位。它同时满足 R4(17 位 + 1 位)和 R5(16 到 19 位),也就是说身份证规则和银行卡规则都会把它打成星号。

这比手机号误判更麻烦:订单号是排障时最主要的检索键。把订单号脱掉,线上出问题时按订单号搜日志会直接搜不到,人只能按时间盲翻。

3.3 误删比漏删更难发现

漏删是「该掩的没掩」,误删是「不该掩的掩了」。这两件事在监控上的可见度完全不对称:

  • 漏删:日志内容看起来正常,没有任何异常信号,只有等审计或等出事才知道。
  • 误删:日志内容变短、字段变成星号,但没有人会为「日志里少了一个订单号」告警。

判据四:误删比漏删更该被盯住。漏删没有任何信号,误删却会悄悄让日志失去排障能力,而后者通常没人报警。所以判断一条规则好不好,不能只看它抓到了多少敏感字段,还要看它在正常字段上留了几个洞。本案的 11 次误判分布在 4 个正常字段上,其中 3 个是排障必用的定位键(时间戳、运单号、订单号)。

四、归一化与锚点:两个必做的动作

4.1 归一化必须在匹配之前

归一化,指的是在比对之前先把不同写法统一成一种形式——这里具体指全角数字转半角数字。

# 摘自 work/2026-10-22_脱敏实测.py,这两行是实跑过的FW=str.maketrans("0123456789","0123456789")text=s.translate(FW)# 先归一化,再拿 text 去匹配

这一步的收益在数字上很小:R1 从 9/15 到 10/15,R2 从 14/15 到 15/15,都只多了 1 条。但它补的是整整一个类别,而且这一类是「规则怎么写都补不上」的。

有个常见的误解值得点出来:有人觉得\d本来就能匹配全角数字,所以不用归一化。按 Python 官方文档re模块的说明,\d在 Unicode(str)模式下确实匹配任何 Unicode 十进制数字,包括全角数字。但问题出在规则的另一半——1[3-9]里的1和3-9都是 ASCII 字面量,138…这一行在第一个字符就断了,根本走不到后面的\d。所以把[3-9]换成\d救不了全角,只有把文本先归一化才行。

顺序也不能反:判据二:归一化必须放在匹配之前。先匹配、后归一化,等于用错误的输入做了一次判断,再补救已经晚了。

4.2 锚点:给长数字规则加前后边界

锚点,指的是在规则两头加上「前面必须是什么、后面必须是什么」的约束。对纯数字字段来说,最有用的一种锚点是否定型数字边界:前后不许紧挨着数字或分隔符。

⚠️代码待验证

# 本文改进写法,未在主代理的实测脚本里跑过PHONE=re.compile(r"(?<![\d\-])"# 前面不能是数字或短横r"(?:(?:\+|00)?86[\- ]?)?"# 可选国家码:+86 / 0086r"1[3-9]\d(?:[\- ]?\d{4}){2}"# 3 位号段 + 两组 4 位r"(?![\d\-])"# 后面不能是数字或短横)

按这个结构,前面那 11 次误判里能被推掉的是三类:13 位时间戳、15 位流水号、13 位运单号里的数字部分。机制是(?![\d\-])——时间戳那 11 位后面还跟着99,边界断言不成立,整条匹配失败,正则不会退而求其次去匹配一个短一点的子串。

但18 位订单号推不掉。它本身就是 18 位纯数字,前后都没有别的数字,任何「前后不挨数字」的断言对它都是成立的。要区分它和身份证,只能看它前面写的是「订单号」还是「身份证」——这就是下一节。

另外提醒一个反向的坑:加了(?<![\d\-])之后,008613812345678这种紧贴国家码的写法会被前面那个0挡住,所以国家码部分要显式允许00前缀,否则会新造一个漏点。这类「修一个漏点、长出另一个漏点」的情况,只有靠用例回归才拦得住。

4.3 位数不够,必须看字段名前缀

判据三:长数字规则必须带上下文锚点。把上面的结论压成一句:位数只能筛候选,字段名才能定类型。

同一个 18 位数字串,出现在「订单号 202610221234567890」里是业务主键,出现在「身份证 110101199003071234」里是敏感个人信息。正则看到的两个串长得一模一样,它没有办法自己判断。能判断的信息在字段名前缀上——也就是那个 18 位数字前面若干个字符里出现的「身份证」「证件号」「卡号」「账号」这类词。

工程上的做法是关键词窗口:不是全行搜索,而是要求敏感词出现在匹配位置之前的一个有限窗口内(比如前 8 个字符)。窗口不能开太大,否则一行里同时有「身份证」和「订单号」时又会串味;窗口也不能不开,否则就等于回到纯位数判断。

这里要接受一个事实:关键词窗口是业务知识,不是正则技巧。它依赖你对自家日志格式的了解,换一个系统就得重写。这也是为什么脱敏规则没法直接从网上抄一份通用的。

五、上线前的五个自检用例

自检用例的价值不在于「证明规则能跑」,而在于能证伪。一条用例如果怎么改规则都能过,它就没有存在意义。

判据五:规则要能被证伪。下面五个用例是从上面 11 次误判里反推出来的,每个都对准一种具体的失败模式。

用例输入片段(构造样本)期望结果对准的失败模式
同行混排用户 13812345678 下单 ts=1759999999999手机号被掩,时间戳原样子串误吃长数字
写法混排联系 159 0000 1111,备用:13812345678两条都被掩分隔符与全角各漏一类
末位小写证件号 11010119900307123x被掩[Xx]只写了大写
长数字互吃订单号 202610221234567890 卡号 6222021234567890订单号原样,卡号被掩两位数规则互相吃
单号带字母邮箱 zhangsan@example.com 运单号 SF1381234567890邮箱被掩,运单号原样字母前缀没进锚点

⚠️代码待验证

# 用例断言:输出里必须出现 expected 里的每个片段,顺序不限CASES=[("同行混排","用户 13812345678 下单 ts=1759999999999",["138****5678","1759999999999"]),("写法混排","联系 159 0000 1111,备用:13812345678",["159****1111","138****5678"]),("末位小写","证件号 11010119900307123x",["110101********123x"]),("长数字互吃","订单号 202610221234567890 卡号 6222021234567890",["202610221234567890","6222********7890"]),("单号带字母","邮箱 zhangsan@example.com 运单号 SF1381234567890",["z***@example.com","SF1381234567890"]),]

写用例时有个细节容易漏:期望值要写「原样保留」的那一半。只断言「敏感字段被掩了」,规则把整行都打成星号也能通过;把「订单号必须原样出现」写进断言,误删才会变成红灯。

这份资料是什么:一份按「先跑通再排查」顺序整理的大模型学习路线图,里面有一节专门讲工程侧的数据处理与合规动作该怎么拆步骤。放在资料包里,扫码即可获取:

六、一张可抄的规则表

下面这份清单是本文的结论汇总。「实测量」一列写的是主代理脚本在本机跑出来的结果;「锚点改进」一列是本文加的,属于改进写法,没有实跑。

字段实测量(分母 5 或 15)锚点改进要点
手机号R2 归一化后 15/15;R2 原样 14/15数字边界 + 显式国家码 + 分隔符可选
身份证5/5,但只写大写X会漏末位小写[\dXx]+ 关键词窗口,位数不足以定类型
银行卡3/5,带空格/短横全漏;反向吃身份证 5/5允许分隔符 + 关键词窗口 + 校验位
邮箱5/5,本轮无误判@天然锚点,保持简单,不要过度加约束
时间戳被三条手机号规则误吃不靠正则排除,走豁免清单
订单号 / 运单号被 R4、R5 与 R1 误吃只能靠字段名关键词豁免,需业务方给字段清单

⚠️代码待验证

# 本文改进写法(锚点 + 豁免),未在主代理脚本中实跑,需按自家日志格式调FW=str.maketrans("0123456789","0123456789")# 1) 豁免清单:已知的正常长数字,匹配前先挂号,脱敏时跳过这些区间EXEMPT=re.compile(r"(?<!\d)1[6-9]\d{11}(?!\d)")# 13 位毫秒时间戳# 2) 四类字段:长规则先跑,短规则后跑,避免 16~19 位吃掉 18 位身份证ID_CN=re.compile(r"(?<![\dXx])\d{17}[\dXx](?![\dXx])")CARD=re.compile(r"(?<![\d\-])\d{16,19}(?![\d\-])")PHONE=re.compile(r"(?<![\d\-])(?:(?:\+|00)?86[\- ]?)?1[3-9]\d(?:[\- ]?\d{4}){2}(?![\d\-])")EMAIL=re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")

这份清单里有三处是「必须按你家的日志改」的,抄的时候不要整段复制:豁免清单里的时间戳位数、国家码的写法、以及银行卡前后允许哪些分隔符。我这轮样本里只有空格和短横两种分隔,实际日志里可能还有括号、点号、斜杠。

6.1 掩码留多少:留可定位的部分

掩码,指的是用星号等字符替换敏感片段。留几位是有讲究的:留得太少,客服核对不了;留得太多,等于没脱敏。

字段建议保留掩码后保留的理由
手机号前 3 后 4138****5678前 3 位看号段,后 4 位供本人核对
身份证前 6 后 4110101********1234前 6 位是地区码,排障时常按地区筛
银行卡后 46222********7890后 4 位是用户报卡时唯一会说的信息
邮箱首字母 + 域名z***@example.com域名能定位到企业客户
订单号 / 时间戳全部保留原样它们是排障主键,脱了就没法查

6.2 顺序与幂等

执行顺序会直接影响结果:\d{16,19}会吃掉 18 位身份证,所以身份证规则必须在银行卡规则之前跑,或者在银行卡规则里排除已经被身份证规则占用的区间。

幂等,指的是同一段文本处理两次,结果和一次一样。掩码用*替换数字之后,第二次跑时那些位置已经不是数字了,不会二次命中,所以天然幂等。但如果某天有人把掩码改成「保留位数、把数字换成 0」,第二次跑就会因为位数没变而再掩一次,长度悄悄变短。这种改动必须在用例里回归。

七、收尾:把脱敏当接口契约

7.1 契约要写清三件事

《中华人民共和国个人信息保护法》第五十一条第(三)项要求个人信息处理者「采取相应的加密、去标识化等安全技术措施」。去标识化,指的是让信息在不借助额外信息的前提下无法指向特定个人。脱敏脚本就是这条要求最直接的落地点。

但落到工程上,脱敏不该被当成一个脚本,而该被当成一份接口契约。契约至少要写清三件事:

  1. 输入写法清单:这套日志里手机号有几种写法、身份证会不会带小写 x、卡号会不会分组。这份清单要由写日志的人确认,不是脱敏的人猜。
  2. 输出掩码格式:每个字段保留几位、用什么字符掩。这决定了排障的人还能不能干活。
  3. 豁免字段清单:哪些长数字是正常字段,绝不能动。这份清单是误判的唯一解药。

7.2 规则要有版本号,也要有回归

最后一条判据:规则文件要带版本号,日志落盘处要记下用的是哪一版。

理由是误判的形态会变。今天被误吃的是 13 位时间戳,明天业务加了一个 19 位的内部单号,同样会被\d{16,19}吃掉。如果日志里没有版本信息,出问题时你无法判断「这条日志里的订单号为什么变成星号」是新规则造成的,还是三个月前就有的老问题。

回归的方式很简单:把第 5 章那五个用例做成每次改规则都跑的检查,再加一条「正常字段不得被掩」的断言。用例不需要多,需要的是每次都会跑。

这份资料是什么:在线可查的大模型知识库,里面有 Agent Skills 与工程化落地的专题目录,适合用来对照本文这种「一条规则怎么变成可回归的接口」的写法。放在资料包里,扫码即可获取:

附表 A:本文引用事实与出处对照表

事实出处本文位置
全部命中与误判数字(9/15、10/15、14/15、15/15、11 次误判、5/5、3/5)本机实测(work/2026-10-22_脱敏实测.py,日志work/2026-10-22_脱敏实测.log,Python 3.13.14,仅标准库 re)第 2、3 章
手机号 15 条样本按「连续 / 带分隔符 / 全角或带前缀」三类各 5 条同上脚本的 SAMPLES 字典1.2、2.1
R1 原样 9/15(60.0%),归一化后 10/15(66.7%)同上日志「手机号召回」段2.1
R2 原样 14/15(93.3%),归一化后 15/15同上日志「手机号召回」段2.1、4.1
6 条正常字段共 11 次误判,批次号与商品条码未被误判同上日志「正常字段误判」段第 3 章
身份证规则 5/5,末位小写 x 需[\dXx]才命中同上日志「其他关键点」段2.2
银行卡规则 3/5,且吃掉 5 条 18 位身份证同上日志「其他关键点」段2.2
邮箱规则 5/5,本轮无误判同上日志命中矩阵2.3
\d在 Unicode(str)模式下匹配任何 Unicode 十进制数字(含全角)Python 3 文档 re 模块,Python Software Foundation,https://docs.python.org/3/library/re.html4.1
\b为词边界,「词」在 Unicode 模式下指 Unicode 字母数字加下划线同上附表 B
第五十一条第(三)项:采取相应的加密、去标识化等安全技术措施《中华人民共和国个人信息保护法》全文,中央网络安全和信息化委员会办公室 2021-08-20 发布,https://www.cac.gov.cn/2021-08/20/c_1631050028355286.htm7.1
锚点版正则、豁免清单、关键词窗口为本文改进写法,未在本机脚本中实跑本文作者改写,对应代码块上方已标注待验证提示4.2、6.1

附表 B:术语速查表

术语一句话解释在本文哪里用到
脱敏把敏感字段替换、遮蔽后再落盘或外发的动作第 1 章开头
正则用一串符号描述「什么样的字符串算命中」的规则文本1.1
召回该抓到的里面,实际抓到了多少2.1
误判不该动的正常字段被动过了第 3 章
归一化比对之前先把不同写法统一成一种形式,这里指全角转半角4.1
锚点在规则两头加「前后必须是什么」的约束4.2
负向断言锚点的一种写法,要求「前后不能是什么」,如(?!\d)4.2
关键词窗口要求敏感词出现在匹配位置前方有限字符数内的约束4.3
豁免清单已知的正常长数字,匹配前先挂号,脱敏时跳过6.1
掩码用星号等字符替换敏感片段的做法6.1
幂等同一段文本处理两次,结果和一次一样6.2
去标识化让信息在不借助额外信息的前提下无法指向特定个人7.1

写在最后:这篇用到的资料

写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进