正则暴力拆解:如何从 10 层嵌套的 JSON 屎山中光速提取所有 Key?
糟糕的开头
凌晨两点,群里的后端发来一个 5MB 的 JSON 响应报文,长得像俄罗斯套娃,嵌套整整 10 层!
“兄弟,帮我把里面所有的 field_name 都提取出来,我要做个映射表。”
面对这坨屎山,一般的做法是什么?写个递归函数,一层层去遍历解析?说实话,光想想那 Maximum call stack size exceeded 的报错,我就已经开始犯恶心了。
我的思考
为什么不写递归?在浏览器端,尤其是像 daima.life 这样主打“纯净、极速”的纯前端工具(结合 Cloudflare Pages 部署),内存和执行效率就是生命。如果真的去用 JSON.parse 然后递归遍历 5MB 且 10 层深的对象,大概率会让主线程卡死 2 秒以上,用户体验直接拉胯。
我的核心架构理念一直是 Privacy-First (离线可用),所以不可能扔给服务端去算。
既然目标仅仅是提取 Key,那为什么要反序列化整个字符串?把对象当成纯文本,用一把锋利的快刀(正则表达式)直接顺劈下去,不是更爽吗?
技术硬核区:不讲武德的正则提取
要在原始的 JSON 字符串中精准拿到所有 Key,并且排除掉那些其实是 Value 的字符串,我们需要观察 JSON 中 Key 的特征:它一定是被双引号包裹,并且紧跟着一个冒号(或冒号前有空格)。
核心的匹配正则如下:
const extractKeys = (jsonString) => {
// 匹配规则: "key" 紧跟着 0 或多个空白符,再加上一个冒号
const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/g;
const keys = new Set();
let match;
while ((match = regex.exec(jsonString)) !== null) {
keys.add(match[1]); // 提取的分组就是 Key 的裸名
}
return Array.from(keys);
};
细节拆解
"[^"\\]*": 匹配大部分没有转义字符的双引号字符串。(?:\\.[^"\\]*)*: 这是一个无捕获分组,用来处理 Key 本身包含被转义的特殊字符(如\"或\\\\)的极端情况。\s*:: 这是灵魂所在!只有后面紧跟着冒号的字符串,才可能是 JSON 的 Key。
这套正则避开了将 5MB 文本转化为内存消耗惊人的 AST 树或 JS 对象,直接把时间复杂度降维打击。在 10 层嵌套下,正则依然如履平地,秒开真香!
FAQ 模块
Q1:正则的 (?:\\.[^"\\]*)* 怎么理解?会不会导致正则回溯灾难 (Catastrophic Backtracking)?
无捕获分组 (?:...) 配合占有型匹配结构,确保了在遇到大量转义字符时,状态机不会陷入没完没了的回溯。这里由于没有互相重叠的可选路径([^"\\]* 互斥于 \\.),所以是极为安全的线性匹配。
Q2:如果 JSON 的 Value 也是一段包含 "key": 的字符串(比如被序列化的内层 JSON),这套正则是不是就翻车了?
好问题。这就是正则纯文本解析的盲区!如果你确信有大量被 stringify 且包含冒号的值混入,这招确实会误杀。此时你应该祭出基于流处理器(如 JSONStream)在词法分析(Lexer)阶段去提取,而不是纯正则表达式。这就是极简付出的代价。
Q3:为什么使用 Set 来收集结果?
嵌套屎山里,Key 最容易重复出现(比如各个层级都有 id、createdAt)。用 Set 自动去重,比用 Array 去 includes 检查性能高太多了,Set.add() 是 O(1) 的。
结尾展望
靠正则手撕 JSON 终究是走钢丝的极客玩法。未来的 Web 工具,边缘计算 (Edge Computing) 会将这类脏活累活以毫秒级延迟处理掉,再传回客户端。或许到了 2027 年,我们压根连正则都不用写,直接喂给设备端的 NPU 跑一个小模型解析了。但现在嘛,这段 10 行的高效正则,依旧是我的心头好……