開篇:夜色漸濃,佳人亦在
那天晚上,辦公室的燈已經(jīng)滅了大半,只剩幾個(gè)工位發(fā)出輕輕的藍(lán)光。中央空調(diào)早就熄了,但顯示器的熱度依然在屏幕前形成一圈圈淡淡的光暈。
我坐在靠窗的位置,剛把代碼提交推送完,正打算收鍵盤走人。
這時(shí),小語(yǔ)走過(guò)來(lái),端著還冒著熱氣的速溶咖啡——她果然又是那個(gè)留下來(lái)最晚的人之一。
“誒~”她蹲在我旁邊的桌子邊上,語(yǔ)氣帶著一絲挫敗,“你這邊有沒(méi)有遇到 JSON 字符串明明格式看著沒(méi)錯(cuò),卻死活 JSON.parse
不過(guò)的情況?”
一個(gè)普通的錯(cuò)誤,卻不是普通的崩潰
原來(lái)她在調(diào)試一個(gè)用戶日志上傳模塊,前端接收到的日志數(shù)據(jù)是從后端來(lái)的 JSON 字符串。
問(wèn)題出在一個(gè)看似再平常不過(guò)的解析操作上——
const logData = JSON.parse(incomingString);
可是控制臺(tái)總是報(bào)錯(cuò):Unexpected token
。數(shù)據(jù)一眼看去也沒(méi)問(wèn)題,{'name': 'Tom', 'age': 30}
—— 結(jié)構(gòu)清晰,屬性齊全,但偏偏就是“壞掉了”。
她抿了一口咖啡,苦笑,“我知道是引號(hào)的問(wèn)題,可這種數(shù)據(jù)是從破舊的系統(tǒng)里吐出來(lái)的,量還特別大,我不可能一個(gè)個(gè)手動(dòng)改?!?/p>
風(fēng)起 · JSON.parse 不是萬(wàn)靈藥
我們一起回顧了她的實(shí)現(xiàn)方式。她用的是最基礎(chǔ)的 JSON.parse()
,這是我們?cè)陧?xiàng)目里默認(rèn)的處理方式——簡(jiǎn)單、直接、快速。
但這個(gè)方法對(duì) JSON 格式的要求極其嚴(yán)格:
- 只能使用雙引號(hào)
"
- 屬性名必須加引號(hào)
- 不容忍任何額外字符或注釋
一旦出現(xiàn)諸如單引號(hào)、缺少逗號(hào)、多余空格這些“微小過(guò)失”,就直接拋錯(cuò)了。
小語(yǔ)嘆氣,“很多時(shí)候這些 JSON 是設(shè)備端拼出來(lái)的,不規(guī)范,又沒(méi)有錯(cuò)誤提示,我根本不知道該怎么修?!?/p>
我翻了翻之前的代碼,從夾縫中找出來(lái)一張破舊的黃皮紙,我們倆一起瞅了上去,看到上面寫著
function tryParseJSON(jsonString) {
try {
return JSON.parse(jsonString);
} catch (e) {
// 嘗試簡(jiǎn)單修復(fù):去除可能的多余字符
const cleaned = jsonString.replace(/[^\x20-\x7E]/g, '').trim();
try {
return JSON.parse(cleaned);
} catch (e2) {
console.error("無(wú)法解析JSON:", e2);
return null;
}
}
}
下面?zhèn)渥⒘艘恍行∽郑?span style="font-weight: 700; color: rgb(3, 106, 202);">此法在一些更輕量的場(chǎng)景里,做一些“簡(jiǎn)陋修復(fù)“,對(duì)于簡(jiǎn)單的問(wèn)題有時(shí)能奏效,但對(duì)于更復(fù)雜的錯(cuò)誤,比如混合了單引號(hào)和雙引號(hào)的情況,只能再實(shí)現(xiàn)另一個(gè)方法可以做更針對(duì)性的修復(fù)方法:
function fixQuotes(jsonString) {
// 將單引號(hào)替換為雙引號(hào)(簡(jiǎn)單情況)
return jsonString.replace(/'/g, '"');
}
小語(yǔ)感嘆一聲:“沒(méi)有更好的了嗎?”
解決篇 · 來(lái)自大佬的一句話
恰好這時(shí),阿杰從會(huì)議室出來(lái),耳機(jī)還掛在脖子上。
他聽了一耳朵后隨口說(shuō)了句:“你們?cè)囘^(guò) jsonrepair
嗎?那玩意能把壞 JSON 修回來(lái),就像修車?!?/p>
“json... repair?”小語(yǔ)一臉困惑。
我忽然想起,之前有個(gè)日志監(jiān)控服務(wù)也碰到類似的問(wèn)題,當(dāng)時(shí)就是用了這個(gè)庫(kù)一把梭。
我打開編輯器,快速翻出來(lái)了這一段:
npm install jsonrepair
const { jsonrepair } = require('jsonrepair');
const damaged = "{name: 'John', age: 30}";
const fixed = jsonrepair(damaged); // => {"name":"John","age":30}
const obj = JSON.parse(fixed);
小語(yǔ)湊過(guò)來(lái)看了一眼,眼睛一亮:“它真的把引號(hào)補(bǔ)好了?”
我點(diǎn)頭。這個(gè)工具是為了解決類似“非標(biāo)準(zhǔn) JSON”問(wèn)題的,它會(huì)盡可能地補(bǔ)全缺失引號(hào)、逗號(hào),甚至處理 Unicode 異常字符。
當(dāng)然,也不是所有情況都適用。
比如碰到亂碼或者非法嵌套結(jié)構(gòu),jsonrepair
有時(shí)也會(huì)無(wú)能為力。這時(shí)可以退一步——用更寬松的解析器,比如 JSON5
:
const JSON5 = require('json5');
const result = JSON5.parse("{name: 'John', age: 30}"); // 也能解析
我看著認(rèn)真學(xué)習(xí)的小語(yǔ),語(yǔ)重心長(zhǎng)的講道:它不是修復(fù),而是擴(kuò)展 JSON 標(biāo)準(zhǔn),讓一些非標(biāo)準(zhǔn)寫法也能解析(JSON5 能容忍的內(nèi)容包括:?jiǎn)我?hào)、尾逗號(hào)、注釋、未加引號(hào)的屬性名、十六進(jìn)制、科學(xué)計(jì)數(shù)法等數(shù)字格式),
接著我們還討論了更復(fù)雜的修復(fù)方式,比如用正則處理批量日志,甚至用 AST 工具逐步構(gòu)建 JSON 樹。但那是更遠(yuǎn)的故事了。
面對(duì)當(dāng)前的問(wèn)題,我們準(zhǔn)備搞一套組合拳:
function parseJson(jsonString) {
// 第一步:嘗試標(biāo)準(zhǔn)JSON解析
try {
return JSON.parse(jsonString);
} catch (e) {
console.log("標(biāo)準(zhǔn)JSON解析失敗,嘗試修復(fù)...");
// 第二步:嘗試使用jsonrepair修復(fù)
try {
const { jsonrepair } = require('jsonrepair');
const fixedJson = jsonrepair(jsonString);
return JSON.parse(fixedJson);
} catch (e2) {
console.log("修復(fù)失敗,嘗試使用JSON5解析...");
// 第三步:嘗試使用JSON5解析
try {
const JSON5 = require('json5');
return JSON5.parse(jsonString);
} catch (e3) {
// 最后:如果所有方法都失敗,返回錯(cuò)誤信息
console.error("所有解析方法都失敗了:", e3);
throw new Error("無(wú)法解析JSON數(shù)據(jù)");
}
}
}
}
結(jié)局
一段時(shí)間后,小語(yǔ)在前端監(jiān)控日志里貼了段截圖:原本一天上千條的 parse error
錯(cuò)誤,幾乎消失了。
她補(bǔ)了一句:“終于不用再一個(gè)個(gè)點(diǎn)開調(diào)日志了?!?/p>
我回頭看她的工位,屏幕亮著,瀏覽器里是一個(gè)模擬器頁(yè)面,console 正在緩緩輸出內(nèi)容。
她突然抬起頭看著我,問(wèn)道:“AST是什么?聽說(shuō)也能實(shí)現(xiàn)json修復(fù)?”