资讯详情

从一串怪字符说起:频率统计、信息熵与异常数据识别

📅 2026/10/16 12:58:04 | 华诺云谱 👁 阅读
从一串怪字符说起:频率统计、信息熵与异常数据识别
前几天排查一个内部系统的异常请求日志时我看到了一条这样的输入11111111155555。它安安静静躺在请求参数里既不像正常的业务数据也不像加密文本。第一反应当然是“脏数据直接忽略”但我还是花了一晚上把它完整拆了一遍。不是因为这串东西本身多重要而是这类“看起来毫无意义”的字符串在数据清洗、日志分析、接口安全校验里实在太常见。这个拆解过程用到的方法——频率统计、块结构识别、信息熵判断、编码排除、清洗策略——几乎可以平移到任何怪数据的处理场景。这篇就记录一下我是怎么从这串11111111155555开始一步步完成观察、统计、编码尝试和实战验证的。1. 第一眼拆解统计永远比猜测先行1.1 先做三件最基础的事面对任何一串不明数据我的习惯是不要急着猜测含义先做三件事量长度、数频率、看位置。这串11111111155555的长度是 22 个字符。字符集非常小只有三种符号数字 1、数字 5、加号。我用一个最简单的计数器跑了一遍得到这样的频率分布字符出现次数占比1940.9%5522.7%836.4%频率本身没有直接给出答案但它透露了一个重要信息三种字符的比例不是均匀的1 的出现次数几乎是 5 的两倍。如果是随机生成的字符串在三种字符等概率的情况下22 个字符里某种字符出现 9 次并不罕见但更关键的是“位置分布”而非“出现次数”。我把每个字符按位置列出来前 9 位全是 1中间 5 位全是 5最后 8 位全是加号。也就是说这不是一个字符均匀混杂的随机串而是一个典型的“三段式”结构连续的 1、连续的 5、连续的。这个观察比频率重要得多。1.2 结构上发现“三段式”特征如果你写过正则表达式看到11111111155555的第一反应可能和我一样提取连续块。用re.findall(r(.)\1*, s)跑一下结果非常干净import re s 11111111155555 blocks re.findall(r(.)\1*, s) print(blocks) # [1, 5, ]连续相同字符的块只有三个分别是“9个1”、“5个5”、“8个”。这看起来太整齐了。真实世界里完全随机的 22 位字符串几乎不可能恰好把相同字符排成三个连续大块。用概率感觉说如果字符是独立随机的相邻两个字符相同的概率只有约 1/3而这里整串只在两个边界处发生了字符切换转移次数只有 2 次。22 个字符理论上最多有 21 个相邻位置随机情况下预期会出现大约 14 次左右的不同字符转移。实际只有 2 次说明这串字符有很强的内部秩序。这个“三段式”结构直接排除了一大类猜测比如随机密码、随机 Token、加密输出。真正的随机串或加密后的 Base64 文本字符分布应该接近均匀且没有这种整齐的块状排列。1.3 用信息熵做一次“随机性体检”频率和块结构都比较主观我习惯再用信息熵做一个定量的判断。信息熵衡量的是一个数据源的不确定性如果按单字符概率计算公式是H - Σ p(x) log2 p(x)代入这串字符的概率p(1) 9/22 ≈ 0.409 p(5) 5/22 ≈ 0.227 p() 8/22 ≈ 0.364 H ≈ -0.409 * log2(0.409) - 0.227 * log2(0.227) - 0.364 * log2(0.364) ≈ 0.527 0.482 0.531 ≈ 1.54 bit这个 1.54 bit 是什么水平呢如果完全等概率三字符最大熵是 log2(3) ≈ 1.585 bit。只看单字符频率这串字符的熵其实并不低接近理论最大值。但真正的随机性不能只看单字符概率还要看“转移熵”——也就是相邻字符之间是否存在规律。这串字符的转移只有两次相邻字符高度相关转移熵趋近于 0。所以结论很明确单字符频率接近随机但转移结构极度规整说明它一定来自某种有规律的生成过程。注意信息熵不是万能的但它能帮你在第一轮判断中快速区分“真随机”和“似随机”。看到熵值很高但转移次数极低的数据优先怀疑是人为构造、模板拼接或键盘误触而不是加密数据。2. 数值与编码几种看起来合理、最后被排除的解读2.1 把它当作普通整数和表达式拿到一串“1”和“5”很多人第一反应是这会不会是一个数字如果把加号去掉11111111155555是一个 14 位整数约等于 1.1 万亿。这个数字本身没有特别之处。如果把加号当作运算符情况就尴尬了111111111 55555 不是合法表达式因为加号后面没有第二个操作数。你可以把它理解成“从11111111155555上又加了很多次空操作”但这不是正常的表达式语法。还存在一种可能性加号是不是分隔符比如把数字串按切分得到11111111155555然后后面 8 个加号全是空字段。这种模式在某些 CSV 或协议解析里是有可能出现的比如“字段1、字段2、字段3都为空”会用连续的符号表示。但在没有上下文的情况下这只是一种可能性不能作为结论。2.2 分组与数位联想既然看到了“9个1、5个5、8个”很自然地会想到9、5、8 这三个数字是不是有含义9、5、8 可能组成958一个三位数。9 × 5 4545 8 5353 也许对应某个 ASCII 码ASCII 53 就是字符5这有点自指的味道但更像是巧合。9 5 8 22正好是字符串长度但这只是把长度拆开又加回去没有新信息。这类联想非常容易“自圆其说”。拿到一串怪数据时大脑会自动搜索所有可能的解释而且一旦找到一个勉强说得通的解释就倾向于深信不疑。这在实际分析中是非常危险的确认偏误。我自己的经验是所有基于数字组合的联想只有在拿到明确的业务上下文后才能作为候选假设否则一律先放一边。2.3 ASCII、Base64 与 URL 编码视角接下来用编码视角走一遍。字符1、5、的 ASCII 码分别是 49、53、43。把码值拼起来会得到495343...这依然不像可读文本。Base64 呢Base64 的字符集确实包含但 Base64 要求字符串长度是 4 的倍数。这个字符串长度为 22不是 4 的倍数直接解码会失败。而且 Base64 解码结果如果全变成不可读字节通常意味着输入不对。还有一点特别值得注意在 URL 编码中号会被解析为空格。如果这串字符出现在 URL 查询参数里服务端拿到的值很可能是11111111155555后面跟着 8 个空格。这不是这个字符串本身的意思而是传输层做了隐式转换。类似的坑在日志分析中经常出现你看到的字符串和业务层实际解析出来的值不是同一个东西。我把几种常见的编码尝试做了一个汇总解码方向结果排除原因整数解析11111111155555加号无法解释ASCII 码拼接495343...无可读文本特征Base64解码失败长度不是 4 的倍数URL 解码数字 8 个空格是传输层解释非内容本身十六进制长度不合法偶数字节要求不满足这一轮排查结束后基本可以判断它不是一个标准的编码输出更像是一段“有人按着键盘乱输入的测试数据”。3. 动手写脚本把字符串完整“体检”一遍3.1 准备一个极简 Python 环境做这类单次分析不需要任何第三方库Python 自带的collections、re、math就够了。整个分析脚本不超过三十行。我建议你把这类“字符串体检”脚本保存成一个通用工具以后遇到任何怪数据都能直接套用。先写最基础的统计逻辑from collections import Counter s 11111111155555 print(长度:, len(s)) freq Counter(s) for ch, n in freq.most_common(): print(f字符 {repr(ch)}: {n} 次, 占比 {n / len(s):.2%})输出很直接长度 221出现 9 次5出现 5 次出现 8 次。这个脚本本身几分钟就能写出来但它能避免你靠肉眼数错——尤其是字符串更长的时候。3.2 用脚本验证“块结构”和“转移次数”肉眼看到三段式结构之后最好用脚本验证一下尤其是“转移次数”这个指标。我写了一段小逻辑s 11111111155555 # 提取连续块 import re blocks re.findall(r(.)\1*, s) block_lens [len(m) for m in re.findall(r(.)\1*, s)] print(连续块:, blocks) print(每块长度:, block_lens) # 统计相邻字符转移 transitions [s[i] s[i 1] for i in range(len(s) - 1)] distinct_transitions set(transitions) print(相邻转移种类:, distinct_transitions) print(转移次数:, len(transitions), 不同转移数:, len(distinct_transitions))跑完结果连续块是[1, 5, ]长度是[9, 5, 8]相邻转移总共 21 次但不同转移种类只有两个15和5。这意味着字符串内部只发生了两次“变字符”事件其余 19 个相邻位置都是同字符连续。对比随机生成的含三种字符的长度 22 字符串不同转移数通常会在 8 到 14 之间。这个结果从数量上证实了“高度规整”的判断。3.3 写一个自动化“解码尝试小框架”前面的编码排除都是手动做的我建议把它整理成一个可复用的解码尝试框架至少覆盖以下几种常见处理import base64 def quick_decode_try(s): print(原串:, s) # 去掉所有符号后当纯数字看 digits .join(ch for ch in s if ch.isdigit()) print(纯数字部分:, digits, 长度, len(digits)) # 尝试补位 base64 padded s * (-len(s) % 4) try: print(base64解码:, base64.b64decode(padded)[:20]) except Exception as e: print(base64解码失败:, e) # URL 解码模拟 转空格 print(URL 风格解码:, s.replace(, )) quick_decode_try(11111111155555)注意 base64 这里我做了补位因为原串长度 22不是 4 的倍数。补到 24 位后再解码输出的也是乱码字节进一步说明它不是 base64 内容。实操提示给 base64 补位的时候用 * (-len(s) % 4)是安全的技巧。它能算出最少的补位等号数量避免手动计算。这个用法在处理任意长度字符串时非常有用。3.4 运行结果与结论整个“体检”下来我的结论很明确11111111155555是一串低复杂度、高块状结构、非标准编码的数据。它不携带可识别的语义最大概率来自以下三种情况之一键盘连续按键产生的输入、自动化脚本生成的测试占位符、或者某个系统在拼接时出现了异常残留。脚本的价值在于它把“我觉得”变成了“数据证明”。哪怕最后没有得到一个具体含义你已经可以用统计特征向别人描述这串数据为什么异常。4. 真实场景这串“怪数据”通常从哪里来4.1 用户侧误触与输入习惯排查过真实表单数据的人都知道11111111155555这类输入在用户侧真的会出现。常见场景包括用户在键盘上按住数字键不松开、手机九宫格输入时连续按同一个键、或者测试人员在表单里随便打了一串字符然后提交。如果后端没有做格式校验这些数据就会进入数据库、进入日志、进入分析报表。我碰到过一个案例某个统计系统里出现大量111111开头的字符串因为用户习惯性用“111111”作为姓名、手机号、验证码的测试占位。这类数据的特征非常统一大量重复数字、夹杂一两个符号、长度偏短。它们不构成攻击但对数据质量的影响很大。4.2 日志中的探测与爬虫特征另一类来源更需要注意安全扫描器和爬虫。很多自动化扫描工具在探测接口时会构造大量“畸形输入”比如超长数字、连续符号、特殊字符混搭。11111111155555这种字符串结构太典型了——高重复、带符号、无明显语义完全符合扫描器用来测试解析器鲁棒性的“怪样本”。需要注意的是这类输入不一定是恶意的。有些爬虫会把占位符塞进参数里比如用11111111155555作为 ID 去批量探测接口是否存在越权漏洞。如果你的接口没有做参数校验就可能返回异常信息被扫描器进一步利用。4.3 程序 BUG 产生的占位与污染还有一种比较隐蔽的来源程序内部 bug。举例来说某个生成 ID 的模块初始化失败返回了默认值11111111155555然后在拼接参数时又把若干个符号也拼了进去最终形成了整条字符串。这种数据进入日志如果不加处理会污染统计口径比如统计“今日请求总量”时把异常请求也算了进去。我个人在排查数据异常时有一个习惯先查调用链里有没有默认值拼接再看网络层有没有篡改最后才会怀疑外部攻击。因为内部 bug 产生的数据往往有固定格式比如同样的数字串反复出现这比偶发的外部攻击更容易定位。4.4 如何防御与清洗针对这类怪数据有两个层面的处理方式。参数校验层给接口入参加上白名单正则是最快的手段import re def is_valid_input(value): # 根据业务场景调整这里只允许数字、字母、中划线、下划线 return bool(re.fullmatch(r[0-9a-zA-Z_-]{1,64}, value)) print(is_valid_input(11111111155555)) # False print(is_valid_input(order_20240101)) # True注意白名单必须跟着业务走。如果你的业务允许手机号字段带比如国际区号86就不应该在手机号字段直接拦截。过于一刀切的规则会造成误杀比漏杀还麻烦。日志清洗层可以用字符块特征把这种数据标记出来而不是完全依赖黑名单。比如连续相同字符超过 5 个、并且包含非字母数字符号的字段就有较大概率是异常数据。这个规则比单纯拦截要稳得多。4.5 反过来用模糊测试里的“无意义字符串”这串“无意义”字符串在防御场景里是坏数据在测试场景里却是好素材。做接口模糊测试时我会把类似11111111155555的样本加进测试集专门用来验证解析器的鲁棒性连续加号是否会导致解析异常超长数字串是否会导致整数溢出重复字符是否会让正则匹配陷入灾难性回溯这类测试的价值在于真正的线上问题往往不是设计出来的而是这些“看起来乱按”的输入打出来的。把这些怪字符串收集起来就是一个不错的基础模糊测试语料库。5. 解读怪字符串的三个方法论5.1 先统计再猜测每次有人拿一串不明数据来问我我的建议都是同一个在你猜测它的含义之前先跑一遍统计。统计能告诉你的不是“它是什么”而是“它像什么”。频率分布、长度、字符集、块结构、转移次数——这五个指标能快速把绝大多数错误猜测排除掉。没有统计支撑的猜测只是灵光一闪有了统计支撑的猜测才叫假设。5.2 结构感优于单个字符词义很多人在分析11111111155555时会死死盯着“1”和“5”试图算出某个特殊数字。但真正有价值的信息其实是结构为什么三个块的长度是 9、5、8为什么字符切换恰好只发生两次在数据处理和安全分析里结构特征是比内容语义更通用的判断依据。我甚至建议你忘掉 1 和 5 的字面含义只把它们看成三个符号 A、B、C然后研究 A 和 C 的出现规律这样反而更容易发现规律。5.3 警惕确认偏误人类大脑在信息不确定时会疯狂寻找“说得通”的解释。一旦你产生了“958 可能是某个日期”的念头就会下意识找证据去支持这个念头而忽略那些不吻合的信息。应对办法是每次产生一个解释时主动问自己“如果这个解释是错的什么证据能推翻它”如果找不到任何能推翻这个解释的方法它就不是一个科学假设只是一个故事。对于11111111155555我能够提出的所有具体含义解释都找不到可靠的验证手段所以最终我不会把它解读成任何具体含义。最后再分享一个很实用的小技巧如果以后你收到这类怪字符串最快的验证方法不是穷举所有可能而是先把它复制到脚本里统计一下连续块长度。如果连续块是整齐的固定数字比如 9、5、8或者出现“一个字符种连续出现多次”的模式那就优先怀疑“手动输入”或“模板生成”如果连续块长度参差不齐且字符高度混杂再考虑编码或加密。这个小判断能帮你省掉大半晚的无效分析。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑