14.4 XML,作为一等公民

Scala把XML当作一等公民。因此,不必把XML文档嵌入字符串,直接放入代码即可,就像放一个intDouble值一样。看个例子:

UsingScala/UseXML.scala

  1. val xmlFragment =
  2. <symbols>
  3. <symbol ticker="AAPL"><units>200</units></symbol>
  4. <symbol ticker="IBM"><units>215</units></symbol>
  5. </symbols>
  6. println(xmlFragment)
  7. println(xmlFragment.getClass())

这里创建了一个val,叫做xmlFragment,直接把一些XML样例内容赋给它。Scala解析了XML的内容,欣然创建了一个scala.xml.Elem的实例,输出如下:

  1. <symbols>
  2. <symbol ticker="AAPL"><units>200</units></symbol>
  3. <symbol ticker="IBM"><units>215</units></symbol>
  4. </symbols>
  5. class scala.xml.Elem

Scala的scala.xml包提供一些类,用以读取、解析、创建和存储XML文档。我想让你看一下XML的一个主要原因是它解析起来更容易。我们来看一下它到底有多容易。

你或许用过XPath,它提供了一种强大的查询XML文档的方式。Scala提供了一种类似XPath的查询能力,但略有差异。对于辅助解析和提取的方法,Scala不用斜线(///)查询,而用反斜线(\\)。这个差异是必需的,因为Scala遵循Java的传统,使用两个斜线表示注释。这样,我们看一下如何解析手头上的这个XML片段。

首先,要获取symbol这个元素。为了做到这一点,可以使用类似XPath的查询,如下:

UsingScala/UseXML.scala

  1. var symbolNodes = xmlFragment \ "symbol"
  2. println(symbolNodes.mkString("\n"))
  3. println(symbolNodes.getClass())

上面代码的输出如下:

  1. <symbol ticker="AAPL"><units>200</units></symbol>
  2. <symbol ticker="IBM"><units>215</units></symbol>
  3. class scala.xml.NodeSeq$$anon$2

这里调用了XML元素的()方法,让它找出所有symbol元素。它会返回一个scala.xml.NodeSeq的实例,表示XML节点的集合。

()方法只会找出是目标元素(本例子里的symbols元素)直接后代的元素。如果想从目标元素出发,搜出层次结构里所有元素,就要用\()方法,如下所示。此外,用text()方法可以获取元素里的文本节点。

UsingScala/UseXML.scala

  1. var unitsNodes = xmlFragment \\ "units"
  2. println(unitsNodes.mkString("\n"))
  3. println(unitsNodes.getClass())
  4. println(unitsNodes(0).text)

上面代码的输出如下:

  1. <units>200</units>
  2. <units>215</units>
  3. class scala.xml.NodeSeq$$anon$2
  4. 200

在上面的例子里,使用了text()方法去获取文本节点。模式匹配也可以获取文本值以及其他内容。如果想浏览XML文档的结构,()\()两个方法很有用。不过,如果想匹配XML文档任意位置的内容,模式匹配会显得更有用。

在第9章,“模式匹配和正则表达式”,我们见识到了模式匹配的威力。Scala也将这个威力扩展到了XML片段的匹配上,如下所示:

UsingScala/UseXML.scala

  1. unitsNodes(0) match {
  2. case <units>{numberOfUnits}</units> => println("Units: " + numberOfUnits)
  3. }

上面代码的输出如下:

  1. Units: 200

这里,取出第一个units元素,让Scala提取出文本值200。在case语句里,我们对感兴趣的片段进行匹配,用一个变量numberOfUnits,当做这个元素的文本内容的占位符。

这样就可以获取到一支股票的股份了。不过还是有两个问题。前一种方式只对内容刚好匹配case里表达式的情况起作用;也就是说,units元素只能包含一个内容项或是一个子元素。如果它包含的是子元素和文本内容的混合体,上面的匹配就会失败。而且,这里想得到的是所有股票的股份,而不只是第一个。运用_*,就可以让Scala抓出所有的内容(元素和文本),如下所示:

UsingScala/UseXML.scala

  1. println("Ticker\tUnits")
  2. xmlFragment match {
  3. case <symbols>{symbolNodes @ _* }</symbols> =>
  4. for(symbolNode @ <symbol>{_*}</symbol> <- symbolNodes) {
  5. println("%-7s %s".format(
  6. symbolNode \ "@ticker", (symbolNode \ "units").text))
  7. }
  8. }

上面代码的输出如下:

  1. Ticker Units
  2. AAPL 200
  3. IBM 215

这是段密度相当大的代码,需要花些时间来理解。

通过使用_*,把之间所有的内容都读到了占位符变量symbolNodes里。在9.3节,“匹配元组和list”,我们见过一个例子,用到了在符号@前放变量名。好消息是它会读出所有内容。坏消息是它读出了所有内容,包括XML片段里表示空格的文本节点(如果你用过XML DOM解析器,应该相当习惯这种问题)。所以,对symbolNodes循环时,需要再一次运用模式匹配,只迭代symbol元素,这次是在for()方法的参数里。记住,提供给for()方法的第一个参数是一个模式(参见8.5节,“for表达式”)。最后,执行XPath查询,获取ticker属性(重新通过XPath采集,用@前缀表示属性查询)和units元素中的文本值。