Scala 和 XML

简化 XML 处理

Scala 是一种在 Java™ 虚拟机(Java™ Virtual Machine,JVM)上运行的流行的新型编程语言。Scala 被编译成字节码,因此它可以利用 Java 编程语言。然而它的语法使其在某些场景下成为 Java 的一个强有力的候补。这些场景之一就是 XML 处理。Scala 容许您以多种方式导航和处理解析后的 XML。它还为 XML 构建提供了一流支持,因此无需创建 XML 字符串或以编程方式构建 DOM 树。在本文中,您将了解 Scala 在这些方面的实际效用以及 Scala 如何将 XML 处理转变为一种乐事。

Michael Galpin (mike.sr@gmail.com), 软件工程师, Vitria Technology

Michael Galpin 拥有加州理工学院的数学学位。从 20 世纪 90 年代末以来,他一直是 Java 开发人员兼 Vitria Technology 在加州桑尼维尔市的软件工程师。您可以发电子邮件到 mike.sr@gmail.com 与作者取得联系。



2008 年 5 月 15 日

常用缩写词

  • API:应用程序编程接口(application programming interface)
  • DOM:文档对象模型(Document Object Model)
  • HTTP:超文本传输协议(Hypertext Transfer Protocol)
  • JSON:JavaScript 对象标志(JavaScript Object Notation)
  • SAX:Simple API for XML
  • StAX:Streaming API For XML
  • XML:可扩展标记语言(Extensible Markup Language)

本文使用了 Scala 编程语言,其版本为 2.6.1。作为一种新生语言,它仍在快速发展,因此需要了解它的最新进展。本文并不要求读者具备 Scala 知识,而是尝试介绍 Scala 的语法和术语。Scala 需要一个 Java 虚拟机。本文使用 JDK 1.6.0_04,但 Scala 只需要 1.5 或更高版本。尽管本文没有包含 Java 代码,但是也要求读者熟悉 Java 编程。

解析 XML

首先探讨如何使用 Scala 解析 XML。像大多数编程语言一样,Scala 提供了多种 XML 解析方法。以下是一些基本的方法:基于表示的 InfoSet/DOM、push (SAX) 或 pull (StAX) 事件、与 JAXB(Java Architecture for XML Binding) 类似的数据绑定。您将探讨基于 DOM 的处理,因为 它演示了 Scala 语法的众多好处。在深入研究之前,您需要了解要解析的 XML 内容以及对它执行哪些操作。因此需要借助一个样例应用程序。


样例应用程序:FriendFeed

FriendFeed 是一个在 2008 年非常流行的 Web 服务,它允许用户在其他服务中聚合他们的行为,例如各种博客(blog)服务、即时信息传递服务、YouTube、Flickr 和 Twitter 等。然后从这种聚合中创建单独的数据提要。您可以针对个人执行上述操作,即对指定的人员实现聚合行为。 尽管可能不是很有用,但是 FriendFeed 的公共提要非常有趣。它在所有 FriendFeed 用户之间聚合所有的公共行为。FriendFeed 提供一个 API 来访问个人提要和公共提要。您将编写一个应用程序来访问和解析公共提要。


利用 Java 库

您要做的首要事情是访问 FriendFeed 的公共提要。其 URL 为 http://friendfeed.com/api/feed/public。默认的情况下它以 JSON 格式显示数据并且显示最新的 30 个条目。要将其改为 XML 格式,添加查询字符串参数 format=xml。例如,要将条目数目改为 100,添加查询字符串参数 num=100 。现在您只需要访问这个 URL。这在 Java 代码中很容易实现,因此在 Scala 代码也很容易。 看一下 清单 1 中访问 FriendFeed 公共提要的代码。

清单 1. 访问 FriendFeed
object FriendFeed {
  import java.net.{URLConnection, URL}
  import scala.xml._ 
  def friendFeed():Elem = {
    val url = new URL("http://friendfeed.com/api/feed/public?format=xml&num=100")
    val conn = url.openConnection
    XML.load(conn.getInputStream)
  }
}

注意,这里要做的第一件事就是导入两个核心的 Java 类。 Scala 不必使用自己的 API 执行诸如打开 HTTP 连接之类的操作,因为它可以利用 Java 的 API 来解决这个问题。注意 Scala 为从同一包导入多个类提供了捷径。下一行导入 Scala 的核心 XML 类。下划线就像 Java 中的星号一样,它导入 scala.xml 包中的所有类。

因此使用 Java 的 API 打开一个到 FriendFeed 的 HTTP 连接。接下来使用 Scala 的 XML 对象进行解析。这里有很多有趣的现象。首先,XML 是一个 Scala 对象,即它是一个单例(singleton)对象。Scala 没有静态的方法、字段和初始化程序。相反您可以定义一个对象(而不是类)并且它将成为类的一个单例实例。您可以像调用静态方法一样访问单例对象的方法。这就是 XML.load 语句的作用。注意,尽管这是一个 Scala 对象的方法,它接受一个 Java 对象(java.io.InputStream)作为参数。这正体现了 Scala 和 Java 之间的紧密联系。 最后要注意没有返回语句。返回语句在 Scala 中是可选的。如果没有返回语句,将返回对方法的最后一个语句的求值(如果可行并且 Scala 没有返回编译错误的话)。现在可以很简单地访问 清单 1 中的方法,如 清单 2 所示。

清单 2. 访问 friendFeed 方法
val feedXml = friendFeed

注意在调用 friendFeed 的方法时没有必要使用圆括号。您也可以使用 Scala 的类型接口。您没有必要声明 feedXml 的类型,因为它是由 friendFeed 方法的返回类型推断出来的。再次查看 清单 1 并了解它如何利用语法捷径。最后要注意的是您所解析的 XML 对象被声明为 val。这使其成为不可变的对象(像 Java 代码中的字符串),这在 Scala 中是很常见的。把 XML 作为一个不可变的对象有很多优点,但是如果您习惯在 DOM 中使用 appendChild API,那么则很难适应这一点。现在已经从 FriendFeed 中解析了 XML,可以开始使用 Scala 对其划分。


导航和模式匹配

许多编程语言将 XML 表示为 DOM 树。这个方法有许多优点,但是不利于以编程的方式遍历树来从 XML 文档中提取数据。Java 技术提供了可以利用 XPath 语法的库。Scala 采取相似的方法,但它有许多优点。Scala 在这个方法中体现了很多函数语言特征。在 Scala 中没有使用操作符(像 + 或 *)。相反,使用 + 或 * 等符号定义可以执行普通数字加减法的函数。这也意味着您可以定义任何类型的操作符(因为它们实际上就是函数)。 这些操作符号比 C++ 这类语言中的重载操作符具有更强大的功能。在 XPath 中,由于可以被转换成一个函数调用,您可以在 Scala 中直接应用 XPath 语法的某一部分。

了解了这些内容,我们来看一下 FriendFeed 中的 XML 是什么样子。清单 3 提供了一个例子。

清单 3. FriendFeed XML 示例
<feed>
    <entry>
        <updated>2008-03-26T05:06:36Z</updated>
        <service>
            <profileUrl>http://twitter.com/karlerikson</profileUrl>
            <id>twitter</id>
            <name>Twitter</name>
        </service>
        <title>Listening to Panic at the Disco on Kimmel</title>
        <link>http://twitter.com/karlerikson/statuses/777188586</link>
        <published>2008-03-26T05:06:36Z</published>
        <id>f18ebf10-06be-98e2-6059-fa78fa44584b</id>
        <user>
            <profileUrl>http://friendfeed.com/karlerikson</profileUrl>
            <nickname>karlerikson</nickname>
            <id>f294a86c-e6f3-11dc-8203-003048343a40</id>
            <name>Karl Erikson</name>
        </user>
    </entry>
    <entry>
        <updated>2008-03-26T05:06:35Z</updated>
        <service>
            <profileUrl>http://twitter.com/asfaq</profileUrl>
            <id>twitter</id>
            <name>Twitter</name>
        </service>
        <title>@ceetee lol</title>
        <link>http://twitter.com/asfaq/statuses/777188582</link>
        <published>2008-03-26T05:06:35Z</published>
        <id>d4099bb0-8186-5aa1-ce1f-672246c0fe9c</id>
        <user>
            <profileUrl>http://friendfeed.com/asfaq</profileUrl>
            <nickname>asfaq</nickname>
            <id>41e24568-ee6b-11dc-a88d-003048343a40</id>
            <name>Asfaq</name>
        </user>
    </entry>
    <entry>
        <updated>2008-03-26T05:06:31Z</updated>
        <service>
            <profileUrl>http://twitter.com/chrisjlee</profileUrl>
            <id>twitter</id>
            <name>Twitter</name>
        </service>
        <title>sleep..</title>
        <link>http://twitter.com/chrisjlee/statuses/777188561</link>
        <published>2008-03-26T05:06:31Z</published>
        <id>8c4ec232-3ad5-28e1-16c0-00a428294c9c</id>
        <user>
            <profileUrl>http://friendfeed.com/chrisjlee</profileUrl>
            <nickname>chrisjlee</nickname>
            <id>5af39ad4-53b6-45d8-ae25-ef7c50fe9568</id>
            <name>Chris</name>
        </user>
    </entry>
    <entry>
        <updated>2008-03-26T05:06:49Z</updated>
        <service>
            <profileUrl>
                http://www.google.com/reader/shared/09566745492004297397
            </profileUrl>
            <id>googlereader</id>
            <name>Google Reader</name>
        </service>
        <title>Poketo First Editions Show!!</title>
        <link>
            http://www.poketo.com/blog/2008/03/24/poketo-first-editions-show/
        </link>
        <published>2008-03-26T05:06:49Z</published>
        <id>4caefceb-d71c-59c9-8199-45c5adbc60f2</id>
        <user>
            <profileUrl>http://friendfeed.com/misterjt</profileUrl>
            <nickname>misterjt</nickname>
            <id>e745cc8a-f9e4-11dc-a477-003048343a40</id>
            <name>Jason Toney</name>
        </user>
    </entry>
</feed>

对于您的应用程序,您将首先得到一个基于某种服务的用户列表。因此,将首先过滤提要,从而只获得感兴趣的服务。查看 清单 4 了解 Scala 如何实现上述功能。

清单 4. 过滤基于服务的提要
def filterFeed(feed:Elem, feedId:String):Seq[Node] = {
   var results = new Queue[Node]()
   feed\"entry" foreach{(entry) =>
     if (search(entry\"service"\"id" last, feedId)){
       results += (entry\"user"\"nickname").last
     }
   }
   return results
 }
 
 def search(p:Node, Name:String):Boolean = p match {
   case <id>{Text(Name)}</id> => true
   case _ => false
 }

您的函数 filterFeed 接受一个 XML 元素(提要)和一个服务 ID 作为参数。 首先创建一个称为 results 的 XML 节点队列。队列被参数化,类似 Java 中的 List 和 Map。 Scala 使用方括号来表示泛型类型,而不是像 Java 编程使用的尖括号。feed\"entry" 行是一个类 XPath 表达式。反斜杠符号实际上是 scala.xml.Elem 类的一个方法。它返回具有给定名称的所有子节点,即提要中所有 <entry> 元素。这将作为一个 scala.xml.NodeSeq 类的实例返回。这个类扩展了 Seq[Node]。因为它是一个 Seq,它具有一个 foreach方法,并将一个闭包作为参数。

(entry) => ... 标记表示一个将单个参数标记为条目的闭包。在这个闭包中,您将再次使用类 XPath 表达式 entry\"service"\"id" 来从 entry 节点提取服务的 ID。把服务 ID 传递给搜索函数来将其与传递给方法的提要 ID 相比较。我们稍后将查看这个函数体。如果匹配的话,您可将创建条目的用户别名添加到结果队列中。注意这个队列目标中类似操作符的符号,+=。再次声明这仅仅是一个队列对象的函数。您可以使用 Scala 的类 XPath 语法来提取用户别名节点。

现在参看搜索函数,这个函数使用一个功能最强大的 Scala 特性:模式匹配。在这种情况下,将输入节点与一个名为 id 的节点相比较,id 节点的子文本节点由传递给函数的 Name 字符串构成。如果匹配则函数返回 true。语法 case _ 和所有内容匹配。其中__再次用作 Scala 的通配符。诸如 case _ 这样的声明和 Java 或 C++ 代码中 case 语句的默认子句类似。这个简单的例子证明了 Scala 中模式匹配的强大功能。下面您将会明白如何构建 XML 结构。


利用模式匹配构建 XML

在应用程序中,您需要为从 FriendFeed 公共提要提取出的所有用户别名构建一个新的 XML 结构。实现上述操作有许多方法,但我们将演示如何再一次使用模式匹配方法。看一下 清单 5 中所示的函数。

清单 5. 利用模式匹配构造函数
def add(p:Node, newEntry:Node ):Node = p match {
   case <UserList>{ ch @ _* }</UserList> => 
     <UserList>{ ch }{ newEntry }</UserList>
}

这个模式将会和一个具有任意类型的子节点的 UserList 元素匹配。继而返回一个具有相同子节点的新 UserList 元素,另外在现有子节点之后又增加了一个子节点。这在功能上等效于 DOM 规范中的 appendChild 用法。但它有本质的不同,因为原始节点没有改变(它也不能改变,因为它是不可变的)。相反创建并返回了一个新节点。这样比等效的 DOM 操作使用更多的内存。我们来看一下使用 Scala 构建 XML 结构的其他方法。


创建 XML

当创建新的 XML 文档时,Scala 的原生 XML 语法再合适不过。第一个例子是获取创建的 UserList 结构并把它封装在相关服务的节点中。清单 6 显示了这些代码。

清单 6. 创建服务结果
def results(name:String, cnt:Int, elements:NodeSeq):Any = {
   if (cnt > 0){
     return <Service id={name}>{elements}</Service>
   } 
 }

由于 Scale 提供了对 XML 的原生支持,您可以利用一个模板样式的语法将动态数据插入到 XML 结构中。在本例中,使用传入的名称字符串设置 id 属性。您将获得一串传入的元素,将它们作为正在创建的 Service 元素的子节点。但是要注意,只有在 cnt 参数大于 0 的情况下才执行上述操作。如果 cnt 值等于 0,这个函数将不返回任何值。在 Scale 中您可以通过声明函数返回 Any 来解决这个问题。Any 类在 Scala 中是一个原始的类,类似于 java.lang.Object。Scale 没有 void 类型,但是有一个等价的 Unit 类型。它的优点是可以扩展 Any 类,并且允许函数在某些情况下返回对象,而在其他时候不返回任何内容。

如您所见,在 Scala 的 XML 语法中结合动态数据可以产生强大的功能。再举一个例子,您可以创建一个统计 XML 文档,其中显示的 XML 描述每个服务在提要中出现的次数。代码如 清单 7 所示。

清单 7. 创建统计 XML
def stats(map:HashMap[String,Int]):Node = {
   var nodes = new Queue[Node]()
   map.foreach{(nvPair) =>
     nodes += <Service id={nvPair._1} cnt={nvPair._2.toString}/>
   }
   return <Stats>{nodes}</Stats>
}

您的函数要求 HashMap 的键是服务的名称,其值为服务在 FriendFeed 中出现的次数。这个函数使用熟悉的 foreach-closure 风格遍历 HashMap,然后使用 HashMap 的名称/值对创建一个新节点,将这个节点添加到节点队列中。随后创建 Stats 结构并作为动态数据访问节点队列,节点队列随后被赋值给一个 XML 结构。现在准备好了所有函数,您只需驱动程序以便进行测试。


运行和测试

在运行程序之前,需要加入一些代码来驱动它。将创建一个 main 方法,就像使用 Java 编程一样,如 清单 8 所示。

清单 8. FriendFeed main 方法
def main(args:Array[String]) = {
    val feedXml = friendFeed
    var map = new HashMap[String,Int]
    args.foreach{(serviceName) =>
      val filteredEntries = filterFeed(feedXml, serviceName)
      var users:Node = <UserList/>
      filteredEntries.foreach{(user) =>
        users = add(users, user)
      }
      map += serviceName -> filteredEntries.length
      println(results(serviceName,filteredEntries.length,users))
    }
    println(stats(map))
}

这个方法创建了 FriendFeed。它接受命令行参数确定哪些服务查找用户并计算统计数据。注意这些语法与 Java 语法非常相似。main 函数接受一个 String 数组(称为 args)作为参数。这个程序为统计文档创建 HashMap,并且为每个服务创建 UserList 文档。然后输出每个 UserList 和统计文档。要运行这个程序,需要使用 scalac FriendFeed.scala 和 scala FriendFeed 进行编译,如 清单 9 所示。

清单 9. 运行程序
$ scalac FriendFeed.scala
$ scala FriendFeed googlereader flickr delicious twitter blog
<Service id="twitter"><UserList><nickname>ntamaoki</nickname>
<nickname>terrazi</nickname><nickname>ntamaoki</nickname>
<nickname>terrazi</nickname><nickname>ntamaoki</nickname>
<nickname>parodi</nickname><nickname>trevor</nickname>
<nickname>cindy</nickname><nickname>christinelu</nickname>
<nickname>clint</nickname><nickname>savvyauntie</nickname>
<nickname>44gi</nickname></UserList></Service>
<Serviceid="blog"><UserList><nickname>nechipor</nickname>
<nickname>mdolla</nickname><nickname>kyhpudding</nickname>
<nickname>hanayuu</nickname><nickname>hanayuu</nickname>
</UserList></Service><Stats><Service cnt="12" id="twitter">
</Service><Service cnt="0" id="delicious"></Service><Service 
cnt="0" id="flickr"></Service><Service cnt="0" id="googlereader">
</Service><Service cnt="5" id="blog"></Service></Stats>

您当然可以选择不同的服务名称作为命令行参数或其他参数。Scala 具备完美的 printer 类,可以使用正确的空格、制表符和格式打印 XML。还提供了 XML 写入程序(writer)将 XML 写回数据流,比如文件。您可以使用 Scala 完成所有普通的任务,同时还可以使用 Scala 提供的一些独有的功能。


结束语

许多人把 Scala 视为 Java 编程语言发展历程中的重要一步。XML 已经成为一种重要的技术,编程语言只有在其语法中内置了 XML 支持,才能自然地应用 XML 技术。而 Scale 做到了这一点。它使得复杂问题变得简单。查看本文使用 Scale 执行的所有功能,想像一下做同样的事情需要使用多少行 Java 代码。


下载

描述名字大小
本文的样例代码friendfeed.example.zip1KB

参考资料

学习

获得产品和技术

  • FriendFeed 公共提要:访问这个 URL,以 JSON 格式显示数据并显示最近的 30 个条目。
  • Scala:下载 Scala。
  • IBM 试用软件:使用 IBM 试用软件构建您的下一个开发项目,可直接从 developerWorks 下载获得。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Java technology
ArticleID=308097
ArticleTitle=Scala 和 XML
publish-date=05152008