在Groovy里,生成和解析xml是一件比较愉快的事情(如果你用过java处理xml的传统方法,你会同意的,即使对比java世界里提供方便的xml处理工具Dom4j,Groovy也有自己的优势)。
XmlSlurper
首先我们来认识XmlSlurper,我们通过一个例子来看下如何使用这个类。
def xmlSource = new File('xmllocation')
def slurper= new XmlSlurper().parse(xmlSource)
parse方法接受一个文件,然后得到一个GPathResult类型的结果,有了这个结果,我们就可以对整个xml文档进行遍历。使用“.”号,我们可以从父节点获得子节点。如果需要对特征节点针对性的查找,在GPathResult里为我们提供了如findAll、find这种可以以查找闭包为参数的方法,使针对性的查找变得简单起来。
下面具体来说一个例子:
<Persons>
<person age="16" name="xiao1">
<city>hangzhou</city>
<link rel="http://test.com/xiao1"/>
</person>
<person age="16" name="xiao2">
<city>hangzhou</city>
<link rel="http://test.com/xiao2"/>
</person>
<person age="16" name="xiao3">
<city>hangzhou</city>
<link rel="http://test.com/xiao3"/>
</person>
<person age="16" name="xiao4">
<city>hangzhou</city>
<link rel="http://test.com/xiao4"/>
</person>
<person age="16" name="xiao5">
<city>hangzhou</city>
<link rel="http://test.com/xiao5"/>
</person>
<person age="16" name="xiao6">
<city>hangzhou</city>
<link rel="http://test.com/xiao6"/>
</person>
</Persons>
这是一个待解析的xml文档,我们可以使用上面说过的XmlSlurper来试试解析它,我们先获得city的值。
def xml = new XmlSlurper()
def result = xml.parse(new File("C:/Users/xiaonanzhi/Person.xml"))
println result.person*.city
注意,parse完成后,得到的起点就是根节点了,所以使用点号导航的时候无需体现Persons这个层级。result.person我们会得到多个节点,这时候使用列表操作符*.可以对多个节点收集信息并返回为一个集合。所以我们得到了city的一个列表如下:
[hangzhou, hangzhou, hangzhou, hangzhou, hangzhou, hangzhou]
然后我们来获得person的名字
println result.person*.@name
使用@属性名的语法,我们就可以轻松获得对应属性的值
[xiao1, xiao2, xiao3, xiao4, xiao5, xiao6]
然后我们来个更常用的,找出name为xiao5的节点,然后把它的link值打印出来
result.person.find{it->
if(it.@name == "xiao5"){
println it.link.@rel
}
}
和期望的一样,我们得到了合适的结果,如下所示:
http://test.com/xiao5
XmlParser
在大部分的使用场景里,XmlParser就像是XmlSlurper的一个孪生兄弟,上面的例子,我们使用XmlParser做一下处理看看。
def xml = new XmlParser()
def result = xml.parse(new File("C:/Users/xiaonanzhi/Person.xml"))
result.person.find{it->
if(it.@name == "xiao5"){
println it.link.@rel
}
}
使用上面的代码,只是改变了类名,我们就得到了相同的结果,当时并不能说明他们是完全相同的。因为内部机制的不同(Slurper是基于SAX模式的解析器,它会完全载入文档,但是对于解析式懒加载的,而Parser则是把xml完全解析成了node,查询起来更快),Slurper对内存的消耗更小一点,如果是要完全处理文档,则Parser的性能会更好一点。另外,如果使用确定类型来接收parse方法的返回值,我们会发现两种解析器返回的结果完全不同,Slurper返回的是GPathResult,而parser返回的是groovy.util.Node。
命名空间处理
简单说了一下常规的解析xml方式,我们还需要谈及一个主题,对命名空间的支持。xml中的命名空间类似于java的包名,起着把相关的标签组织起来的作用,不过在实际的使用过程中,如何能访问命名空间中的元素却成为一个开发者经常面对的问题,让我们来看看groovy中的解决方案。
我们先来定义一份待解析的文档,这是某产品的一个请求结果,为了简化只保留了少量的xml元素。
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:pris="http://www.163.com/pris/1.0" xml:base="http://easyread.163.com">
<entry>
<title>为什么我们会被电影中的替身演员轻易骗过</title>
<pris:assess times="0" />
<pris:image_thumbnail type="image/jpg" href="http://easyread.ph.126.net/vYu8kkZJxtNbBBMbElFdbg==/7916516705336671812.jpg" width="600" height="399" />
<pris:entry_status type="news" style="0" subtype="" />
</entry>
<pris:entry_status type="news" style="0" subtype="" />
</feed>
XmlSlurper的解析过程如下:
1.import groovy.util.slurpersupport.GPathResult
2.def xml = new XmlSlurper()
3.GPathResult result = xml.parse(new File("C:/Users/xiaonanzhi/index.xml"))
4.result.declareNamespace("pris":"http://www.163.com/pris/1.0")
5.println result.entry.image_thumbnail.@type
6.println result.entry."pris:image_thumbnail".@type
值得注意的是,用slurper做解析的时候,实际上5、6两种方式都是能得到正确结果的。即slurper并不是严格遵守命名空间规范的,你可以不使用命名空间来访问节点,不过有时候我们也需要带着前缀来访问,就像6的情况(有时候命名空间去掉有些元素就同名了,这时候要带上命名空间才能访问被屏蔽的那个),在slurper里面,如果需要6的情况能被访问,则必须有标号为4的这条语句,即需要使用方法declareNamespace(Map)来定义前缀才能访问到。
接下来我们看下Parser的情况:
1.def xml = new XmlParser()
2.def result = xml.parse(new File("C:/Users/xiaonanzhi/index.xml"))
//it won't work
3.//println result.entry.image_thumbnail.@type
4.def pris = new Namespace("http://www.163.com/pris/1.0", "pris")
5.//println result.entry."pris:image_thumbnail".@type
6.println result.entry[pris.image_thumbnail].@type
如果不带命名空间前缀,parser是不能获得正确的结果的。使用parser的时候,我们有两种方式来访问命名空间:一种是直接带着命名空间前缀来访问(因为命名空间和元素名称中间的冒号不能直接通过编译,你需要用引号包裹起来);另外一种是创建一个Namespace的对象,通过[]来访问,就像标号为6的代码所示。
xml的创建
接下来我们来讨论硬币的另外一面,如何用groovy来创建xml文档。
创建xml的一个类叫做MarkupBuilder,如果不涉及命名空间的话,这会是非常直观的一个过程。
def st1 = new StringWriter()
MarkupBuilder mb1 = new MarkupBuilder(st);
mb.feed{
entry(id:"1234567"){
title{
show(name:"1234","fdfsa")
}
link:"readf"
}
}
print st
我们来直接看一下运行结果:
<feed>
<entry id='1234567'>
<title>
<show name='1234'>fdfsa</show>
</title>
</entry>
</feed>
如你所见,构建xml的代码本身看起来就已经是xml的结构了,我们只要创建一个MarkupBuilder的实例,然后后面跟着的是xml的root节点,这就开始了整个build过程,
- 如果是属性,我们就用小括号里键值对表达,
- 如果是元素包含的文本,我们就直接把它放在小括号里,
- 如果是子元素,我们就把它放到当前元素后面的大括号里,以此类推。
使用MarkupBuilder,如何加入命名空间呢,也用一段代码来说明:
def st = new StringWriter()
MarkupBuilder mb = new MarkupBuilder(st);
mb.feed('xmlns:pris':'http://www.163.com/pris/1.0'){
entry(id:"1234567"){
'pris:title'{
show("fdfsa")
}
link:"readf"
}
}
print st
首先可以在根节点声明命名空间,然后在后面的节点就可以使用带有命名空间前缀的名称来直接作为节点名称。得到的结果是:
<feed xmlns:pris='http://www.163.com/pris/1.0'>
<entry id='1234567'>
<pris:title>
<show>fdfsa</show>
</pris:title>
</entry>
</feed>
除了MarkupBuilder之外,我们还可以使用StreamingMarkupBuilder来生成xml,这个类比起MarkupBuild来说更加的强大和复杂。
直接使用一个《Programming Groovy2》的例子
def langs = ['C++' : 'Stroustrup', 'Java' : 'Gosling', 'Lisp' : 'McCarthy']
def xmlDocument = new groovy.xml.StreamingMarkupBuilder().bind {
mkp.xmlDeclaration()
mkp.declareNamespace(computer: "Computer")
languages {
comment << "Created using StreamingMarkupBuilder"
langs.each { key, value ->
computer.language(name: key) { author (value) }
}
}
}
println xmlDocument
结果为:
<?xml version='1.0'?>
<languages xmlns:computer='Computer'><!--Created using StreamingMarkupBuilder -->
<computer:language name='C++'>
<author>Stroustrup</author>
</computer:language>
<computer:language name='Java'>
<author>Gosling</author>
</computer:language>
<computer:language name='Lisp'>
<author>McCarthy</author>
</computer:language>
</languages>
后者高级的用法更多,不过写作起来比MarkupBuilder复杂一些,一般的使用用MarkupBuilder就足够了。