级别: 初级 David Mertz,博士 (mertz@gnosis.cx), 创新者, Gnosis Software, Inc.
2002 年 10 月 01 日 本文中,David 向您介绍了 YAML,这是一种人们可以轻松阅读的数据序列化格式,并且它非常适合对动态编程语言中使用的数据类型进行编码。与 XML 不同的是,YAML 使用了清晰且结构极其紧凑的指示符,这主要依赖嵌套元素的缩排。更重要的是,对于许多任务来说,YAML 出众的语法非常适合介于 YAML 和“自然的”数据结构之间的语义。
我猜想读者心中要问的第一个问题一定是,“为什么要命名为
YAML?”已经有许多工具采用了招人喜欢的“YA*”形式的首字母缩略词,来表示“还有另一种 XXX(Yet Another XXX)”。在开放源码这个充满智慧的领域中,YAML 没有使用其名称所暗示的首字母缩略词,而是采用了循环的“YAML 不是标记语言(YAML Ain't Markup Language)”的缩写。然而对此产生的某种感觉是:YAML 确实可以做标记语言所做的工作,却不需要任何的
标记。
尽管 YAML 与 XML 一样普通,但在阅读、编辑、修改和产生方面,它比 XML 简单得多。即,您可以用 XML 表示的任何东西,都可以用 YAML 表示(几乎总是更紧凑)。名称空间代表一种“容器”,在这种“容器”中,需要一些特定的需求 ― 您可以“将名称空间联接到” YAML 中,但它们不是当前规范的一部分。
在本专栏中我经常提到对 XML 的一个批评(而且远不止我一个人强调这一点)是 XML 妄图面面俱到。XML 是一个典型的由委员会驱动的庞然大物,它试图成为一种文档格式、数据格式、消息包格式、安全的 RPC 通道(SOAP)以及一种对象数据库。而且,XML 为每一类型的访问和操作都提供了大量的 API:DOM、SAX、XSLT、XPATH、JDOM 以及许多不太常见的接口层(在
gnosis.xml.pickle 、
gnosis.xml.objectify 和
gnosis.xml.validity 包中我自己也编写了几个接口层)。非常了不起的是 XML 完成了所有这些工作;令人失望的是没有一项工作是完美无缺的。
YAML 的关注面比较窄,它只是清晰地表示在动态编程语言(如 Perl、Python、Ruby 以及使用范围不太广的 Java 编程)中所遇到的数据结构以及数据类型。目前,对于这些语言,已经有了一些绑定/库。许多其它语言中已存在可以
很好地使用YAML 的数据模型,但现在还没有人编写库。这些语言包括 Lisp/Scheme、Rebol、Smalltalk、xBase 和 AWK。动态程度较低的语言也不适合使用 YAML。YAML 的语法结合了 Perl 的上下文类型测定、Python 的缩排结构以及 MIME 标准上的一些约定。Python 有时被赞誉为是
可执行的伪码,与此非常相同的是,YAML 的具体语法非常接近于您可能使用的将数据结构非正式地解释为一个类或工作组的语法。
草拟应用程序
了解为什么要使用 YAML 的最简单方法是看一下不同格式编写的代码。本篇专栏文章中,我假定已创建了有关数据存储和传输需求的一个小应用程序。这是一个我特别喜爱的项目,其灵感源自“巴林人机大战(Brains in Bahrain)”国际象棋对抗赛(编写本文时正在进行),对战双方是 FISA 国际象棋世界冠军和排名第一的计算机选手(请仔细研究我的数据详细信息)。如果您想创建一个追踪国际象棋俱乐部活动的程序,则可以使用由以下 Perl 代码描述的数据结构。
清单 1. 国际象棋俱乐部数据结构的 Perl 描述
$players = {
'Vladimir Kramnik' => {'status'=>'GM', 'rating'=>'2700'},
'Deep Fritz' => {'status'=>'Computer','rating'=>'2700'},
'David Mertz' => {'status'=>'Amateur', 'rating'=>'1400'},
};
$club = {
'_players' => $players,
'matches' => [
{'Date' => '2002-10-04',
'White' => $players->{'Deep Fritz'},
'Black' => $players->{'Vladimir Kramnik'},
'Result' => 'Draw' },
{'Date' => '2002-10-06',
'White' => $players->{'Vladimir Kramnik'},
'Black' => $players->{'Deep Fritz'},
'Result' => 'White' }
]
};
|
用 Python 编写的代码类似于上面的 Perl 代码:
清单 2. 国际象棋俱乐部数据结构的 Python 描述
ts = yaml.timestamp
# or mx.DateTime or othr date class
players = {
'Vladimir Kramnik'
: {
'status'
:
'GM'
,
'rating'
:
2700},
'Deep Fritz'
: {
'status'
:
'Computer'
,
'rating'
:
2700},
'David Mertz'
: {
'status'
:
'Amateur'
,
'rating'
:
1400},
}
matches = [
{
'Date'
: ts(
'2002-10-04'
),
'White'
: players[
'Deep Fritz'
],
'Black'
: players[
'Vladimir Kramnik'
],
'Result'
:
'Draw'
},
{
'Date'
: ts(
'2002-10-06'
),
'White'
: players[
'Vladimir Kramnik'
],
'Black'
: players[
'Deep Fritz'
],
'Result'
:
'White'
}
]
club = {
'_players'
:players,
'matches'
:matches}
|
其它动态编程语言可以使用类似的数据结构描述。这基本上是一个顶级的字典/映射/散列,它以几个递归级别来包含字典和列表;这还允许嵌套的元素相互引用。
推测起来,管理国际象棋俱乐部的应用程序可以执行象记录附加赛、添加/删除俱乐部中的会员,或根据进行的比赛更新排名之类的任务。而且,应用程序不仅要
记录数据快照,而且它还要与使用同一个数据模型的其它应用程序(用其它语言编写的)一起
分享该数据快照。此外,如果我们可以在应用程序之外手工方便地修改数据,则就更好了;任何已经开发过面向数据的应用程序或维护过某个组织的记录的人都知道,在底层数据结构中查找具体某个内容是多么有用。
选择表示
您可能已经注意到我已描述的事实:YAML 将迅速成为一种令人满意的解决方案。但我并不认为这种安排不公平。使数据结构化所用的方法完全类似于数据通常表示其本身的方法,至少在主要方面。我已选择以每一个看起来都是最自然的方式使用上述映射、列表、引用和数据类型。另外,我只选择根本性问题,它足够复杂从而能够说明问题,但又足够简单,使得在一篇文章中就能够讲明白。
如果您想对所有的国际象棋俱乐部应用程序都规定某种特定的编程语言(可能是某个特定的版本),则大多数语言在内置的或公共的库中都有非常好的序列化能力:
- Python 有
cPickle 、
gnosis.xml.pickle 和
pprint
- Perl 有
Data::Dumper 、
Data::Denter 和
Data:DumpXML
- Ruby 有
Marshal 和
XmlSerialization
- Java 语言有
java.io.Serializable 、
org.apache.xml.serialize.XMLSerializer 和其它各种库
正如这些名称所表示的,一些库产生了 XML,但那绝不表示 XML 在各语言之间是可轻松转移的。
另外,使用 XML 表示这个国际象棋俱乐部数据时有几个常规的语义问题。XML 中存在这样的概念:对元素属性采用无序映射,而对嵌套元素使用严格的排序。特定的应用程序当然具有
忽略某些排序信息的能力,但 XML 的
信息模型总是声称排序的重要性,这常常是不合逻辑的。例如,按照特定的序列(如根据日期)考虑对比赛排序,同时,这自然不能对选手排序。(您当然可以按照如排名或登记日期
强制进行排序。)问题是在每个应用程序中您都需要定制编程,从而
除去不合逻辑的隐含的排序信息,并
保留重要的隐含的排序信息。
各种语言中的 XML-RPC、SOAP、
gnosis.xml.pickle 和 XML 序列化器库采用常规方法来表示映射。在所有这些例子中,基本原则是使用(相当繁琐地)
<key> 和
<val> (或类似标记)来表示无序对,以及用不同的容器元素来表示有序项。这个原则添加了几个层从而除去了部分 XML 信息模型:
清单 3. 有序集合和无序集合的 XML-RPC 模型
>>> import xmlrpclib
>>> print xmlrpclib.dumps(({'this':'that',
... 'spam':('eggs','toast')},))
<params>
<param>
<value><struct>
<member>
<name>this</name>
<value><string>that</string></value>
</member>
<member>
<name>spam</name>
<value><array><data>
<value><string>eggs</string></value>
<value><string>toast</string></value>
</data></array></value>
</member>
</struct></value>
</param>
</params>
|
XML-RPC 有几个额外的构件 ― 就象需要将整个对象封装在包含一项的元组中 ― 但这些都是小问题。“本机”数据模型和 XML 数据模型之间难以匹配的问题同样在这里所提到的任何 XML 序列化格式中也是很明显的。
尝试 XML
用 XML 表示这个国际象棋俱乐部数据时至少产生了两个问题。第一个问题,也是比较简单的问题,就是在理论上最佳的 XML 表示是什么。对此,我建议使用类似如下的代码作为使用 XML 的最佳尝试:
清单 4. 国际象棋俱乐部数据的最优 XML 描述
<?xml version="1.0"?>
<club>
<players>
<player id="kramnik"
name="Vladimir Kramnik"
rating="2700"
status="GM" />
<player id="fritz"
name="Deep Fritz"
rating="2700"
status="Computer" />
<player id="mertz"
name="David Mertz"
rating="1400"
status="Amateur" />
</players>
<matches>
<match>
<Date>2002-10-04</Date>
<White refid="fritz" />
<Black refid="kramnik" />
<Result>Draw</Result>
</match>
<match>
<Date>2002-10-06</Date>
<White refid="kramnik" />
<Black refid="fritz" />
<Result>White</Result>
</match>
</matches>
</club>
|
上述 XML 数据表示十分清晰。与 Perl 和 Python 示例中提供的本机数据描述,以及与
清单 5中的 YAML 描述相比,它都不十分冗长。用象文本编辑器之类的通用工具来修改文档并不那么困难。(事实上,那就是我最初创建 XML 的方法。)
从语义上说,我提出的 XML 包含了所有讨论的问题。选手的排列是有序的,即使未有意这样做。而且选手列表出现在比赛列表之前,即使这种概念性次序不是有意安排的。正如预料的那样,(作为 XML 属性的)选手属性是无序的,但由于比赛“属性”不适合作为 XML 属性,所以 XML 强加了一个人为的次序。
在实际
读取和编写我最佳的 XML 格式时出现了更重要的问题。甚至没有一个公共的 XML API 能够使这个操作自动进行。例如,SAX 读取器可以查找各类“选手”和“比赛”事件,并将它们手工添加到相关的嵌套字典或列表中,但这个方法很脆弱,在开发期间当数据结构中出现最轻微的更改时都需要对这个方法重新编程。遍历 DOM 树也存在同样的问题。象
JDOM 或
REXML 这样定制的 API 对此也无能为力。但
gnosis.xml.objectify 可以出色地自动生成本机对象,然而这只对 XML 中的读有效,而不适用于写回该对象。当然,写是和读对称的,它具有所有相应的脆弱性。
YAML 进行营救
YAML 格式仅使动态语言的数据结构匹配得更好。而且看上去也更好一些。这里是同一个国际象棋俱乐部数据的 YAML 表示:
清单 5. 国际象棋俱乐部数据的 YAML 描述
---
players:
Vladimir Kramnik: &kramnik
rating: 2700
status: GM
Deep Fritz: &fritz
rating: 2700
status: Computer
David Mertz: &mertz
rating: 1400
status: Amateur
matches:
-
Date: 2002-10-04
White: *fritz
Black: *kramnik
Result: Draw
-
Date: 2002-10-06
White: *kramnik
Black: *fritz
Result: White
|
这种格式有许多优点。YAML 网站提供了确切的规范(请参阅
参考资料),但这个简短的样本向您展示了这些基本元素相当精确的思想。该规范还涉及包含(多)段落字符串的直观方法。YAML 很简洁,但仍然具有可读性。而且,引号使用降到了最低,其所带的数据类型是从模式中推理而来的(例如,如果它看上去象日期,那么除非显式地用引号将其括起来作为字符串,否则它被视为时间戳记值)。您可以使用对任何指定的目标的引用。而且值得注意的是,YAML 保留着有序集合和关联集合之间的区别。作为额外的优点,您可以在文本编辑器中非常方便地编辑 YAML。
上面列出的语义和语法优点并不是我将 YAML 用于应用程序的真正最重要的理由。实际上,最重要的部分是在所有受支持的语言中其统一的接口。如下所示,我可以方便地读取、操作和编写上述 YAML 数据文件:
清单 6. 用 Python 对 YAML 数据源进行访问
import
yaml
club = yaml.loadFile(
'club.yml'
).next()
# ...manipulate the 'club' data structure...
club_yamlstr = yaml.dump(club)
# ...do something w/ formatted YAML in club_yamlstr...
|
我使用了上述
.next() 方法,因为 YAML 文本可以包含多个流,每个流由
--- 分隔开。顺便说一句,
club 中的数据结构与前面纯 Python 定义中定义的数据结构
完全相同。
用 Perl(或 Ruby 或 Java 编程)来访问 YAML 数据源,步骤几乎相同:
清单 7. 用 Perl 对 YAML 数据源进行访问
use
YAML ();
my $club = YAML::LoadFile(
'club.yml'
);
my $club_yamlstr = YAML::Dump($club);
|
YAML 和本机数据结构之间的差别很小……唔,十分接近。我发现了两个很小的缺点:
- 引用丢失了其名称(例如,“*kramnik”),被简单地编了号(例如,“*1”)
- 目标总是在第一次出现时被清楚地说明。
在美学观点上,我喜欢在“选手”这一部分中查看选手的详细信息,但使用无序字典不能保证这一点(使用 Perl/Python 样本中
_player 可以解决这一问题)。
它意味着什么
YAML 还有许多特性在这里没有提及。正式的规范很好,虽然读起来有点困难(与大多数规范一样)。例如,现有的 YAML 库附带了用于在 XML 和 YAML 之间进行转换的充足的、但不是最佳的转换工具。而且还对 YPATH 技术提供支持,该技术是 XPATH 的 YAML 版本。
本简介旨在推荐几种情况,在这几种情况下,YAML 提供的对象序列化格式比 XML 所提供的更佳。对我而言,XML 并不总是数据表示的最佳选择 ― 甚至在那些看上去它明显很适合的情况下。
参考资料
我以前的许多专栏文章涉及以下相关主题:
IBM 参考资料
关于作者
对本文的评价
|