你好,我是四火。

XML 和 JSON,是程序员几乎每天都会打交道的数据、特别是配置数据的承载格式。我想你心里应该有一个大致的印象,它们二者各有什么优劣,但是也许没有系统地比较过。那今天我们就把它们放到一起,丁是丁卯是卯地分析分析,对比一下它们各自的特点。另外,这些年来,对于配置,特别是复杂 DSL 的配置,YAML 也逐渐流行开来,因此我们也把它拿过来剖析一番。

XML 和 JSON 的比较

XML 全称叫做 Extensible Markup Language,就像 HTML、CSS 一样,是一种标记语言(标记语言不属于传统意义上的编程语言),且是一种具备结构化特征的数据交换语言;类似地,JSON,也就是 JavaScript Object Notation,被称作 JavaScript 对象表示法,非结构化,更轻量,但归根结底也是一种数据交换语言。因此,二者具备相当程度的相似性,在实际应用中,往往也可以比较和替代。

1. 简洁还是严谨

[第 04 讲] 的时候,我介绍了 REST 和 SOAP 这样一个简洁、一个严谨的俩兄弟。而在本讲中,JSON 和 XML 也在一定程度上同样满足这样的比较关系,JSON 往往是更为简洁、快速的那一个,而 XML 则更为严谨、周全。

我们来看一个简单的例子,id 为 1 的城市北京:

<?xml version="1.0" encoding="UTF-8" ?>
<city>
  <name>Beijing</name>
  <id>1<id>
</city>

如果用 JSON 表示:

{
  "city": {
    "name": "Beijing",
    "id": 1
  }
}

你可能会说,除了 XML tag 的名字,在 JSON 中只需要写一遍以外,看起来复杂复杂、严谨的程度似乎也差不太多啊。

别急,往下看。XML 的结构,强制要求每一个内容数据,都必须具备能够说明它的结构,而 JSON 则没有这样的要求。比方说,如果我们把城市组成数组,用 XML 来表示,请你把这个文件存成 cities.xml,因为我们会多次用到这个文件:

<?xml version="1.0" encoding="UTF-8" ?>
<cities>
  <city>
    <name>Beijing</name>
    <id>1</id>
  </city>
  <city>
    <name>Shanghai</name>
    <id>2</id>
  </city>
</cities>

如果使用 JSON 的话,由于对于数组可以使用中括号直接支持,而不需要显式写出上例中的 city 这个 tag 的名称,请你同样建立 cities.json:

{
  "cities": [
    {"name": "Beijing", "id": 1},
    {"name": "Shanghai", "id": 2}
  ]
}

从这就可以看出,在这种情况下,JSON 似乎确实要更为简洁一些。上例中 JSON 能够使用中括号直接表示数组,能够直接支持数值、字符串和布尔型的表示。

等等,这样说的话,JSON 因为能够直接支持数值的表示,这个 id 没有双引号修饰,就是数值类型,可是从 XML 中并不能看出这一点啊。因此,从这个角度说,应该是 JSON 更严谨啊!那为什么说 XML 更严谨,严谨在哪呢?

有些程序员朋友可能会马上想到,XML 是可以定义 tag 属性的,预定义一个 type 不就好了?

<city>
  <name type="String">Beijing</name>
  <id type="number">1<id>
</city>

看起来也能达到“严谨”的目的,可这很可能就是一个不好的实践了,因为XML 对于这些常见的数据类型,内置了直接的支持。我们可以通过定义 XML Schema Definition(XSD)来对 XML 的结构做出明确的要求,也就是说,我们不必自己去造轮子,来定义并实现这个 type 属性。针对上面的 cities.xml,我们可以定义这样的 XSD:

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="cities">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="city" maxOccurs="unbounded" minOccurs="0">
          <xs:complexType>
            <xs:sequence>
              <xs:element type="xs:string" name="name"/>
              <xs:element type="xs:byte" name="id"/>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

这样一来,我们就对 cities 和 city 这两个 tag 做了严格的内容限定,包括包含的子节点有哪些,顺序如何,取值类型是什么等等。在实际的 XML 定义中,我们可以引用这个 XSD,这样 XML 的处理程序就会加载这个 XSD 并根据 schema 的规则对 XML 进行校验,从而发现 XML 不合要求的问题。

进一步地,你可以自己动动手,看一下这个工具,它可以帮助你通过 XML 快速生成样例 XSD;而这个工具则可以帮你快速验证 XML 是不是满足某 XSD 的要求,它们都很有用。

补充一下,你可能也听说过,或使用过类似的叫做 DTD,也就是 Document Type Definition 的方式,能想到这个很好,但是 XSD 相对来说有着更大的优势,并成为了 W3C 的标准。因此我在这里不提 DTD,但是我在扩展阅读中放了关于 XSD 和 DTD 的比较材料,供感兴趣的朋友拓展。

我想,从 XSD 你应该可以体会到 XML 的严谨性了。那喜爱使用 JSON 的程序员,就不能创造一个类似的东西,来填补这个坑——即定义和保证 JSON 的严谨性吗?

有,它就是 JSON Schema,也许你已经在项目中使用了,但是还没有统一成标准,也没有被足够广泛地接纳,因此我就不展开来说了。你可以自己实践一下,把上面提到的 JSON 填写到这个 JSON Schema 推断工具上面,去看看生成的 JSON Schema 样例。

2. JavaScript 一家亲

对于全栈工程师来说,和 XML 比较起来,JSON 对于前端开发来说,可以说有着不可比拟的亲和力。本来,JSON 就是 JavaScript 对象表示法,就是从 JavaScript 这个源头发展而来的,当然,JSON 如今是不依赖于任何编程语言的。这个“一家亲”,首先表现在,JSON 和 JavaScript 对象之间的互相转化,可以说是轻而易举的。

我们来动动手实践一下,打开 Chrome 的开发者工具,切换到 Console 页,打开前面建立的 cities.json,拷贝其中的内容到一对反引号(backtick,就是键盘上 esc 下面的那个按键)中,并赋给变量 text:

var text = `JSON 字符串`;

我们很轻松地就可以把它转化为 JavaScript 对象(反序列化),不需要任何第三方的 parser:

var obj = JSON.parse(text);

在早些时候的 ES 版本中,这个方法不支持,那么还可以使用 eval 大法,效果是一样的:

var obj = eval('(' + text + ')');

不过,在现代浏览器中,如果 text 不是绝对安全的,就不要使用这样的方法,因为 eval 可以执行任何恶意代码。

当然,我们也可以把 JavaScript 对象转换回(序列化)JSON 文本:

var serializedText = JSON.stringify(obj);

完成以后,先不要关闭控制台,下面还会用到。

3. 路径表达式

对于一段巨大的 XML 或 JSON 文本,我们经常需要找出其中特定的一个或多个节点(tag)、内容(content)或者属性,二者都有相似的方法。

对于 XML 来说,它就是 XPath,也是 W3C 的标准。现在我们来动手操作一下吧:

我们可以利用 Xpath Generator 来直观地生成相应的 XPath。让我们打开这个工具,找到我们刚才保存下来的 cities.xml 文件,拷贝里面的内容,粘贴到页面上。

接着,点击第一个 id 标签,你将看到这样的 XPath:

/cities/city[1]/id

这就唯一确定了 XML 中,cities 这个节点下,city 节点中的第一个,它下面的 id 节点。

我们再来试一试,点击 Shanghai 这个 content,你将看到:

/cities/city[2]/name/text()

对于 JSON 来说,也有 JSONPath 这样的东西,但是,我们却很少提及它,因为正如上文我提到的,它和 JavaScript 极强的亲和力。我们在前端代码中已经不自觉地通过对象的点操作符,或是数组的下标操作符使用了,于是,JSONPath 在多数场景中就显得不那么重要了。

举例来说,上面我给出的两个 XPath 例子,我们在将 cities.json 反序列化成 JavaScript 对象以后,我们可以直接访问(你可以在前面实践的控制台上,继续键入):

obj.cities[0].id

以及:

obj['cities'][1].name

但是,还有很多场景,特别是对于 JSON 支持不像 JavaScript 那么足够的场景,JSONPath 就有其用武之地了。和前面介绍的 XPath 查找的例子一样,你可以打开 JSON Path Finder 页面,把之前 cities.json 的文本粘贴到左侧的输入框中,在右侧选择对应的节点或值,上方就会显示出 JSONPath 了:

所以,Beijing 的 id 和 Shanghai 的 name 分别显示为:

x.cities[0].id
x.cities[1].name

这和 JavaScript 对象访问的表达式是一致的。

4. 特殊字符

任何格式都要使用特定的字符来表示结构和关系,那么 XML 和 JSON 也不例外,这些特定字符,如果被用来表示实际内容,就会出现“冲突”,于是我们就需要转义。

对于 XML 来说,且看下面的表格,第三列是“预定义实体”,也就是字符转义后相应的形式:

值得一提的是,在 XML 中,我们还能够以 CDATA 来表示内容,这种方式可以尽可能地避免对于转义的使用,并可以直接使用换行等控制字符,增加 XML 的可读性。比方说下面这个例子,实际的 function 使用 CDATA 的方式给嵌在了 function 这个 tag 内:

<function><![CDATA[
function compare(a, b) {
  ...
}
]]></function>

对于 JSON 来说,没有这样的预定义实体,但是我们也经常需要转义,比如说,如果双引号出现在了一对双引号内的字符串中,这时候我们可以用常规的反斜杠转义序列来表示,比如引号“转义为 \”等。

审视一番 YAML

最后来讲一讲 YAML,这是一种可读性非常优秀的数据交换语言。如果你使用过 Python,那么对于其“有意义”的缩进应该印象深刻,而 YAML,也具备这样的特点。你可以打开 XML to YAML Converter 这个网站,然后将前面我们创建的 cities.xml 的内容拷贝出来,贴到左侧的输入框中:

然后点击 XML TO YAML 按钮,你就能看到前面的例子,如果使用 YAML 来表示,它会是这么个样子:

cities: 
 city: 
  - 
   name: Beijing
   id: 1
  - 
   name: Shanghai
   id: 2

你看,这种方式对于层次结构的表达,可以说比 XML 或 JSON 更为清晰。对于 XML 和 JSON 的表达,对于每一层节点,你都要去找结尾标记,并和节点开头标记对应起来看;但是 YAML 则完全不用,它利用了人阅读的时候,一行一行往下顺序进行的特性,利用直观的缩进块来表达一个特定深度的节点。对于某些强调层次结构的特定信息表达的场景,比如说电子邮件消息,或者是商品信息、候选人的简历等等,使用 YAML 比其它的数据交换格式都要直接和清晰。

值得注意的是,对于缩进,YAML 要求不可以使用 TAB,只能使用空格,这和 Python 是不同的;而对于每层缩进多少个空格,这不重要,只要保证不同层次的缩进空格数量不同即可,这一点和 Python 是相同的。

YAML 由于极强的可读性,它在数据类型的明确性上做了一定程度的牺牲。从上面的例子你可以发现,本来我们希望 name 是字符串,id 是数值,可是 YAML 它根本不关心这一点,如你所见,字符串也没有双引号修饰,它只是诚实地把具体的文本数值罗列展示出来罢了。这一点,在我们权衡不同的数据交换格式的时候(比如设计哪一种格式作为我们的模块配置项文件),需要注意。

总结思考

今天我们一边动手比较、一边学习了 XML 和 JSON 的前前后后,包括它们的风格、schema 和路径表达式等等,并在之后了解了可读性至上的另一种数据交换语言 YAML。希望这些内容能够帮助你对于这些数据交换语言有更为全面的认识,并能够在工作中选择合适的技术来解决实际问题。

今天的提问环节,我想换个形式。这一讲我们已经比较了许多 XML 和 JSON 的特性了,其中一些涉及到了它们之间的优劣。那么,你能不能归纳出 XML 和 JSON 各自的一些优劣来呢?比如有这样几个方面供你参考,当然,你完全可以谈其它方面:

扩展阅读

评论