上個月替人查一個起不來的服務。設定是他手寫的 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 |
| 字串裡裸換行 | | 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 縮排 | 3815 | 1357 | 35.6% |
| 2 空格縮排 | 4744 | 2286 | 48.2% |
| 4 空格縮排 | 6602 | 4144 | 62.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 解析器,再拿行列號往前回一格,然後按給誰讀決定縮排,最後只有非塞進字串不可時才疊轉義。