YAML
自从有了递归定义,大家都爱上了这种起名方式,YAML 也是。YAML 的定义是:“YAML Ain ’ t a Markup Language”,即:YAML 不是一种标记语言的递归缩写。要问 YAML 到底是不是一种标记语言呢?答案:是的。有意思的是:在 YAML 开发的早期,YAML 其实参考了许多其他语言,如 XML, SDL 及电子邮件格式等等,并最终把自己定义为:“Yet Another Markup Language”。既然明明是标记语言,为什么后来又改名换姓,非说自己不是标记语言了呢?其实名字的更换正是为了强调 YAML 的与众不同:YAML 是以数据为设计语言的重点的,而不是像 XML 以标记为重点。实事上,正是因为这样一种设计理念使得 YAML 在后来的不少应用中取代 XML,成为一种可读性高,易于表达数据序列的编程语言。
YAML 的数据组织主要依靠的是空白,缩进,分行等结构。这使得 YAML 语言很容易上手。我们就以 YAML 官方网站上给出的一个例子来看看 YAML 文件的书写(如清单 1 所示)。相信熟悉 XML 的人都会感受到 YAML 是多么的简洁明了!
YAML 的语法十分简单:用“-”来表示一些序列的项(Sequence),如清单 1 里的产品(product)有两样东西(Basketball 和 Super Hoop)组织为一个序列;用“:”来表示一对项目(Map)里的栏目(Key)和其相应的值(Value),比如清单 1 发票里的时间(date)的值是 2001-01-23,这就是一个 Map。这些就是 YAML 里最重要的语法了。如果想知道其他语法的细节可以参看 YAML 官方网页里的参考卡片(reference card):http://www.yaml.org/refcard.html
--- !clarkevans.com/^invoice invoice: 34843 date : 2001-01-23 bill-to: &id001 given : Chris family : Dumars address: lines: | 458 Walkman Dr. Suite #292 city : Royal Oak state : MI postal : 48046 ship-to: *id001 product: - sku : BL394D quantity : 4 description : Basketball price : 450.00 - sku : BL4438H quantity : 1 description : Super Hoop price : 2392.00 tax : 251.42 total: 4443.52 comments: > Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338. |
正因为 YAML 的语法和组织结构简单巧妙,YAML 很容易就可以插入另一个 YAML 文件,甚至其他类型的文件,包括 XML, SDL, JSON 等。相反,如果要在 XML 里插入 YAML,相信了解 XML 的朋友都知道,那是要加很多符号(Potential Sigils)才能完成。
通过上面的举例,我们可以看出,YAML 较 XML 而言可读性更好,也更易于实现。但 YAML 的优点远不止于此。当 YAML 诞生不久的时候,其应用还只在动态编程语言如 Perl,Python, Ruby,及当时还应用得不算广泛的 java 编程中。而现如今 YAML 的支持库已包括 C/C++,C#/.NET,PHP 等。可以说,几乎在如今流行的编程语言中,YAML 已经无处不在。
事实上,纵观程序语言的发展,从 C 到 C++,Java,再到动态脚本语言 Perl,Python,PHP, 直到如今相当之流行的 Ruby,人们越来越摈弃那些规则复杂,语法深奥的语言,取而代之的则是灵活简单,易读易写的,越来越趋近人类阅读书写习惯的程序语言。在数据的序列化上,这种趋势其实是一样的。YAML 作为可以和 XML 一样扩展性强,表达力强且基于流操作的语言,凭借着自身在可读性,易实现,与脚本语言易交互,于宿主语言的数据结构类型易使用等各方面优势,在数据序列化格式领域正成为越来越为人们喜爱的一种语言。
本文的后续两个章节将从开发应用和配置文件两个方面以及 C++ 和 Ruby 这两种语言的角度,以实例介绍 YAML 的使用方法和优越之处。
我们还以清单 1 里的发票数据为例。假如你有一个 C++ 开发的应用,其中的发票数据需要用一种语言来做序列化。选择 YAML,我们现在就把发票信息数据用 YAML 写在一个文件里的(具体请参考附件里的 invoice.yaml),然后下载并安装 yaml-cpp 库文件包,详细的方法和步骤可以参考 yaml-cpp 的官方网址上的说明:http://code.google.com/p/yaml-cpp/。接着我们就可以开始设计相应的 C++ 代码里的数据结构了。具体定义可以参看下面的清单 2.
struct Product { std::string sku; int quantity; std::string description; float price; }; struct Address { std::string lines; std::string city; std::string state; int postal; }; struct Bill { std::string given; std::string family; Address address; }; struct Invoice { int invoice; std::string date; Bill bill; std::vector <Product> products; float tax; float total; std::string comments; }; |
虽然 C++ 在标准模板库(STL)里有 Map,List 等与 YAML 相对应的数据类型,但是本例中为了更清晰看出 YAML 的用法,只简单构建了一些结构体与之对应。(从另一角度说,如果 YAML 用在像 Ruby 一样的脚本语言中,那么数据类型的对应将更加简单。在下一节的内容中将给出相关的实例。)另外,在 C++ 里,为了能够使用操作符“>>”(stream extraction operator)帮组我们完成 YAML 数据和 C++ 数据之间的赋值,我们需要对该操作符进行重载(overload)。具体实现可参考清单 3。
void operator >> (const YAML::Node& node, Product& p) { node["sku"] >> p.sku; node["quantity"] >> p.quantity; node["description"] >> p.description; node["price"] >> p.price; } void operator >> (const YAML::Node& node, Address& a) { node["lines"] >> a.lines; node["city"] >> a.city; node["state"] >> a.state; node["postal"] >> a.postal; } void operator >> (const YAML::Node& node, Bill& b) { node["given"] >> b.given; node["family"] >> b.family; node["address"] >> b.address; } void operator >> (const YAML::Node& node, Invoice& invoice) { node["invoice"] >> invoice.invoice; node["date"] >> invoice.date; node["bill-to"] >> invoice.bill; const YAML::Node& products = node["product"]; for(unsigned i=0;i<products.size();i++) { Product p; products[i] >> p; invoice.products.push_back(p); } node["tax"] >> invoice.tax; node["total"] >> invoice.total; node["comments"] >> invoice.comments; } |
在对 Product 的实现中,这里使用了 C++ 标准模板库里的 vector 容器以完成对多个 product 的赋值。
有了数据结构,有了“>>”,那么当我们需要使用 YAML 数据的时候,只需读入 YAML 文件,交给 YAML 库的解析器(Parser)便高枕无忧了。相应的代码示例请参看清单 4。
std::ifstream fin("invoice.yaml"); YAML::Parser parser(fin); YAML::Node doc; parser.GetNextDocument(doc); Invoice invoice; doc >> invoice; |
完整的代码示例请参考附件里的“invoice.cpp”和“invoice.yaml”。对于例子中的数据结构和方法,读者也可以构建更加完美的实现,这里仅是一简单示例。
假设你在开发一个分布式系统,系统中有不同的节点(node)。那么配置文件里需要写明各种不同角色的节点各自的配置信息。如果你打算用 Ruby 语言来解析这种配置文件,那么用 YAML 来书写配置文件一定是你的不二选择。其实不仅仅是 Ruby,YAML 对于几乎所有的脚本语言来说,它的解析都比 XML 来得容易得多。
Ruby 有着丰富灵活的数据结构,这使得 YAML 加 Ruby 的组合仿佛天生一对。比如:YAML 里的序列(Sequence)对应 Ruby 里的数组(Array);YAML 的一对项(Map)对应着 Ruby 里的 Hash 等等。详细的对应关系可见:http://www.yaml.org/YAML_for_ruby.html#folded_block_as_a_mapping_value。
假设我们的配置文件如清单 5 所示。
# # Config file example # node_a: conntimeout: 300 external: iface: eth0 port: 556 internal: iface: eth0 port: 778 broadcast: client: 1000 server: 2000 node_b: 0: ip: 10.0.0.1 name: b1 1: ip: 10.0.0.2 name: b2 |
该配置文件定义了两种不同角色的节点(node),节点 node_a 是一个负责内外通信的节点。它的配置信息包括:其连接的 timeout 时间为 300 秒,外部及内部连接使用的接口(interface)及端口号(port),其中,内部连接还配置了广播的客户端和服务器端的端口。而 node_b 类型的节点共有两个节点组成,分别是 b1 和 b2,其配置信息包括 IP 地址和主机名。实际的配置当然会比这个文件复杂一些,这里仅仅是一个示例。当我们用 Ruby 语言解析该数据后(解析方法如清单 6 所示),就可以得到清单 7 里的数据结果。
#!/usr/bin/ruby require 'yaml' yml = YAML::load(File.open('t.yml')) p yml |
大家可以看到,Ruby 语言解析 YAML 只需了了四行代码。其实我们完全可以打印行省掉,改写为“p YAML::load(File.open('t.yml'))”。也就是说,真正需要的就两行,require 和 YAML::load。再看结果,由于清单 7 里的结果在结构上比较复杂,也为了能和清单 5 中的 YAML 数据“看”起来相对应,这里做了一些缩进上的修改。由此我们可以更清晰得看出 YAML 数据在 Ruby 中的组织。
{"node_a"=>{ "internal"=>{ "broadcast"=>{ "client"=>1000, "server"=>2000 }, "port"=>778, "iface"=>"eth0" }, "conntimeout"=>300, "external"=>{ "port"=>556, "iface"=>"eth0" } }, "node_b"=>{ 0=>{ "name"=>"b1", "ip"=>"10.0.0.1" }, 1=>{ "name"=>"b2", "ip"=>"10.0.0.2" } } } |
在 Ruby 中使用 YAML 就是这么简单,你只有 require 了 YAML 的库,一切数据到 Ruby 数据结构的转换都轻而易举。其实 Ruby 的数据也可以 dump 为 YAML。感兴趣的朋友可以参看 YAML for Ruby 的文档:http://yaml4r.sourceforge.net/doc/。
YAML 的官方网址为http://www.yaml.org/