14.4 XML,作为一等公民
Scala把XML当作一等公民。因此,不必把XML文档嵌入字符串,直接放入代码即可,就像放一个int
或Double
值一样。看个例子:
UsingScala/UseXML.scala
val xmlFragment =
<symbols>
<symbol ticker="AAPL"><units>200</units></symbol>
<symbol ticker="IBM"><units>215</units></symbol>
</symbols>
println(xmlFragment)
println(xmlFragment.getClass())
这里创建了一个val
,叫做xmlFragment
,直接把一些XML样例内容赋给它。Scala解析了XML的内容,欣然创建了一个scala.xml.Elem
的实例,输出如下:
<symbols>
<symbol ticker="AAPL"><units>200</units></symbol>
<symbol ticker="IBM"><units>215</units></symbol>
</symbols>
class scala.xml.Elem
Scala的scala.xml包提供一些类,用以读取、解析、创建和存储XML文档。我想让你看一下XML的一个主要原因是它解析起来更容易。我们来看一下它到底有多容易。
你或许用过XPath
,它提供了一种强大的查询XML文档的方式。Scala提供了一种类似XPath
的查询能力,但略有差异。对于辅助解析和提取的方法,Scala不用斜线(/
和//
)查询,而用反斜线(\
和\
)。这个差异是必需的,因为Scala遵循Java的传统,使用两个斜线表示注释。这样,我们看一下如何解析手头上的这个XML片段。
首先,要获取symbol
这个元素。为了做到这一点,可以使用类似XPath的查询,如下:
UsingScala/UseXML.scala
var symbolNodes = xmlFragment \ "symbol"
println(symbolNodes.mkString("\n"))
println(symbolNodes.getClass())
上面代码的输出如下:
<symbol ticker="AAPL"><units>200</units></symbol>
<symbol ticker="IBM"><units>215</units></symbol>
class scala.xml.NodeSeq$$anon$2
这里调用了XML元素的()
方法,让它找出所有symbol
元素。它会返回一个scala.xml.NodeSeq
的实例,表示XML节点的集合。
()
方法只会找出是目标元素(本例子里的symbols
元素)直接后代的元素。如果想从目标元素出发,搜出层次结构里所有元素,就要用\()
方法,如下所示。此外,用text()
方法可以获取元素里的文本节点。
UsingScala/UseXML.scala
var unitsNodes = xmlFragment \\ "units"
println(unitsNodes.mkString("\n"))
println(unitsNodes.getClass())
println(unitsNodes(0).text)
上面代码的输出如下:
<units>200</units>
<units>215</units>
class scala.xml.NodeSeq$$anon$2
200
在上面的例子里,使用了text()
方法去获取文本节点。模式匹配也可以获取文本值以及其他内容。如果想浏览XML文档的结构,()
和\()
两个方法很有用。不过,如果想匹配XML文档任意位置的内容,模式匹配会显得更有用。
在第9章,“模式匹配和正则表达式”,我们见识到了模式匹配的威力。Scala也将这个威力扩展到了XML片段的匹配上,如下所示:
UsingScala/UseXML.scala
unitsNodes(0) match {
case <units>{numberOfUnits}</units> => println("Units: " + numberOfUnits)
}
上面代码的输出如下:
Units: 200
这里,取出第一个units
元素,让Scala提取出文本值200
。在case
语句里,我们对感兴趣的片段进行匹配,用一个变量numberOfUnits
,当做这个元素的文本内容的占位符。
这样就可以获取到一支股票的股份了。不过还是有两个问题。前一种方式只对内容刚好匹配case
里表达式的情况起作用;也就是说,units
元素只能包含一个内容项或是一个子元素。如果它包含的是子元素和文本内容的混合体,上面的匹配就会失败。而且,这里想得到的是所有股票的股份,而不只是第一个。运用_*
,就可以让Scala抓出所有的内容(元素和文本),如下所示:
UsingScala/UseXML.scala
println("Ticker\tUnits")
xmlFragment match {
case <symbols>{symbolNodes @ _* }</symbols> =>
for(symbolNode @ <symbol>{_*}</symbol> <- symbolNodes) {
println("%-7s %s".format(
symbolNode \ "@ticker", (symbolNode \ "units").text))
}
}
上面代码的输出如下:
Ticker Units
AAPL 200
IBM 215
这是段密度相当大的代码,需要花些时间来理解。
通过使用_*
,把
和之间所有的内容都读到了占位符变量
symbolNodes
里。在9.3节,“匹配元组和list”,我们见过一个例子,用到了在符号@
前放变量名。好消息是它会读出所有内容。坏消息是它读出了所有内容,包括XML片段里表示空格的文本节点(如果你用过XML DOM解析器,应该相当习惯这种问题)。所以,对symbolNodes
循环时,需要再一次运用模式匹配,只迭代symbol
元素,这次是在for()
方法的参数里。记住,提供给for()
方法的第一个参数是一个模式(参见8.5节,“for表达式”)。最后,执行XPath
查询,获取ticker
属性(重新通过XPath采集,用@
前缀表示属性查询)和units
元素中的文本值。