← 全部文章

XML 轉 JSON:屬性、文字節點、陣列與命名空間

正確地把 XML 轉成 JSON:屬性、文字節點、重複元素、命名空間如何對應 —— 附約定、邊界情況以及 JS / Python 程式碼。

把 XML 轉換為 JSON 聽起來很機械,但你很快就會撞上 XML 中 JSON 沒有直接對等物的部分:屬性、文字與元素混合的內容、出現一次或多次的元素、以及命名空間。這裡沒有唯一「正確」的對映 —— 只有慣例。本指南講解標準慣例、你必須做的決定,以及如何在 JavaScript、Python 與瀏覽器中把 XML 轉為 JSON。

對映一覽

多數 XML 轉 JSON 工具(xmltodict、fast-xml-parser,本站工具也一樣)都遵循同一種形態:每個元素變成一個物件,屬性變成帶特殊前綴的鍵,文字要麼成為值、要麼放到一個保留鍵裡。

<!-- XML -->
<note id="1" priority="high">
  <to>Ada</to>
  <from>Bob</from>
  <body>Hello &amp; welcome</body>
</note>
// JSON
{
  "note": {
    "@id": "1",
    "@priority": "high",
    "to": "Ada",
    "from": "Bob",
    "body": "Hello & welcome"
  }
}

屬性 → @ 前綴鍵

JSON 物件沒有屬性的概念,因此一個近乎通用的慣例是給屬性名稱加上 @ 前綴。這樣它們與子元素能區分開來,對映也可以反向。

<book id="b1" lang="en"/>
→ { "book": { "@id": "b1", "@lang": "en" } }

有些工具用別的前綴($_),或用一個巢狀的 "@attributes" 物件。挑一種就一以貫之 —— 下游程式需要知道屬性在哪。

文字節點與混合內容

當一個元素包含文字時,它會塌縮為一個字串值。但當元素同時擁有屬性與文字時,文字需要一個去處 —— 約定俗成的位置是鍵 #text

<price currency="USD">9.99</price>
→ { "price": { "@currency": "USD", "#text": "9.99" } }

<title>Effective TypeScript</title>
→ { "title": "Effective TypeScript" }

真正的混合內容(文字與子元素交錯出現,像 HTML 一類的標記)是最棘手的情況 —— 多數資料導向的轉換器會拼接或丟棄零散的文字。如果你的 XML 是文件風格而非資料風格,預計這裡會有損。

單一 vs 陣列 的問題

這是比其他都更容易把程式碼害死的雷。一個出現一次的元素會變成物件;同一個元素出現兩次就變成陣列。所以 JSON 的形態取決於資料,而不是 schema:

<tags><tag>a</tag></tags>
→ { "tags": { "tag": "a" } }          // 物件

<tags><tag>a</tag><tag>b</tag></tags>
→ { "tags": { "tag": ["a", "b"] } }   // 陣列

期望 tags.tag 始終是陣列的下游會在單項情況下崩潰。兩個解法:把解析器設定成對已知可重複的元素總是當成陣列處理,或者在解析之後做正規化(const arr = [].concat(node.tag ?? []))。

命名空間

XML 命名空間使用前綴(soap:Envelope)並透過 xmlns 宣告繫結。JSON 沒有命名空間概念,所以轉換器通常採用以下做法之一:

  • 把前綴保留在鍵裡 —— "soap:Envelope"。簡單且可反向,但鍵裡含冒號,需要以方括號存取(obj["soap:Envelope"])。
  • 丟掉前綴 —— "Envelope"。鍵更乾淨,但你會失去命名空間,而且兩套使用同一個本地名的命名空間之間可能會撞鍵。
  • xmlns 當屬性保留 —— 宣告變成 "@xmlns:soap" 之類的鍵,使繫結能在往返中存活。

對多數資料任務而言,把前綴保留在鍵裡是最安全的預設 —— 它永遠不會遺失資訊。

實體與 CDATA

正確的轉換器會把五個預定義實體(&lt;&gt;&amp;&quot;&apos;)以及數字引用(&#169;)解碼為對應字元,並把 <![CDATA[...]]> 區塊視為字面文字。

慣例的名字:BadgerFish、GData、Parker

「屬性用 @、文字用 #text」並不是江湖上唯一的方案。讀其他系統的 XML→JSON 輸出時,會遇到這三個有名字的慣例:

  • BadgerFish —— 屬性放在以 @ 為前綴的鍵之下;文字放到 $;命名空間宣告放到 @xmlns。囉嗦但無損。
  • GData —— Google 的變體:屬性帶 $ 前綴;文字放在 $t 之下;重複元素總是變成陣列。無損且形態可預期。
  • Parker —— 完全捨棄屬性;最簡單也最有損的對映。在你同時掌控兩端、只關心元素值時有用。

在與一個已經把 XML 轉成 JSON 的系統整合時,請先辨識它使用哪一種慣例,再寫解析程式碼。

用 JSONPath 查詢轉換結果

一旦 XML 被轉好,就可以用 JSONPath 來尋址值。相較於 XPath 的習慣,兩處小調整:

  • 屬性鍵帶著對映中的 @ 前綴,所以 XPath 的 @id 在 JSONPath 裡是 $..['@id']
  • 上面提到的「單一 vs 陣列」意味著像 book/title 這樣一條本來能用的 XPath,在 JSONPath 裡可能需要寫成 $..book[*].title 才能相容兩種形態。

用程式碼把 XML 轉為 JSON

// JavaScript(瀏覽器) —— DOMParser + 一個小走樹器,或者一個函式庫:
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@' });
const obj = parser.parse(xmlString);

# Python —— xmltodict 會把屬性對映到 "@name",文字對映到 "#text"
import xmltodict, json
doc = xmltodict.parse(xml_string)
print(json.dumps(doc, indent=2))

線上把 XML 轉為 JSON

想快速轉換,把 XML 貼進 JSON ⇄ XML 轉換器 並點擊 To JSON。它套用上述慣例 —— 屬性用 @、混合內容用 #text、重複元素用陣列 —— 並完全在你的瀏覽器中執行,因此內部資料流與 API 負載永遠不會離開你的機器。

常見問題

XML 屬性在 JSON 中如何表示?

依慣例,它們變成以 @ 為前綴的鍵(例如 @id),與子元素區分開來,使對映可以反向。

為什麼同一個元素有時變成物件、有時變成陣列?

因為形態跟隨資料:一次出現對映為物件,多次出現對映為陣列。請把解析器設定成對已知可重複的元素一律按陣列處理,或者在解析之後用 [].concat(value) 正規化。

同時擁有屬性的元素的文字去了哪裡?

放到保留鍵 #text 之下,因為物件已經容納了屬性。只有文字的元素則塌縮為一般字串。

XML 命名空間怎麼處理?

JSON 沒有命名空間。最安全的做法是把前綴保留在鍵裡("soap:Envelope"),同時把 xmlns 宣告作為 @xmlns:* 屬性保留,這樣什麼都不會遺失。

相關工具與指南