几乎每个人都知道XML是什么:XML是一种结构化的,机器可读的文本格式,用于表示信息,可以轻松地检查标签,属性及其相互之间的“语法”(例如,使用DTD)。 这与HTML形成对比,HTML可以包含未关闭的元素(例如,<p> foo <p> bar而不是<p> foo </ p> <p> bar </ p>),并且仍可以处理。 XML曾经只是要用作机器的一种格式,但是它却变成了一种数据表示形式,许多人最终(不幸的是,对他们来说)最终是手工编辑的。
但是,即使以机器可读的格式存在问题,例如比实际所需的冗长得多,当您需要在机器之间传输大量数据时,这一点也很重要-在下一篇文章中,我将讨论JSON和Avro,可以看作是XML的演进,并且对于在“大数据”上下文中至关重要的许多应用程序而言,它们的工作效果要好得多。 无论如何,有许多以XML形式生成的遗留数据,并且仍有许多社区(例如数字人文社区)仍然喜欢XML,因此从事任何合理数量的文本分析工作的人们可能会发现自己最终需要使用XML编码的数据。
关于XML和Scala的教程很多,只需在Web上搜索“ Scala XML”就可以了。 与其他博客文章一样,该文章的目的是非常明确,以便初学者可以看到包含所有步骤的示例,并且我将使用它来设置JSON处理文章。
XML的简单示例
首先,让我们考虑一个创建和处理XML的非常基本的示例。
关于Scala中XML的第一件事是Scala可以处理XML文字。 也就是说,您不需要在XML字符串周围加上引号,而是可以直接将它们括起来,Scala会自动将它们解释为XML元素(类型为scala.xml.Element)。
scala> val foo = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>
foo: scala.xml.Elem = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>
现在让我们对此进行一些处理。 您可以使用text方法获取所有文本。
scala> foo.text
res0: String = hi1yellow
因此,这将所有文本混在一起。 为了使它们之间有空格打印出来,让我们首先获取所有条形节点,然后获取其文本并在该序列上使用mkString 。 要获取条形节点,可以使用\选择器。
scala> foo \ "bar"
res1: scala.xml.NodeSeq = NodeSeq(<bar type="greet">hi</bar>, <bar type="count">1</bar>, <bar type="color">yellow</bar>)
这给了我们一系列直接出现在foo节点下面的bar节点的信息。 请注意, \运算符(选择器)只是XPath中使用的/选择器的镜像。
当然,既然我们有了这样的序列,我们就可以映射它以获得所需的序列。 由于text方法返回节点下的文本,因此我们可以执行以下操作。
scala> (foo \ "bar").map(_.text).mkString(" ")
res2: String = hi 1 yellow
要获取每个节点上的type属性的值,我们可以使用\选择器,后跟“ @type”。
scala> (foo \ "bar").map(_ \ "@type")
res3: scala.collection.immutable.Seq[scala.xml.NodeSeq] = List(greet, count, color)
(foo \ "bar").map(barNode => (barNode \ "@type", barNode.text))
res4: scala.collection.immutable.Seq[(scala.xml.NodeSeq, String)] = List((greet,hi), (count,1), (color,yellow))
请注意, \选择器只能检索您要从中选择的节点的子级。 要任意深入研究以拔出给定类型的所有节点,无论它们在哪里,都可以使用\\选择器。 考虑下面的(零散)XML代码段,其中的“ z”节点处于不同的嵌入级别。
<a>
<z x="1"/>
<b>
<z x="2"/>
<c>
<z x="3"/>
</c>
<z x="4"/>
</b>
</a>
首先让我们将其放入REPL。
scala> val baz = <a><z x="1"/><b><z x="2"/><c><z x="3"/></c><z x="4"/></b></a>
baz: scala.xml.Elem = <a><z x="1"></z><b><z x="2"></z><c><z x="3"></z></c><z x="4"></z></b></a>
如果要获取所有“ z”节点,请执行以下操作。
scala> baz \\ "z"
res5: scala.xml.NodeSeq = NodeSeq(<z x="1"></z>, <z x="2"></z>, <z x="3"></z>, <z x="4"></z>)
当然,我们可以轻松地挖掘出每个z的x属性值。
scala> (baz \\ "z").map(_ \ "@x")
res6: scala.collection.immutable.Seq[scala.xml.NodeSeq] = List(1, 2, 3, 4)
在上述所有过程中,我们都使用了XML文字-即直接键入Scala的表达式,后者将它们解释为XML类型。 但是,我们通常需要处理保存在文件或字符串中的XML,因此scala.xml.XML对象具有几种从其他来源创建scala.xml.Elem对象的方法。 例如,以下内容使我们可以从字符串创建XML。
scala> val fooString = """<foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>"""
fooString: java.lang.String = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>
scala> val fooElemFromString = scala.xml.XML.loadString(fooString)
fooElemFromString: scala.xml.Elem = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>
此Elem与使用XML文字创建的Elem相同,如以下测试所示。
scala> foo == fooElemFromString
res7: Boolean = true
有关其他创建XML元素的方法,例如从InputStreams,Files等,请参见Scala XML对象 。
更丰富的XML示例
作为一些要处理的XML的更有趣的示例,我创建了以下简短的XML字符串,描述了艺术家,专辑和歌曲,您可以在github gist music.xml中看到这些字符串 。
https://gist.github.com/2597611
除了确保它具有嵌入的标签(其中一些具有属性以及一些相当有趣的内容(以及一些很棒的歌曲))之外,我没有对此特别注意。
您应该将其保存在名为/tmp/music.xml的文件中。 完成此操作后,您可以运行以下代码,该代码仅打印出每个艺术家,专辑和歌曲,并为每个级别缩进。
val musicElem = scala.xml.XML.loadFile("/tmp/music.xml")
(musicElem \ "artist").foreach { artist =>
println((artist \ "@name").text + "\n")
val albums = (artist \ "album").foreach { album =>
println(" " + (album \ "@title").text + "\n")
val songs = (album \ "song").foreach { song =>
println(" " + (song \ "@title").text)
}
println
}
}
将对象与XML相互转换
XML的一种使用情况是为对象提供一种机器可读的序列化格式,该格式仍可以由人类轻松读取和编辑。 将对象从内存中转换为磁盘格式(如XML)的过程称为封送处理。 我们从一些XML开始,所以我们要做的是定义一些类并将XML“解组”到这些类的对象中。 将以下内容放入REPL。 (提示:您可以使用“ :paste ”输入如下所示的多行语句。这些语句无需粘贴即可工作,但是在某些情况下(例如,如果您在Song之前定义Artist,则必须使用它。)
case class Song(val title: String, val length: String) {
lazy val time = {
val Array(minutes, seconds) = length.split(":")
minutes.toInt*60 + seconds.toInt
}
}
case class Album(val title: String, val songs: Seq[Song], val description: String) {
lazy val time = songs.map(_.time).sum
lazy val length = (time / 60)+":"+(time % 60)
}
case class Artist(val name: String, val albums: Seq[Album])
非常简单明了。 请注意,使用懒惰值来定义诸如歌曲的时间(以秒为单位)之类的内容。 这样做的原因是,如果我们创建一个Song对象但从不询问其时间,则需要从类似“ 4:38?”之类的字符串中进行计算的代码。 永远不会运行 但是,如果我们懒惰了,那么它将在创建Song对象时进行计算。 另外,我们不想在这里使用def(即使方法成为时间),因为它的值基于长度字符串是固定的。 使用一种方法将意味着每次要求特定对象时重新计算时间。
给定上面的类,我们可以手动创建和使用它们的对象。
scala> val foobar = Song("Foo Bar", "3:29")
foobar: Song = Song(Foo Bar,3:29)
scala> foobar.time
res0: Int = 209
使用本机Scala XML API
当然,我们对根据音乐示例等文件中指定的信息构造Artist,专辑和Song对象更感兴趣。 尽管这里没有显示REPL输出,但是您应该在其中输入以下所有命令以查看会发生什么。
首先,请确保已加载文件。
val musicElem = scala.xml.XML.loadFile("/tmp/music.xml")
现在,我们可以使用文件来选择各种元素,或创建上面定义的类的对象。 让我们从歌曲开始。 我们可以忽略所有艺术家和专辑,而直接使用\\运算符进行挖掘。
val songs = (musicElem \\ "song").map { song =>
Song((song \ "@title").text, (song \ "@length").text)
}
scala> songs.map(_.time).sum
res1: Int = 11311
而且,我们可以一路构建并构造Artist,专辑和Song对象,这些对象直接镜像存储在XML文件中的数据。
val artists = (musicElem \ "artist").map { artist =>
val name = (artist \ "@name").text
val albums = (artist \ "album").map { album =>
val title = (album \ "@title").text
val description = (album \ "description").text
val songList = (album \ "song").map { song =>
Song((song \ "@title").text, (song \ "@length").text)
}
Album(title, songList, description)
}
Artist(name, albums)
}
有了艺术家序列,我们可以做一些事情,例如显示每张专辑的长度。
val albumLengths = artists.flatMap { artist =>
artist.albums.map(album => (artist.name, album.title, album.length))
}
albumLengths.foreach(println)
给出以下输出。
(Radiohead,The King of Limbs,37:34)
(Radiohead,OK Computer,53:21)
(Portished,Dummy,48:46)
(Portished,Third,48:50)
将对象编组到XML
除了根据XML规范构造对象(也称为反序列化和取消编组)之外,通常还需要将已经用代码构造的对象编组为XML(或其他格式)。 在这方面,XML文字的使用实际上非常方便。 为了看到这一点,让我们从第一张专辑的第一张专辑的第一首歌开始(Radiohead的Bloom )。
scala> val bloom = artists(0).albums(0).songs(0)
bloom: Song = Song(Bloom,5:15)
我们可以如下构造一个Elem。
scala> val bloomXml = <song title={bloom.title} length={bloom.length}/>
bloomXml: scala.xml.Elem = <song length="5:15" title="Bloom"></song>
这里要注意的是,使用的是XML文字,但是当我们要使用变量中的值时,可以使用大括号将其从文字模式中转出。 因此, {bloom.title}变为“ Bloom”,依此类推。 相反,可以通过String如下进行操作。
scala> val bloomXmlString = "<song title=\""+bloom.title+"\" length=\""+bloom.length+"\"/>"
bloomXmlString: java.lang.String = <song title="Bloom" length="5:15"/>
scala> val bloomXmlFromString = scala.xml.XML.loadString(bloomXmlString)
bloomXmlFromString: scala.xml.Elem = <song length="5:15" title="Bloom"></song>
因此,字面量的使用更具可读性(尽管这样做的代价是在Scala中很难在许多用例中使用“ <”作为运算符,这是许多人都考虑使用XML字面量的原因之一,并不是一个好主意)。
我们可以一口气为所有艺术家和专辑创建整个XML。 请注意,在XML文字的转义括号部分中可以包含XML文字,这使得以下内容可以正常工作。 注意 :您需要在REPL中使用:paste模式,此功能才能起作用。
val marshalled =
<music>
{ artists.map { artist =>
<artist name={artist.name}>
{ artist.albums.map { album =>
<album title={album.title}>
{ album.songs.map(song => <song title={song.title} length={song.length}/>) }
<description>{album.description}</description>
</album>
}}
</artist>
}}
</music>
请注意,在这种情况下,for-yield语法可能不需要更多花括号,因此可读性更高。
val marshalledYield =
<music>
{ for (artist <- artists) yield
<artist name={artist.name}>
{ for (album <- artist.albums) yield
<album title={album.title}>
{ for (song <- album.songs) yield <song title={song.title} length={song.length}/> }
<description>{album.description}</description>
</album>
}
</artist>
}
</music>
当然,可以将toXml方法添加到Song,专辑和Artist类的每个类中,以便在顶层具有类似以下内容。
val marshalledWithToXml = <music> { artists.map(_.toXml) } </music>
这是一个相当普遍的策略。 但是,请注意,此解决方案的问题在于,它会在程序逻辑(例如,歌曲,专辑和艺术家之类的事情可以做到)与其他正交逻辑(例如对其进行序列化)之间产生非常紧密的耦合。 要了解分离这些不同需求的方法,请查看Dan Rosen 关于类型类的出色教程 。
结论
标准的Scala XML API随Scala一起提供,对于某些基本的XML处理来说,它实际上是相当不错的。 但是,这引起了一些“争议”,因为许多人认为核心语言没有为XML之类的格式提供专门处理的业务。 此外,还有一些效率问题。 Anti-XML是一个旨在更好地处理XML的库(尤其是在允许XML的程序化编辑方面更具可伸缩性和灵活性)。 据我了解,随着当前标准XML库的逐步淘汰,Anti-XML可能会在将来成为一种正式的XML处理库。 尽管如此,与上面显示的XML文档进行交互的许多方式都是相似的,因此,熟悉标准的Scala XML API提供了其他此类库所需的核心概念。
参考:来自Bcompose博客的JCG合作伙伴 Jason Baldridge 使用Scala进行的基本XML处理 。
翻译自: https://www.javacodegeeks.com/2012/05/scala-basic-xml-processing.html