首页 / 资讯中心 / 文章详情

一串27位数字暗藏两层等差数列:用Python拆解重复字符串的规律

一串27位数字暗藏两层等差数列:用Python拆解重复字符串的规律 ★ FEATURED ARTICLE
1. 第一眼以为是乱码第二眼才看出门道1.1 先做最笨的统计再谈聪明的主意前几天有人在技术群里丢了一串数字1111111155555555599999999999后面没带任何解释。第一反应是手滑了吧第二反应是这不会是从哪复制的验证码吧。但我多看了两秒发现事情不简单——这串 27 位的数字根本不是随机噪声而是三个整整齐齐的连续重复段8 个 1、9 个 5、10 个 9。处理任何来路不明的字符串我的习惯都是先做最笨的统计数一数每个字符出现几次有没有连续重复的片段这些片段按什么顺序排列。先不急着猜含义把它到底长什么样这件事彻底搞清楚后面才不会跑偏。拿这串数字来说统计结果一眼就能看明白数字出现次数连续成段的长度占比18829.6%59933.3%9101037.0%三个数字的出现次数和单独成段的长度完全一致这说明整串字符串的分段结构和总频次是一回事。真正的随机数很难长成这样连续 8 个 1 的随机概率是 10⁻⁸ 量级再叠上连续 9 个 5、连续 10 个 9概率低到可以忽略。所以基本可以断定这串数字是被人为构造出来的而不是键盘误碰或者随手乱按。1.2 分段的顺序也藏着信息光有重复还不够顺序同样重要。三段依次是 1、5、9而不是 9、5、1 或者 5、1、9。把每个重复段看成整体整个字符串就是11111111 | 555555555 | 9999999999三段接龙段内是同一个数字段与段之间的数字按某种规律切换。这种先重复、后切换、再重复的结构在信息论里叫游程结构run-length structure也是**游程编码RLE**的天然产物。一段连续相同的字符就是一个 run而这串 27 位数字可以被压缩成一个非常短的描述1 重复 8 次5 重复 9 次9 重复 10 次。注意连重复次数本身都像是有规律的我下一章专门拆这一点。2. 两层等差数列1→5→9 和 8→9→102.1 数字部分公差为 4 的等差数列把每段的代表数字拎出来1、5、9。相邻两项之差都是 4这是一个干净的等差数列。为什么是 1、5、9而不是 1、5、8因为等差数列意味着匀速推进5 刚好是 1 和 9 的平均数9 又是 54。这种设计感在纯手工打出来的字符串里几乎不可能出现。人眼对这种等差推进其实非常敏感。心理学上有个现象叫顺势感知大脑会自动补全规律看到 1、5 就预期 9看到 8、9 就预期 10。这串数字能被一眼锁定恰恰因为它迎合了人对规律的预期。顺着这个规律继续推下一段的代表数字应该是 1394。这里冒出一个很有意思的问题1、5、9 都是单个字符而 13 是两位数。如果继续按每段重复 11 次来扩展写出来就不是13 个 13而是一串1313...的交替序列原有的连续段视觉结构瞬间就破了。也就是说这套规律在个位数范围内是自洽的跨进两位数之后就变得不再好看。做编码规则设计的人对这种边界条件应该格外敏感。2.2 段长部分公差为 1 的等差数列再看每段的长度8、9、10。同样是个等差数列公差为 1。两套等差数列叠在一起整串数字就可以用一句话描述以 1 开头每次数字加 4、重复次数加 1连写三段。这句话才是这串数字真正的源码27 位可见字符串只是它的一次实例化。工程上这叫模板化生成只要给定规则程序就能无限造出类似的数据。写出来也就是一行 Pythonparts [] d, n 1, 8 for _ in range(3): parts.append(str(d) * n) d 4 n 1 print(.join(parts)) # 1111111155555555599999999999这里有个值得记住的思维切换看到重复结构先问生成规则是什么而不是这段话是什么意思。很多协议字段、序列号、优惠码本质上都是规则 参数的产物解开了规则数据就不再神秘。2.3 顺着规律外推会得到什么按规则继续推第四段应该是13 重复 11 次。如果把 13 当成一个符号重复 11 次输出是131313131313131313131311 组共 22 位拼上原来的 27 位整串变成11111111555555555999999999991313131313131313131313。这个外推实验说明两件事。第一识别规律和用规律生成数据是同一枚硬币的两面只要找到了生成规则后续内容就完全可预测这在数据解压、协议字段解析、测试数据构造里都是核心思想。第二任何规则都有适用范围跨过个位数边界后原来的漂亮结构会退化。这也是为什么校验位算法、编码协议设计时都会严格限制位数和进制——规则越紧结构越稳。3. 这些重复数字在数学上有什么脾气3.1 身世repunit 与 repdigit全部由同一个数字组成的整数数学上叫repdigitrepeated digit 的合成词其中全由 1 组成的又叫repunit。11111111 就是第 8 个 repunit通常记作 R₈通项公式是 Rₙ (10ⁿ - 1) / 9。验证一下10⁸ - 1 99999999除以 9 正好是 11111111。我们这串数字的三段分别是 1×R₈、5×R₉、9×R₁₀相当于一个repunit 家族的缩放版。repunit 之所以被反复研究是因为它们身上背着大量整除规律而整除特性恰恰是判断一个数字有没有被精心构造的快速试纸。3.2 整除脾气速查谁和这三段合得来判断一个数能不能被 11 整除有个经典技巧从右往左奇数位数字之和减去偶数位数字之和差是 11 的倍数则可整除。对 8 位的 111111111-11-11-11-1 00 是 11 的倍数所以能被 11 整除——实际一算11111111 ÷ 11 1010101干干净净。再看 555555555。它是 9 位数奇数位比偶数位多一个 5交替和是 5所以不能被 11 整除。但它有另外两个明显特征以 5 结尾能被 5 整除数位和是 9×5 45是 9 的倍数所以能被 9 整除。555555555 ÷ 9 61728395一样是整除。最后看 9999999999。10 位全 9 数交替和是 0能被 11 整除9999999999 ÷ 11 909090909。同时数位和 90 是 9 的倍数也能被 9 整除9999999999 ÷ 9 1111111111。把三段的整除属性汇总一下数字长度数位和交替和能整除的数111111118801155555555594555、99999999999109009、11这里还藏着一个一般规律10ⁿ - 1也就是 n 个 9必定被 9 整除n 为偶数时因为交替和恰好是 0还必定被 11 整除。三段里两段撞上 11两段撞上 9。与其说是巧合不如说是长度和数字都被等差数列约束之后自然长出来的属性。3.3 整串 27 位反而不完美了把三段拼回一整串27 个数位之和是 8×1 9×5 10×9 143。143 11×13而它的数位和 1438说明整串既不能被 3 整除也不能被 9 整除。用 Python 验证也就是一句话s 1111111155555555599999999999 total sum(int(c) for c in s) print(total, total % 3, total % 9) # 143 2 8这个结果很有意思三段各自都有漂亮的整除属性合并成一个整数之后反而不完美了。它提醒我分析数据时分段统计和总体统计要分开做局部的规律未必能简单叠加成更大的规律。4. 用代码解剖正则、groupby 与规律校验4.1 正则一行拿下所有连续段工程上提取这类连续段最常用的是正则。核心表达式是(\d)\1*\d匹配一个数字括号把它捕获成组 1\1反向引用组 1*表示贪婪匹配后面所有相同字符。注意要用finditer而不是findall——findall在有捕获组时默认返回的是分组内容而不是整个匹配import re s 1111111155555555599999999999 runs [m.group(0) for m in re.finditer(r(\d)\1*, s)] print(runs) # [11111111, 555555555, 9999999999]这个写法在处理日志、解析协议数据时非常常用。比如从混合文本里提取所有被重复的字符段或者判断一段数据里有没有高重复区域一行就能搞定。我自己在排查某个字段是不是被错误地填充成了同一个值这类问题时也常拿它做第一道筛子。4.2 groupby 方案不背正则也能拆如果对反向引用不熟标准库itertools.groupby是更直白的方案。它的语义就是把相邻且相等的元素合并成一组from itertools import groupby s 1111111155555555599999999999 runs [.join(g) for _, g in groupby(s)] print(runs) # [11111111, 555555555, 9999999999]groupby返回 (key, iterator) 对key 是当前组的代表元素iterator 是该组里的连续元素列表用.join(g)拼回字符串即可。两种方案怎么选我的经验是正则适合从大文本里抓特定模式groupby 适合把整个字符串按相邻重复分段可读性更好也不容易踩捕获组的坑。处理测试数据时我绝大多数时候写 groupby因为几乎不可能写错。4.3 把看起来是规律变成脚本证据前面说了那么多规律真正严谨的做法是写代码验证而不是拍脑袋说这看着像等差数列。下面的代码把分段、取代表数字、取段长、判断等差、验证整除一次做完from itertools import groupby s 1111111155555555599999999999 runs [.join(g) for _, g in groupby(s)] digits [int(r[0]) for r in runs] lengths [len(r) for r in runs] def is_arith(seq): if len(seq) 2: return True d seq[1] - seq[0] return all(seq[i] - seq[i - 1] d for i in range(2, len(seq))) print(runs) # [11111111, 555555555, 9999999999] print(digits, is_arith(digits)) # [1, 5, 9] True print(lengths, is_arith(lengths)) # [8, 9, 10] True n1, n5, n9 (int(r) for r in runs) print(n1 % 11, n5 % 9, n9 % 11, n9 % 9) # 0 0 0 0这套先提出假设再转成可判定条件最后跑脚本验证的流程几乎可以套用到任何不明格式的数据上。养成这个习惯之后遇到神秘字符串就不会再靠猜。5. 真实工程里这种字符串到底从哪来5.1 测试数据里的边界怪胎长串重复字符在测试领域太常见了。接口测试、正则性能压测、存储边界测试经常需要造极端输入而1*8 5*9 9*10这种模板可以几秒钟生成大量有规律、易辨认的样本。如果某个测试用例里出现这类字符串多半不是随手打的而是在测长输入截断重复字符匹配性能序列化长度上限这些点。我之前排查过一个诡异问题某模块把 27 位以内的输入当成无害短串跳过检查而测试组正好塞进来一个 27 位的高重复数字串把边界条件打穿了。后来翻数据才发现那是用9*27之类的模板批量生成的压测样本。这种长度 规则双重卡边界的数字串就是专门用来踩阈值的。5.2 密码安全的反面教材重复数字字符串在密码世界里名声很差。历次泄露的密码榜单里111111000000123456这类结构常年霸榜原因很简单人脑容易记住按住同一个键攻击者的字典里也正好收录了所有重复数字 简单递增的组合。别觉得1111111155555555599999999999有 27 位就安全。它的生成规则一旦被看穿——数字是 1、5、9 的等差数列长度是 8、9、10 的等差数列——整串就能被预测。密码安全从来不只看长度更看不可预测性也就是信息论里的熵。一个能被简单规则压缩的密码熵低得吓人和短但乱的随机密码完全不在一个安全级别。真要生成高强度密码老老实实用密码管理器生成的随机串比任何有规律的超长数字都靠谱。5.3 培养字符串直觉先统计再解码最后下结论最后聊点习惯层面的事情。收到来路不明的字符串我的默认流程永远是三步先统计频次、连续段、长度分布再解码正则、分组、进制换算、校验位计算最后才决定是丢弃还是深入解析。这套流程帮我避开了无数次过早下结论的坑——很多人拿到一串数字就开始猜含义方向猜错了后面全是白费。个人体会识别设计过的结构和真正的随机噪声之间的区别是工程直觉里很值钱的一部分。这串数字如果不是刻意构造的几乎不可能同时满足两套等差数列、三段又都撞上整除特征。但反过来遇到真随机字符串也别硬凑规律——过拟合是分析数据时最容易犯的错。判断标准只有一个你找到的规则能不能稳定地预测下一段数据。能预测才是真规律。
阅读完成 · 觉得有帮助?
咨询建站