项目工程中使用 XML 作为配置文件很常见,比如 Android 项目中的 AndroidManifest.xml
和 Maven 项目中的 pom.xml
,我们在编写脚本工具时经常需要对这类 XML 文件进行读写操作。
XML 的解析有很多种方法,但总体来说都比 JSON 要复杂。今天来介绍一个我常用的 XML 解析库——Dom4J。
为了避免引起逻辑上的混乱,下面我会分几个例子介绍其用法。
添加依赖:
dependencies {
implementation("org.dom4j:dom4j:2.1.4")
}
首先看如何读取 XML,一般情况下我们会读取 XML 文件:
fun parseXml(file: File) {
val document = SAXReader().read(file)
// ...
}
SAXReader
从 SAX 解析事件创建 Dom4J 树,通过 read()
方法读取 XML 并生成一个 Document
对象。
read()
不仅支持 File
类型,其他的如 URL
、InputStream
、Reader
等也是可以的。
如果希望从字符串中解析,可以换一种方式:
fun parseXml(xml: String) {
val document = DocumentHelper.parseText(xml)
// ...
}
读取 XML 内容后,我们通过这个 Document
对象,就可以获取到里面的节点。
举个例子,我们解析如下 XML:
<school>
<student>
<name>Tom</name>
<age>18</age>
</student>
</school>
代码如下:
fun parseDocument(document: Document) {
val rootElement = document.rootElement
val student = rootElement.element("student")
val name = student.elementText("name")
val age = student.element("age").text
// ...
}
通过 Document
的 getRootElement()
方法拿到根节点,每个节点都是一个 Element
对象,调用其 element()
方法可以获取子节点。最内层的节点,我们有两种方式获取它的值,一种是对该节点的父节点调用 elementText()
方法,可以直接根据子节点的名称获取到值;另一种是先获取到该子节点 Element
对象再调用 getText()
方法获取值。所获取到的值都是 String
类型,按需转换。
在 Android 中我们常见的配置往往是通过属性配置的,比如:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="warp_content"
android:background="@color/white"
tools:background="@color/black" />
</FrameLayout>
这是一个布局文件,解析其属性的方法如下:
fun parseDocument(document: Document) {
val rootElement = document.rootElement
val textView = rootElement.element("TextView")
val id = textView.attributeValue("id")
// ...
}
获取到对应元素的 Element
后,调用 attributeValue()
方法传入属性名获取对应的值,可以看到,虽然该 XML 中每个属性都指定了命名空间,也就是前面的 android:
,但我们并不需要传入完整的 android:id
才能获取到该属性的值,恰恰相反,如果把命名空间也写上反而获取不到,这是因为命名空间需要额外配置。
相信你已经注意到,上方对于 background
属性,我在两个命名空间下都有定义,分别是 android:background
和 tools:background
,如果直接调用 attributeValue("background")
,你猜它会获取到哪个值?
实际上,哪个定义排在前面,它就会获取哪个属性的值。
对于不同命名空间的属性,我们可以通过以下方法进行获取:
fun parseDocument(document: Document) {
val rootElement = document.rootElement
val toolsNamespace = Namespace.get("tools", "http://schemas.android.com/tools")
val androidNamespace = Namespace.get("android", "http://schemas.android.com/apk/res/android")
val textView = rootElement.element("TextView")
val toolsBackground = textView.attributeValue(QName.get("background", toolsNamespace))
val androidBackground = textView.attributeValue(QName.get("background", androidNamespace))
// ...
}
先传入前缀和其 Uri
构建对应的命名空间 Namespace
,获取属性值时,我们不再简单粗暴地传入属性名,而是通过 Namespace
构建 QName
,QName
是 XML 元素或属性的限定名称值,它能够帮助我们根据命名空间解析正确的值。
对于一些相同标签的组,我们无法直接通过标签名直接获取到我们想要的值,可以一次性把这个组下所有的标签都获取到,再根据条件筛选。
比如 Android 中用到的:
<application xmlns:android="http://schemas.android.com/apk/res/android">
<activity android:name=".MainActivity" />
<activity android:name=".SplashActivity" />
<activity android:name=".AboutActivity" />
</application>
通过 Element
的 elements()
可以获取其所有的子节点。
fun parseDocument(document: Document) {
val rootElement = document.rootElement
val activities = rootElement.elements()
activities.forEach {
if (it.attribute("name").text.contains("MainActivity")) {
// ...
}
}
}
获取所有属性也是一样的操作,调用 attributes()
即可,不再赘述。
另外还可以通过 elementIterator()
和 attributeIterator()
获取迭代器,使用迭代器遍历来完成操作。
以上就是使用 Dom4J 对 XML 的读操作,在这几个例子中我们可以发现,主要操作的几个对象无非就是 Document
、Element
和 Attribute
,接下来的写操作大同小异。
fun createDocument(): Document {
val document = DocumentHelper.createDocument()
val root = document.addElement("root")
val author = root.addElement("author")
.addAttribute("location", "UK")
.addText("James Strachan")
return document
}
节点、属性的添加与修改,都可以通过 addElement()
和 addAttribute()
来完成,需要注意的是,如果涉及到命名空间,依旧要借助 QName
。
为了方便日志打印或其他操作,可以将 Document
转为 XML 字符串:
fun convertToXml(document: Document) {
val text = document.asXML()
// ...
}
作为脚本,最重要的当然是回写到文件:
fun write(document: Document) {
FileWriter("output.xml").use {
val writer = XMLWriter(it)
writer.write(document)
writer.close()
}
}
根据路径创建 FileWriter
,再创建出 XMLWriter
并调用 write()
将 Document
写入到路径所对应的文件中,最后不要忘了调用 close()
。
除了写入到文件中外,还可以输出到控制台:
fun log(document: Document) {
val format = OutputFormat.createPrettyPrint()
val writer = XMLWriter(System.out, format)
writer.write(document)
writer.close()
}
这里同时还创建了一个格式美化的 OutputFormat
,创建 XMLWriter
不再借助 FileWriter
,而是传入 System.out
,相信任何一个 Java 开发者对这个都不陌生。
以上就是 Dom4J 的常见用法,更详细的介绍可以查阅官方文档。