ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数字串异常检测实战:从特征工程到规则打分

数字串异常检测实战:从特征工程到规则打分 线上系统半夜报警我爬起来看了一眼日志一串诡异的数字躺在请求参数里“155555555559999999999999”。第一反应是用户手滑第二反应是机器人刷接口。后来验证发现这串数字既不是正常手机号也算不上标准攻击payload但它确实触发了我负责的某个策略模块。就是这串看似无意义的数字让我花了两天时间调规则、跑测试、上线过滤逻辑。今天把这套处理思路完整写出来希望能帮你少踩几个坑。这串数字放在不同场景里含义完全不一样可能是异常流量探测、内容审核的漏网之鱼、验证码接口的批量调用特征也可能是普通用户随手乱填的脏数据。但不管哪一种背后都指向同一个问题如何快速识别“非人类正常输入”的纯数字串并且做到低误杀、高召回。全文围绕这个核心问题展开包含特征设计、Python实现、阈值调优和线上排障经验适合做风控、反作弊、内容安全以及后端数据清洗的同学参考。1. 从一串异常数字说起它到底意味着什么1.1 复盘当天的那条告警日志先还原一下当时的情况。那天凌晨两点告警群弹了一条消息说某个开放接口的异常请求量在短时间内上涨了六倍。我打开日志按请求参数聚合发现一个非常扎眼的特征大量请求的某个字段值介于18到24位之间而且绝大多数是纯数字。正常用户填写这个字段时常见值是11位手机号偶尔会有固话、区号加号码、或者带分机号的场景。但“155555555559999999999999”这种长度达到24位的纯数字明显不是正常输入。更关键的是这串数字的内部结构很不寻常前面是1个1、连续6个5、再连续11个9。这种高密度重复的模式在正常业务数据里几乎不可能出现。我当时做的第一件事不是写识别规则而是把这批异常样本拉到一起做了一次快速统计。结果发现规律非常明显超过83%的异常数字串都包含连续5位以上的重复数字而正常手机号样本里这个比例不到0.2%。这个对比已经足够说明数字串的重复模式本身就是最强的区分特征。1.2 这类数字串最常见的四个来路同样是超长纯数字串来源不同处理策略也不同。根据我接触过的案例大体可以归成四类。第一类是接口探测与扫描。攻击者会用随机数生成器批量填充参数探测接口是否存在注入、越权、逻辑漏洞。这类数字串往往随机性很高没有明显重复规律。第二类是脚本批量提交比如刷注册、刷验证码、刷抽奖。脚本为了绕过简单校验会故意填充重复数字因为成本最低。前面说的连续5和连续9就属于这类。第三类是谐音/敏感内容编码。有些人会用数字谐音表达特定含义比如用“995”表达某种情绪用“5201314”表达爱情。这一类数字串虽然也是正常的但在内容审核场景里需要单独识别不能一棍子打死。第四类是纯用户手滑。用户长按数字键盘不松手就会出现一长串重复数字。这类数据的处理原则是温和提醒或静默清洗而不是拦截。不同来源的数字串统计特征差异很大。随机探测的数字串熵值高、重复率低批量提交的数字串重复率高、熵值低谐音编码的数字串则在特定位置上呈现局部特征。理解了这些差异你才能设计出有区分度的识别逻辑而不是简单用一个正则表达式去碰运气。2. 识别思路拆解为什么单靠黑名单行不通2.1 黑名单方案的天然缺陷很多人在面对这类问题时第一反应是维护一个“非法数字串黑名单”把见过的异常样本都放进去。这套方案在样本量小的时候确实简单粗暴有效但一旦数据量上来就完全失控。黑名单的致命问题有两个。第一是无法覆盖未知变体。攻击者写脚本时只要在数字串里随机插入一两个字符或者改变重复次数黑名单就失效了。第二是维护成本无限增长。每天都有新样本产生你永远在追着攻击者跑而不是在源头拦截。我见过一个团队用黑名单方案处理类似的接口滥用第一周效果很好拦截率超过90%。到了第二个月黑名单膨胀到几万条拦截率反而降到60%以下。原因很简单攻击脚本换参数的频率远比你更新黑名单的频率快。所以正确的方向不是“记住所有坏样本”而是抽象出坏样本的共同特征形成一套可泛化的判断逻辑。2.2 特征一重复片段的占比回到“155555555559999999999999”这串数字最直观的特征就是重复。24位数字里连续重复的数字占了20位。如果你把这个数字串拆成单个字符会发现字符分布极度不均衡数字“5”出现了7次数字“9”出现了11次其余都是个位数。用专业一点的说法就是最大连续重复长度和重复字符占比这两个指标。正常手机号“13812345678”里最大连续重复长度不会超过2重复字符占比也很低。而攻击样本里最大连续重复长度动辄超过5重复字符占比往往超过60%。这个特征非常稳定。我测试过不同攻击脚本生成的样本重复模式的规律基本一致。原因是脚本生成数字串时最省事的方式就是用同一个数字重复拼凑程序员写代码时也会下意识用“9”或“5”这类按键位置好按的数字。2.3 特征二字符熵值异常第二个特征是字符熵值这个概念看起来高大上其实理解起来并不难。熵值衡量的是字符串里的“混乱程度”。随机生成的数字串每个数字出现的概率差不多熵值就高如果某个数字反复出现字符分布很不均匀熵值就低。“155555555559999999999999”的熵值就非常低因为大量信息集中在“5”和“9”两个数字上。而正常随机数字串的熵值会接近最大值。用熵值作为特征的好处是它不依赖于具体的数字内容能捕捉到“分布异常”这种更本质的问题。在Python里计算字符串的Shannon熵并不复杂用collections.Counter统计每个字符的出现频率然后用-sum(p * log2(p))公式累加就行。这个指标搭配重复率特征能覆盖绝大多数模式化生成的数据。具体代码后面会给你完整实现。2.4 特征三与数字谐音词库的碰撞第三个特征是语义层面的用数字谐音词库做碰撞。像“520”代表“我爱你”、“1314”代表“一生一世”、“995”代表“救救我”、“9958”代表“救救我吧”这些在内容安全领域都已经被广泛使用。加这一层的目的是为了区分恶意数据和带语义的敏感内容。同样是超长数字串如果是“520131499999”前面那部分是有语义的后面那部分是凑数用的它的处理方式和纯恶意探测数据完全不同。前者可能需要进人工审核后者直接拦截就行。谐音词库不需要一开始就建全维护一个持续更新的小词库就好。每遇到一个新变体就追加一条几个月下来覆盖量就很可观。单独用词库做判断同样容易误杀但它和前面两个特征组合起来能显著提高整体方案的精准度。2.5 规则的组合与打分制既然单个特征做判断都不够全面实战中更稳妥的方案是打分制。每个特征根据命中情况给出一定分值最后把总分和阈值比较决定是放行、告警还是拦截。打分制的设计思路是这样的重复率超过50%加20分最大连续重复长度超过5加30分熵值低于某个阈值加30分命中谐音词库加10分。最后根据业务场景设置两档阈值比如总分超过40分进人工审核超过70分直接拦截。这套方案的灵活性在于你可以针对不同接口设置不同阈值。对注册接口阈值可以收紧一点对查询接口阈值可以适当放宽避免误伤正常用户。打分制的本质不是让你追求一个完美规则而是在精确率和召回率之间找到一个可调节的平衡点。这是规则类方案最大的优势也是它相比简单黑名单的进阶之处。3. 落地实现一套可复用的数字串检测模块3.1 数据清洗与基础判断写代码之前先把输入数据的处理逻辑理清楚。这个模块的输入是字符串但线上传过来的参数往往带有空格、换行、前导零等干扰因素。所以第一步是清洗。清洗规则包括去除首尾空白字符去除字符串中间的所有空白和分隔符比如-、空格过滤掉非数字字符。完成清洗之后再做一次基础判断如果字符串不是纯数字直接跳过后续检测逻辑。这个模块只负责纯数字串。基础判断里还包含一个长度检查。我设置了两个阈值小于6位或大于15位的数字串才进入深度检测。这个设计的依据是正常业务场景里的数字型字段手机号、验证码、订单号基本都在这个区间之外超长或超短反而可疑。长度检查能过滤掉大量正常请求减轻后续特征计算的性能压力。以下是清洗和基础判断的Python实现import re from collections import Counter import math def clean_number_string(raw: str) - str: 清洗原始输入保留纯数字部分 if not raw: return # 去除空白和常见分隔符 cleaned re.sub(r[\s\-()], , raw) # 只保留数字字符 cleaned re.sub(r\D, , cleaned) return cleaned def is_suspicious_length(s: str) - bool: 长度异常判断正常数字字段长度通常集中在某个区间 return len(s) 6 or len(s) 15这一步的代码逻辑很简单但容易被人忽略。清洗规则如果太激进会把“138-1234-5678”这种带分隔符的正常号码切坏如果太保守又会把攻击者的变形输入漏掉。我这里采用的策略是保留所有数字忽略除数字外的其他字符。这样既能处理常见的格式变形又不会误伤正常数据。3.2 重复性检测的两种指标重复性检测是本方案的核心。我实现了两个指标最大连续重复长度和重复字符占比。最大连续重复长度可以用正则表达式直接匹配r(\d)\1{4,}表示匹配同一个数字连续出现5次及以上的情况。重复字符占比的计算方式是把每个字符出现次数大于1的数字累加起来除以字符串总长度。这两个指标侧重点不同。最大连续重复长度适合捕捉“111111”、“999999”这种极端重复模式重复字符占比适合捕捉“15151515”这种虽然不连续但字符高度集中的模式。两个指标分开计算、分开打分最终计入总分。def max_repeat_length(s: str) - int: 计算最大连续重复长度 pattern re.compile(r(\d)\1) matches pattern.findall(s) if not matches: return 1 # 重新用finditer获取完整匹配对象 max_len 1 for m in pattern.finditer(s): max_len max(max_len, len(m.group())) return max_len def repeat_char_ratio(s: str) - float: 计算重复字符占比出现次数大于1的字符占比 if not s: return 0.0 counter Counter(s) repeated_count sum(count for count in counter.values() if count 1) return repeated_count / len(s)有个细节需要注意写正则的时候很容易图省事直接findall返回捕获组里那个数字却忘了拿完整匹配串的长度。我第一次实现时就踩过这个坑findall只返回了某个重复数字本身根本算不出连续长度。后来改用finditer遍历所有匹配项才能正确拿到每次连续重复的完整长度。3.3 熵值计算的Python实现熵值的计算方式和前两个指标不同它衡量的是整个字符串的“信息密度”是否异常。理论上纯随机数字串的字符分布接近均匀熵值会接近log2(10)≈3.32。而“155555555559999999999999”这种字符串字符分布极度偏向“5”和“9”熵值会低到2.0以下。def shannon_entropy(s: str) - float: 计算字符串的Shannon熵 if not s: return 0.0 counter Counter(s) length len(s) entropy 0.0 for count in counter.values(): p count / length entropy - p * math.log2(p) return entropy单看一两个样本熵值可能不明显但我在测试集上跑过几百个样本之后发现规律很清晰正常手机号的熵值基本都在3.0以上而批量生成的重复数字串熵值普遍低于2.5中间几乎没有重叠区域。这个特征可以作为重复率特征的有效补充尤其在面对“1313131313”这种交替重复模式时重复率指标会失效但熵值依然能准确识别。3.4 谐音/变形词匹配第三步是谐音词库匹配。这个模块的设计思路是维护一个字典key是数字谐音串value是语义标签。匹配时遍历数字串的所有子串如果子串出现在词库里就记录对应的语义标签。需要注意的是词库匹配不能只做精确匹配还要考虑前缀和后缀的拼接。比如“520131499999”前面的“5201314”是有效谐音后面那一串“99999”是凑数的。这种情况不能因为命中词库就放行而应该进一步检查“命中词库之外的字符占比”。如果命中词库的字符只占一小部分说明整串数字仍然可疑。HOMOPHONE_DICT { 520: I love you, 1314: 一生的爱, 995: 救我, 9958: 救救我吧, 530: 我想你, 886: 拜拜啦, 687: 对不起, 555: 呜呜呜, } def match_homophone(s: str) - int: 统计命中的谐音词条数量 hits 0 for word in HOMOPHONE_DICT: if word in s: hits 1 return hits词库的扩展是个持续性的工作。我建议每两周花半小时过一遍新的告警样本把反复出现的数字组合提取出来确认语义后加入词库。半年时间就能积累一个覆盖常见场景的小词库对精准识别非常有帮助。3.5 综合打分与决策输出把前面的特征计算整合起来就是完整的决策逻辑。打分规则定义如下长度异常加10分最大连续重复长度大于等于5加30分重复字符占比大于等于0.5加20分熵值小于2.8加30分命中谐音词库加10分。阈值设计为两个档位总分大于等于60分判定为高风险直接拦截总分大于等于35分判定为可疑进入人工审核或验证码挑战其余情况放行。def evaluate_number_string(raw: str) - dict: 综合评估数字串风险 s clean_number_string(raw) if not s: return {risk: pass, score: 0, reason: empty} result { length: len(s), max_repeat: max_repeat_length(s), repeat_ratio: repeat_char_ratio(s), entropy: shannon_entropy(s), homophone_hits: match_homophone(s), } score 0 reason [] if is_suspicious_length(s): score 10 reason.append(suspicious_length) if result[max_repeat] 5: score 30 reason.append(high_repeat) if result[repeat_ratio] 0.5: score 20 reason.append(high_repeat_ratio) if result[entropy] 2.8: score 30 reason.append(low_entropy) if result[homophone_hits] 0: score 10 reason.append(homophone_hit) if score 60: risk block elif score 35: risk review else: risk pass return {risk: risk, score: score, reason: reason, features: result} if __name__ __main__: test_input 155555555559999999999999 print(evaluate_number_string(test_input))跑一遍上面的测试输入输出大概是{risk: block, score: 90, reason: [suspicious_length, high_repeat, high_repeat_ratio, low_entropy], ...}。这个结果符合预期长度异常加10分连续重复加30分重复占比高加20分熵值低加30分总分90分直接拦截。打分逻辑的调优空间很大。比如有些业务场景里谐音内容需要优先进入人工审核而不是直接拦截那就可以把命中词库的分值权重调高并且把“review”和“block”的边界阈值做调整。核心思想是规则是死的组合是活的阈值必须根据业务反馈不断迭代。4. 效果验证与误杀控制4.1 测试集设计与阈值调优写了判断逻辑只是一半工作另一半是验证效果。我建了一个包含三类样本的测试集正常样本500条真实手机号、订单号、正常验证码、恶意样本500条脚本生成的重复数字串、随机数字串、谐音变体、边界样本200条介于正常和异常之间的数据。正常样本的标签是“放行”恶意样本的标签是“拦截”边界样本的标签是“人工审核”。跑完测试之后统计精确率和召回率。我第一版规则测试下来恶意样本召回率能到96%但正常样本误杀率有3.4%。这个误杀率对某些接口来说太高了。调低阈值之后把所有特征的分值整体乘了0.8误杀率降到1.2%但恶意样本召回率也降到88%。后来改成只调整熵值的阈值从2.8降到2.5误杀率降到1.5%召回率保持94%。这说明不同特征对误杀的影响差异很大单独调某个特征的阈值比整体调分更有效。4.2 分场景差异化策略从实际运营角度来看一个全局规则很难同时满足所有业务场景。比如验证码接口和订单查询接口对误杀的容忍度完全不同验证码接口输错一次可以重试误杀顶多影响用户体验订单查询接口如果误杀用户会以为订单丢失直接打客服投诉。所以我把规则拆成了三档配置严格模式注册、支付、验证码接口阈值降到50分、标准模式普通查询、内容提交阈值保持60分、宽松模式只记录日志不拦截用于灰度观察阈值提高到80分。每个接口在接入时明确指定模式并且在后台日志里记录命中的特征方便追溯。这个配置化的思路在落地时非常实用。你可以先用宽松模式观察一周看看正常流量中有多少会被判定为高风险再逐步收紧。如果一开始就直接上严格模式大概率会被误杀反馈淹没。5. 实际落地中踩过的坑5.1 误杀正常电话号码第一个坑就是误判带区号的座机号码。比如“01012345678”这类字符串用我最初的长度判断逻辑长度是11位不算异常但里面有一个“0”和“1”的连续重复重复字符占比也不低熵值偏低整体评分容易被误判成可疑。解决方式是把合法前缀表加进基础判断。如果数字串以已知的区号开头并且总长度在合法范围内直接放行。同样的情况还出现在一些特定业务编号上比如以“00”开头的国际订单号。所以任何识别规则都要和业务数据的真实分布对齐单纯靠通用逻辑一定会误杀。5.2 攻击样本也在变异规则上线大约两周后我注意到一种新的样本模式攻击者开始在数字串中间插入随机字符来绕过清洗逻辑。比如“1a55555b999999”清洗后变成“1555555999999”重复特征依然明显能命中规则。但如果攻击者把重复数字也打散变成“159159159159”重复率指标就不太管用了。这种情况下熵值指标就派上用场了。虽然重复率不高但字符分布仍然不均衡“1”“5”“9”三个数字的占比明显异常。我在特征组合里加强了熵值的权重比例专门应对这种“伪随机”样本。单一特征永远有绕过空间特征组合才是相对稳健的方案。5.3 性能开销比想象中高还有一个容易被忽略的问题是性能。shannon_entropy函数需要对字符串里的每个字符做一次对数运算在低并发场景下毫无压力但在每秒处理几千个请求的高并发接口里这个开销会被无限放大。我做了一次压测纯字符串特征计算单次耗时大约2毫秒到4毫秒看起来不高但并发翻五倍之后就变成了性能瓶颈。后来做了两个优化第一对长度非常短的字符串小于等于8位直接跳过熵值计算因为信息量不够算出来的熵值参考意义不大第二用缓存表记录已计算过的数字串特征相同输入直接读取缓存结果。优化后单次耗时降到1毫秒左右性能问题基本解决。6. 延伸这些特征还能用在哪些场景这套数字串异常检测的方法说到底是基于字符串统计特征的风险识别所以它的应用范围并不局限于防作弊和内容安全。比如在日志清洗场景可以用来过滤爬虫产生的无效日志。很多爬虫日志里的数字ID都是随机生成的熵值特征能准确识别出来。在用户画像场景可以用来识别异常注册行为。连续多个账号使用同一套数字串模式往往表明是同一批自动化工具批量注册的。在验证码安全场景也可以用来识别验证码识别脚本的请求特征提前在接口层阻断。更通用的一点是这种“设计特征、组合打分、阈值调优、灰度上线”的流程适用于大量风控类问题。你不需要一开始就上复杂模型规则方案的可解释性强调优路径清晰特别适合中小团队快速落地。等数据积累到一定规模后再用机器学习模型去替换或增强规则层效果会有质的提升。回到最初那串“155555555559999999999999”它本身没什么特殊含义但在系统眼里它暴露的是模式有人在用最低成本的方式探测你的接口。能把这种低成本攻击挡在门外让正常用户完全无感知就是这套方案的价值。我个人在实际操作中养成了一个习惯每隔一段时间就把拦截数据重新拉出来分批检查误杀和漏报然后微调一次阈值。风控规则不是上线之后就不用管了它会随着攻击手法的变化慢慢失效需要持续维护。这个数字串检测模块运行到现在整体拦截准确率稳定在较高水平误杀率也控制在业务可接受的范围内。如果你也要做类似的需求建议从最小可用版本上线再根据线上反馈逐步迭代。
返回列表