雜記 › 技術筆記 › JSON 少一個逗號查半天?錯誤定位、壓縮與轉義

JSON 少一個逗號查半天?錯誤定位、壓縮與轉義

上個月替人查一個起不來的服務。設定是他手寫的 JSON,他刪掉其中一個節點,程式只回一句 syntax error,不給行號;他從頭掃了三遍,看不出少了哪枚標點。同一份檔案交給 Python 3.13 的 json 模組,才拿到完整一句:Illegal trailing comma before end of object: line 5 column 22 (char 93)。真兇在第 5 行末尾。

{
    "upstream": "10.0.3.7:8080",
    "timeout": 30,
    "retries": 3,
    "keepalive": true,
}

另一半是介面回傳壓成一行,兩千多字元擠在一起,找一個鍵的值時眼睛會跟著滑走。

能跑通的 JavaScript 寫法,多半不是合法 JSON

JSON 是從 JavaScript 物件字面量裡裁出來的嚴格子集,裁掉的四樣恰是最常寫回去的:單引號包字串、裸識別字做鍵名、末尾掛逗號、寫註解。我拿 Node 24 驗過,eval 執行 ({'a': 1, /* c */ b: 2,}) 能拿到物件,JSON.parse 卻從第 1 行第 2 列就拒絕,指的正是那個單引號。本機腳本走的是寬容的 JS 解析器,服務載入走的是嚴格的 JSON 解析器。

下表每條錯串我都照敲過一遍。Python 3.13 把它們都報在第 1 行,所以表裡只留 column;V8 措辭不同但位置接近。

寫法問題錯誤片段Python 3.13 的報錯
尾隨逗號{"a": 1,}Illegal trailing comma before end of object: column 8
鍵名用單引號{'a': 1}Expecting property name enclosed in double quotes: column 2
用註解掉一行{"a": 1 /* note */}Expecting ',' delimiter: column 9
字串裡裸換行
{"a": "line1
line2"}
Invalid control character at: column 13
括號沒配平{"a": [1, 2}Expecting ',' delimiter: column 12

另外兩條:數字寫成 NaN 或帶前導零,Python 自己讀寫得回去,交給前端就炸。還有一種是傳輸錯,Content-Type 寫成 text/plain,部分客戶端不做自動解析,檔案本身其實合法。肉眼配括號太慢,我先讓解析器報位置:JSON 格式化 輸入時就給出錯行列,能按 2 空格、4 空格或 Tab 攤開,也能壓回單行,樹狀展示可逐層摺疊。

行列號怎麼讀,壓成一行為什麼定位不動

除行列外,Python 還給一個 0 基的 char 偏移:行數是偏移之前的換行數加一,列數是偏移減去上一個換行的位置。一條 37 位元組的字串 {"nodes":["a","b","c",],"timeout":30},Python 報 char 21,即第 1 行第 22 列,正落在逗號後那個右方括號上。

開頭那份檔案裡,同一內容 Python 報 line 5 column 22,指著該刪的逗號,V8 報 position 95、line 6 column 1,已跳到下一行。報錯點標的是解析器讀不下去的第一格,該改的往往是它前面那個元素,所以我拿到行列號先往前回一格。壓成一行時行數永遠是 1,列號動輒上千,得先攤成多行才有意義。沒人說得清改了哪一行時,讀報錯不如把前後兩份文字做 文字比對。

壓縮省掉什麼:按我的範例算一遍

那句減小約 30% 到 50% 體積的說法我不敢照抄,壓縮刪掉的只有空白,鍵名和值一個位元組不會少。我構造了一份範例:共 191 鍵、物件套 5 層,值混合字串、整數、小數、布林和 null,用 PHP 編成四種形態實測。

形態位元組數比壓縮單行多佔自身體積
壓縮為單行2458無無
Tab 縮排3815135735.6%
2 空格縮排4744228648.2%
4 空格縮排6602414462.8%

4144 能拆開核對:行首空白 3716、換行 237,剩下的 191 正好等於鍵數,因為美化器在每個冒號後補一個空格,資料本體 2458 卻沒變。換 Tab 後行首空白只剩 929,一個 Tab 就一位元組。自己數可以造十行:每行 4 個空格、11 個換行,格式化 145、壓平 94,差的 51 正是 40 加 11。

比例只對這份範例成立:同一份內容 Tab 版多三成五,4 空格版近六成三,是前者的 1.731 倍。要不要壓平我只看它給誰讀:人要改的設定留縮排;回應主體省頻寬靠 gzip,跟空白不是同一層手段;只有整段要當字串塞進程式碼時壓平才省事,那樣每個換行都得寫成 \n,壓平後轉義少一類。

把 JSON 當字串塞進程式碼,轉義是按層疊的

字串內四種字元要改寫:雙引號寫成反斜線加雙引號,反斜線寫成兩個反斜線,換行寫成反斜線 n,定位字元寫成反斜線 t。翻車在層數,日誌裡的 payload、表單傳上來的 body 都是 JSON 套 JSON,套幾層就得轉義幾次。拿一個 5 位元組的值連套三層,逐字元可核對:

套 1 層:{"v":"a\"b\\c"}
套 2 層:{"p":"{\"v\":\"a\\\"b\\\\c\"}"}
套 3 層:{"w":"{\"p\":\"{\\\"v\\\":\\\"a\\\\\\\"b\\\\\\\\c\\\"}\"}"}

反斜線從 3 到 11 再到 31,整串 15、31、59 位元組,最後一行裡原始那 5 個字元已看不出形狀。換成一句帶 ASCII 雙引號和 Windows 路徑的中文值,值本體 47 位元組,套到第三層整串 129 位元組、46 個反斜線:每多套一層,已有的反斜線又要再轉義一次。手工解到第三層我會數錯,所以解一層核一層,解完跟原始值比對。要從日誌裡挑這類字串,我先用 正規表示式測試 匹配引號邊界和成對反斜線,再交給解析器。

幾個被反覆問到的

Q:報錯只給了偏移量,怎麼換算出行列?

偏移是 0 基位置,行數等於它之前的換行數加一,列數等於它減去上一個換行的位置;只有一行時列號就是偏移加一。

Q:我就註解掉一行,為什麼整個檔案都報錯?

JSON 沒有註解語法,報錯點常落在這行值末尾或下一個鍵名開頭,看著像值寫錯了。要留說明只能做成一個普通鍵。

Q:壓縮到底能省多少體積?

按上面那組數:191 鍵、5 層的範例,Tab 多 35.6%、2 空格多 48.2%、4 空格多 62.8%,多的全是空白。給不出範例的百分比可以跳過。

Q:把生產設定貼進去校驗,內容會不會傳到伺服器?

這類檔案常帶內網位址和金鑰,我只用解析全在瀏覽器本機完成的那類,前面那個入口就是本機解析。

回到那個起不來的服務:改動只是刪掉第 5 行末尾一枚逗號,功夫全在讓解析器把位置說清楚。順序也就固定了:先確認讀它的是嚴格 JSON 解析器,再拿行列號往前回一格,然後按給誰讀決定縮排,最後只有非塞進字串不可時才疊轉義。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22