2008 年 5 月 15 日
Scala 是一種在 Java™ 虛擬機(Java™ Virtual Machine,JVM)上運行的流行的新型編程語(yǔ)言。Scala 被編譯成字節碼,因此它可以利用 Java 編程語(yǔ)言。然而它的語(yǔ)法使其在某些場(chǎng)景下成為 Java 的一個(gè)強有力的候補。這些場(chǎng)景之一就是 XML 處理。Scala 容許您以多種方式導航和處理解析后的 XML。它還為 XML 構建提供了一流支持,因此無(wú)需創(chuàng )建 XML 字符串或以編程方式構建 DOM 樹(shù)。在本文中,您將了解 Scala 在這些方面的實(shí)際效用以及 Scala 如何將 XML 處理轉變?yōu)橐环N樂(lè )事。
![]() |
|
本文使用了 Scala 編程語(yǔ)言,其版本為 2.6.1。作為一種新生語(yǔ)言,它仍在快速發(fā)展,因此需要了解它的最新進(jìn)展。本文并不要求讀者具備 Scala 知識,而是嘗試介紹 Scala 的語(yǔ)法和術(shù)語(yǔ)。Scala 需要一個(gè) Java 虛擬機。本文使用 JDK 1.6.0_04,但 Scala 只需要 1.5 或更高版本。盡管本文沒(méi)有包含 Java 代碼,但是也要求讀者熟悉 Java 編程。
首先探討如何使用 Scala 解析 XML。像大多數編程語(yǔ)言一樣,Scala 提供了多種 XML 解析方法。以下是一些基本的方法:基于表示的 InfoSet/DOM、push (SAX) 或 pull (StAX) 事件、與 JAXB(Java Architecture for XML Binding) 類(lèi)似的數據綁定。您將探討基于 DOM 的處理,因為它演示了 Scala 語(yǔ)法的眾多好處。在深入研究之前,您需要了解要解析的 XML 內容以及對它執行哪些操作。因此需要借助一個(gè)樣例應用程序。
![]() ![]() |
FriendFeed 是一個(gè)在 2008 年非常流行的 Web 服務(wù),它允許用戶(hù)在其他服務(wù)中聚合他們的行為,例如各種博客(blog)服務(wù)、即時(shí)信息傳遞服務(wù)、YouTube、Flickr 和 Twitter 等。然后從這種聚合中創(chuàng )建單獨的數據提要。您可以針對個(gè)人執行上述操作,即對指定的人員實(shí)現聚合行為。盡管可能不是很有用,但是 FriendFeed 的公共提要非常有趣。它在所有 FriendFeed 用戶(hù)之間聚合所有的公共行為。FriendFeed 提供一個(gè) API 來(lái)訪(fǎng)問(wèn)個(gè)人提要和公共提要。您將編寫(xiě)一個(gè)應用程序來(lái)訪(fǎng)問(wèn)和解析公共提要。
![]() ![]() |
您要做的首要事情是訪(fǎng)問(wèn) FriendFeed 的公共提要。其 URL 為 http://friendfeed.com/api/feed/public。默認的情況下它以 JSON 格式顯示數據并且顯示最新的 30 個(gè)條目。要將其改為 XML 格式,添加查詢(xún)字符串參數 format=xml。例如,要將條目數目改為 100,添加查詢(xún)字符串參數 num=100 ?,F在您只需要訪(fǎng)問(wèn)這個(gè) URL。這在 Java 代碼中很容易實(shí)現,因此在 Scala 代碼也很容易??匆幌?清單 1 中訪(fǎng)問(wèn) 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) } } |
注意,這里要做的第一件事就是導入兩個(gè)核心的 Java 類(lèi)。 Scala 不必使用自己的 API 執行諸如打開(kāi) HTTP 連接之類(lèi)的操作,因為它可以利用 Java 的 API 來(lái)解決這個(gè)問(wèn)題。注意 Scala 為從同一包導入多個(gè)類(lèi)提供了捷徑。下一行導入 Scala 的核心 XML 類(lèi)。下劃線(xiàn)就像 Java 中的星號一樣,它導入 scala.xml 包中的所有類(lèi)。
因此使用 Java 的 API 打開(kāi)一個(gè)到 FriendFeed 的 HTTP 連接。接下來(lái)使用 Scala 的 XML 對象進(jìn)行解析。這里有很多有趣的現象。首先,XML 是一個(gè) Scala 對象,即它是一個(gè)單例(singleton)對象。Scala 沒(méi)有靜態(tài)的方法、字段和初始化程序。相反您可以定義一個(gè)對象(而不是類(lèi))并且它將成為類(lèi)的一個(gè)單例實(shí)例。您可以像調用靜態(tài)方法一樣訪(fǎng)問(wèn)單例對象的方法。這就是 XML.load 語(yǔ)句的作用。注意,盡管這是一個(gè) Scala 對象的方法,它接受一個(gè) Java 對象(java.io.InputStream)作為參數。這正體現了 Scala 和 Java 之間的緊密聯(lián)系。最后要注意沒(méi)有返回語(yǔ)句。返回語(yǔ)句在 Scala 中是可選的。如果沒(méi)有返回語(yǔ)句,將返回對方法的最后一個(gè)語(yǔ)句的求值(如果可行并且 Scala 沒(méi)有返回編譯錯誤的話(huà))?,F在可以很簡(jiǎn)單地訪(fǎng)問(wèn) 清單 1 中的方法,如 清單 2 所示。
val feedXml = friendFeed |
注意在調用 friendFeed 的方法時(shí)沒(méi)有必要使用圓括號。您也可以使用 Scala 的類(lèi)型接口。您沒(méi)有必要聲明 feedXml 的類(lèi)型,因為它是由 friendFeed 方法的返回類(lèi)型推斷出來(lái)的。再次查看 清單 1 并了解它如何利用語(yǔ)法捷徑。最后要注意的是您所解析的 XML 對象被聲明為 val。這使其成為不可變的對象(像 Java 代碼中的字符串),這在 Scala 中是很常見(jiàn)的。把 XML 作為一個(gè)不可變的對象有很多優(yōu)點(diǎn),但是如果您習慣在 DOM 中使用 appendChild API,那么則很難適應這一點(diǎn)?,F在已經(jīng)從 FriendFeed 中解析了 XML,可以開(kāi)始使用 Scala 對其劃分。
![]() ![]() |
許多編程語(yǔ)言將 XML 表示為 DOM 樹(shù)。這個(gè)方法有許多優(yōu)點(diǎn),但是不利于以編程的方式遍歷樹(shù)來(lái)從 XML 文檔中提取數據。Java 技術(shù)提供了可以利用 XPath 語(yǔ)法的庫。Scala 采取相似的方法,但它有許多優(yōu)點(diǎn)。Scala 在這個(gè)方法中體現了很多函數語(yǔ)言特征。在 Scala 中沒(méi)有使用操作符(像 + 或 *)。相反,使用 + 或 * 等符號定義可以執行普通數字加減法的函數。這也意味著(zhù)您可以定義任何類(lèi)型的操作符(因為它們實(shí)際上就是函數)。這些操作符號比 C++ 這類(lèi)語(yǔ)言中的重載操作符具有更強大的功能。在 XPath 中,由于可以被轉換成一個(gè)函數調用,您可以在 Scala 中直接應用 XPath 語(yǔ)法的某一部分。
了解了這些內容,我們來(lái)看一下 FriendFeed 中的 XML 是什么樣子。清單 3 提供了一個(gè)例子。
<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> |
對于您的應用程序,您將首先得到一個(gè)基于某種服務(wù)的用戶(hù)列表。因此,將首先過(guò)濾提要,從而只獲得感興趣的服務(wù)。查看 清單 4 了解 Scala 如何實(shí)現上述功能。
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 接受一個(gè) XML 元素(提要)和一個(gè)服務(wù) ID 作為參數。首先創(chuàng )建一個(gè)稱(chēng)為 results 的 XML 節點(diǎn)隊列。隊列被參數化,類(lèi)似 Java 中的 List 和 Map。 Scala 使用方括號來(lái)表示泛型類(lèi)型,而不是像 Java 編程使用的尖括號。feed\"entry" 行是一個(gè)類(lèi) XPath 表達式。反斜杠符號實(shí)際上是 scala.xml.Elem 類(lèi)的一個(gè)方法。它返回具有給定名稱(chēng)的所有子節點(diǎn),即提要中所有 <entry> 元素。這將作為一個(gè) scala.xml.NodeSeq 類(lèi)的實(shí)例返回。這個(gè)類(lèi)擴展了 Seq[Node]。因為它是一個(gè) Seq,它具有一個(gè) foreach方法,并將一個(gè)閉包作為參數。
(entry) => ... 標記表示一個(gè)將單個(gè)參數標記為條目的閉包。在這個(gè)閉包中,您將再次使用類(lèi) XPath 表達式 entry\"service"\"id" 來(lái)從 entry 節點(diǎn)提取服務(wù)的 ID。把服務(wù) ID 傳遞給搜索函數來(lái)將其與傳遞給方法的提要 ID 相比較。我們稍后將查看這個(gè)函數體。如果匹配的話(huà),您可將創(chuàng )建條目的用戶(hù)別名添加到結果隊列中。注意這個(gè)隊列目標中類(lèi)似操作符的符號,+=。再次聲明這僅僅是一個(gè)隊列對象的函數。您可以使用 Scala 的類(lèi) XPath 語(yǔ)法來(lái)提取用戶(hù)別名節點(diǎn)。
現在參看搜索函數,這個(gè)函數使用一個(gè)功能最強大的 Scala 特性:模式匹配。在這種情況下,將輸入節點(diǎn)與一個(gè)名為 id 的節點(diǎn)相比較,id 節點(diǎn)的子文本節點(diǎn)由傳遞給函數的 Name 字符串構成。如果匹配則函數返回 true。語(yǔ)法 case _ 和所有內容匹配。其中__再次用作 Scala 的通配符。諸如 case _ 這樣的聲明和 Java 或 C++ 代碼中 case 語(yǔ)句的默認子句類(lèi)似。這個(gè)簡(jiǎn)單的例子證明了 Scala 中模式匹配的強大功能。下面您將會(huì )明白如何構建 XML 結構。
![]() ![]() |
在應用程序中,您需要為從 FriendFeed 公共提要提取出的所有用戶(hù)別名構建一個(gè)新的 XML 結構。實(shí)現上述操作有許多方法,但我們將演示如何再一次使用模式匹配方法??匆幌?清單 5 中所示的函數。
def add(p:Node, newEntry:Node ):Node = p match { case <UserList>{ ch @ _* }</UserList> => <UserList>{ ch }{ newEntry }</UserList> } |
這個(gè)模式將會(huì )和一個(gè)具有任意類(lèi)型的子節點(diǎn)的 UserList 元素匹配。繼而返回一個(gè)具有相同子節點(diǎn)的新 UserList 元素,另外在現有子節點(diǎn)之后又增加了一個(gè)子節點(diǎn)。這在功能上等效于 DOM 規范中的 appendChild 用法。但它有本質(zhì)的不同,因為原始節點(diǎn)沒(méi)有改變(它也不能改變,因為它是不可變的)。相反創(chuàng )建并返回了一個(gè)新節點(diǎn)。這樣比等效的 DOM 操作使用更多的內存。我們來(lái)看一下使用 Scala 構建 XML 結構的其他方法。
![]() ![]() |
當創(chuàng )建新的 XML 文檔時(shí),Scala 的原生 XML 語(yǔ)法再合適不過(guò)。第一個(gè)例子是獲取創(chuàng )建的 UserList 結構并把它封裝在相關(guān)服務(wù)的節點(diǎn)中。清單 6 顯示了這些代碼。
def results(name:String, cnt:Int, elements:NodeSeq):Any = { if (cnt > 0){ return <Service id={name}>{elements}</Service> } } |
由于 Scale 提供了對 XML 的原生支持,您可以利用一個(gè)模板樣式的語(yǔ)法將動(dòng)態(tài)數據插入到 XML 結構中。在本例中,使用傳入的名稱(chēng)字符串設置 id 屬性。您將獲得一串傳入的元素,將它們作為正在創(chuàng )建的 Service 元素的子節點(diǎn)。但是要注意,只有在 cnt 參數大于 0 的情況下才執行上述操作。如果 cnt 值等于 0,這個(gè)函數將不返回任何值。在 Scale 中您可以通過(guò)聲明函數返回 Any 來(lái)解決這個(gè)問(wèn)題。Any 類(lèi)在 Scala 中是一個(gè)原始的類(lèi),類(lèi)似于 java.lang.Object。Scale 沒(méi)有 void 類(lèi)型,但是有一個(gè)等價(jià)的 Unit 類(lèi)型。它的優(yōu)點(diǎn)是可以擴展 Any 類(lèi),并且允許函數在某些情況下返回對象,而在其他時(shí)候不返回任何內容。
如您所見(jiàn),在 Scala 的 XML 語(yǔ)法中結合動(dòng)態(tài)數據可以產(chǎn)生強大的功能。再舉一個(gè)例子,您可以創(chuàng )建一個(gè)統計 XML 文檔,其中顯示的 XML 描述每個(gè)服務(wù)在提要中出現的次數。代碼如 清單 7 所示。
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 的鍵是服務(wù)的名稱(chēng),其值為服務(wù)在 FriendFeed 中出現的次數。這個(gè)函數使用熟悉的 foreach-closure 風(fēng)格遍歷 HashMap,然后使用 HashMap 的名稱(chēng)/值對創(chuàng )建一個(gè)新節點(diǎn),將這個(gè)節點(diǎn)添加到節點(diǎn)隊列中。隨后創(chuàng )建 Stats 結構并作為動(dòng)態(tài)數據訪(fǎng)問(wèn)節點(diǎn)隊列,節點(diǎn)隊列隨后被賦值給一個(gè) XML 結構?,F在準備好了所有函數,您只需驅動(dòng)程序以便進(jìn)行測試。
![]() ![]() |
在運行程序之前,需要加入一些代碼來(lái)驅動(dòng)它。將創(chuàng )建一個(gè) main 方法,就像使用 Java 編程一樣,如 清單 8 所示。
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)) } |
這個(gè)方法創(chuàng )建了 FriendFeed。它接受命令行參數確定哪些服務(wù)查找用戶(hù)并計算統計數據。注意這些語(yǔ)法與 Java 語(yǔ)法非常相似。main 函數接受一個(gè) String 數組(稱(chēng)為 args)作為參數。這個(gè)程序為統計文檔創(chuàng )建 HashMap,并且為每個(gè)服務(wù)創(chuàng )建 UserList 文檔。然后輸出每個(gè) UserList 和統計文檔。要運行這個(gè)程序,需要使用 scalac FriendFeed.scala 和 scala FriendFeed 進(jìn)行編譯,如 清單 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> |
您當然可以選擇不同的服務(wù)名稱(chēng)作為命令行參數或其他參數。Scala 具備完美的 printer 類(lèi),可以使用正確的空格、制表符和格式打印 XML。還提供了 XML 寫(xiě)入程序(writer)將 XML 寫(xiě)回數據流,比如文件。您可以使用 Scala 完成所有普通的任務(wù),同時(shí)還可以使用 Scala 提供的一些獨有的功能。
![]() ![]() |
許多人把 Scala 視為 Java 編程語(yǔ)言發(fā)展歷程中的重要一步。XML 已經(jīng)成為一種重要的技術(shù),編程語(yǔ)言只有在其語(yǔ)法中內置了 XML 支持,才能自然地應用 XML 技術(shù)。而 Scale 做到了這一點(diǎn)。它使得復雜問(wèn)題變得簡(jiǎn)單。查看本文使用 Scale 執行的所有功能,想像一下做同樣的事情需要使用多少行 Java 代碼。
![]() ![]() |
| 描述 | 名字 | 大小 | 下載方法 |
|---|---|---|---|
| 本文的樣例代碼 | friendfeed.example.zip | 1KB | HTTP |
聯(lián)系客服