残月的技术日志

残月的技术日志

Scala 解析 XML

2024-03-16

本文主要了解如何使用Scala解析XML

Scala 原生方法

XML基本结构

<school address="联榕路8号">
    <strdents>
        <xiaomin strId="202012040293221" sex="男">小明</xiaomin>
        <xiaohua strId="202012040293222" sex="女">小花</xiaohua>
    </strdents>
</school>

根元素:这里 school是根元素

子/父元素:students是school的子元素,school是students的父元素

属性:address是school的属性

文本:小明、小花

scala原生对XML字面量支持

你甚至可以在代码中直接使用XML片段

val elem: Elem =      //scala.xml.Elem
      <a>
        <b>
          hello
        </b>
      </a>

在我们直接写XML的时候,Scala会将其解析并识别为scala.xml.Elem类型

主要的类

Node类。它的一个抽象类,用于对象XML中的所有节点进行抽象:

Text类,仅包含文本的节点,例如<url>http://www.baidu.com/</url> 中的http://www.baidu.com/就是一种Text对象

NodeSeq类,它同样是一个抽象类,指的是节点的序列,Node继承自NodeSeq,可以看Node可作是NodeSeq只有一个元素的情况。

将字符串转成XML.Elem

import scala.xml._
val string="<play><scala></scala></play>"
XML.loadString(string)

//res0: scala.xml.Elem = <play><scala/></play>

获取文本

通过text方法可以获取文本

val elem:Elem = <p>Hello Scala</p>

elem.text

//res0: String = Hello Scala

获取属性

可以通过attributes方法得到当前元素的某个属性

val elem:Elem = <a href="http://www.baidu.com"></a>

elem.attributes("href").text

//res1: String = http://www.baidu.com

也可以通过 \\\ 获取

其实 \ 和 \\ 是 Elem的一个方法

scala允许方法之间不写“.”

val elem:Elem = <a href="http://www.baidu.com"></a>

elem \ "@href"      //带上@符号,让scala知道你要查询的是属性

//res2:  scala.xml.NodeSeq = http://www.baidu.com

遍历元数的属性

val elem:Elem = <a href="http://www.baidu.com" id="web"></a>

for(arr <- elem.attributes) println(arr)
href="http://www.baidu.com" id="web"
id="web"

获取属性(Map类型)

val elem:Elem = <a href="http://www.baidu.com" id="web"></a>

elem.attributes.asAttrMap

// res3: Map[String,String] = Map(href -> http://www.baidu.com, id -> web)

进入子元素

可以通过 \\\ 进入当前节点下的某个子节点

val elem:Elem = <div><p>Hello Scala</p></div>

val newElem = elem \\ "p"

//newElem: scala.xml.NodeSeq = NodeSeq(<p>Hello Scala</p>)

newElem.text
// res4: String = Hello Scala

序列化与反序列化

import scala.language.postfixOps
import scala.xml.Elem

object test5 {
  def main(args: Array[String]): Unit = {

    val mimi = new Cat("咪咪", "black")

    //序列化
    val mimiElem =  mimi.toXML()
    println(mimiElem \ "name" text)

    //反序列化
    mimi.fromXML(mimiElem).jump()

  }
}


class Cat(name:String,color:String){
  //序列化
  def toXML():Elem ={
    <Cat>
      <name>{name}</name>
      <color>{color}</color>
    </Cat>
  }
  //反序列化
  def fromXML(xml:Elem): Cat ={
    new Cat(xml \ "name" text, xml \ "color" text)
  }
  //跳
  def jump(): Unit ={
    println(name + "一蹦跳老高")
  }
}
咪咪
咪咪一蹦跳老高

XML模式匹配

elem match {
  case <Cat><name>{name}</name></Cat> => println(name)                      //匹配单个元数
  case <Cat><name>{name}</name></Cat> => println(name.attribute("enName"))  //匹配属性
  case <Cat>{info}</Cat> => {                                               //匹配多个元素,返回ArrayBuffer
    for (e <- info) println(e.text)      //打印文本
  }
  case _ => print("无匹配")                                                   //未被匹配
}

XML中执行Scala表达式

<sum>{1+2}</sum>
// res5:scala.xml.Elem = <sum>3</sum>

利用Dom4j

Dom4J是dom4j.org出品的应用于Java上的一个开源的XML解析包

由于Scala基于Java,故也可使用

DOM4J 最大的特色是使用大量的接口。它的主要接口都在org.dom4j里面定义:

Attribute

定义了 XML 的属性。

Branch

指能够包含子节点的节点。如XML元素(Element)和文档(Docuemnts)定义了一个公共的行为

CDATA

定义了 XML CDATA 区域

CharacterData

是一个标识接口,标识基于字符的节点。如CDATA,Comment, Text.

Comment

定义了 XML 注释的行为

Document

定义了XML 文档

DocumentType

定义 XML DOCTYPE 声明

Element

定义XML 元素

ElementHandler

定义了Element 对象的处理器

ElementPath

被 ElementHandler 使用,用于取得当前正在处理的路径层次信息

Entity

定义 XML entity

Node

为dom4j中所有的XML节点定义了多态行为

NodeFilter

定义了在dom4j 节点中产生的一个滤镜或谓词的行为(predicate)

ProcessingInstruction

定义 XML 处理指令

Text

定义 XML 文本节点

Visitor

用于实现 Visitor模式

XPath

在分析一个字符串后会提供一个 XPath 表达式

在使用之前需要先引入dom4j,以maven为例,在Pom文件添加

<!--dom4j-->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

<!--前者xPath依赖-->
<dependency>
    <groupId>com.google.code.maven-play-plugin.org.allcolor.shanidom</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.1-patched-shani-1.4.17</version>
</dependency>

读取

Document是一个接口类,里面定义了XML文档数据

Dom4J提供了非常多的方法以便使用者能灵活读取各种来源的数据

import org.dom4j.{Document, DocumentHelper}
import org.dom4j.io.{DOMReader, SAXReader}

import java.io.{File, FileInputStream, FileReader, InputStream, Reader}
import java.net.URL

object DomForJavaTest {
  def main(args: Array[String]): Unit = {
    /*---------------------------数据来自XML文件------------------------------*/
    //读取有两种实现方式,一种是SAXReader,还有一种DOMReader,两者调用了同一个接口,所以调用方式是一样的
    //读取的数据将被转换为org.dom4j.Document对象
    val saxReader = new SAXReader()   //一般来说用这种,下面演示的也是这种
    val domReader = new DOMReader()   //这种只能从org.w3c.dom.Document对象中获取数据

    //可以在systemId中直接传入路径
    val document1:Document = saxReader.read("./data.xml")
    //也可传入一个java.io.File对象
    val file:File = new File("./data.xml")
    val document2:Document = saxReader.read(file)
    //也可是一个统一资源定位符(java.net.URL)读取互联网上的XML文件
//    val url: URL = new URL("xxxx")
//    val document3:Document = saxReader.read(url)
    //也可通过通过java的各种读取器(java.io.Reader)
    val fileReader:Reader = new FileReader(file)
    val document4 = saxReader.read(fileReader)
    //结束了吗,不!还可以读输入取流(java.io.InputStream)中的数据
    val fileStream:InputStream = new FileInputStream(file)
    val document5:Document = saxReader.read(fileStream)

    /*---------------------------数据来自字符串------------------------------*/
    //解析字符串的数据需要通过一个文档助手(字面意思)的类实现
    //官方解释该类是Dom4J的一个辅助工具集合
    //注意 他是一个finalClass,请直接使用它
    val str:String =
      """
        |<school>
        |    <student id="1001">
        |        <name>小明</name>
        |        <sex>男</sex>
        |    </student>
        |    <student id="1002">
        |        <name>小花</name>
        |        <sex>女</sex>
        |    </student>
        |    <student id="1003">
        |        <name>小军</name>
        |        <sex>男</sex>
        |    </student>
        |    <student id="1004">
        |        <name>小鹏</name>
        |        <sex>男</sex>
        |    </student>
        |</school>
        |""".stripMargin
    val document6:Document = DocumentHelper.parseText(str)
  }
}

操作

接下来的演示数据

<school>
 <student id="1001">
     <name>小明</name>
     <sex>男</sex>
 </student>
 <student id="1002">
     <name>小花</name>
     <sex>女</sex>
 </student>
 <student id="1003">
     <name>小军</name>
     <sex>男</sex>
 </student>
 <student id="1004">
     <name>小鹏</name>
     <sex>男</sex>
 </student>
</school>

进入

特此补充,TNN的网上写的都是啥啊,只字不提Node与Element两者的的概念

为了防止后人混淆。为此我特意翻了源码

  • Element(元素)

    Element是一个接口,他定义了一个XML元素,各元数中可以声明名称、注释、属性、子节点以及其中的文本内容,这就很想正常的XML格式

  • Node(节点)

    Node是Element的组成部分,他可以是一个元素中的某个子节点甚至只是某个属性,是Element的多态性为,我们可以通过.getNodeTypeName()得到该节点是属性还是文本等别的

    可以将节点转换为XML格式的字符串,还可以对节点本身计算XPath表达式(xPath语法教程略)。

//获取根元素
val rootElement:Element = document
  .getRootElement
//获取下面的所有的student元素
val studentElements:Array[Element] = rootElement
  .elements("student")
  .toArray()
  .map(_.asInstanceOf[Element])
//继续进入id为1002的student元素的name元素
val index = studentElements
  .map(_.attribute("id").getData.asInstanceOf[String])     //拿各元素的id属性(后面细说)
  .indexOf("1002")                                                //得到下标
val tmp = studentElements(index)
  .element("name")
//    println(tmp.getData)         //小花

//    //进入小花在的那个元素(无法使用,正在研究)
//    val xiaoHuaNode:Node = document
//      .selectNodes("//student[@id='1002]")
//      .toArray()
//      .map(_.asInstanceOf[Node])
//      .head
//    println(xiaoHuaNode.asXML())

读取

//读取某个元素的文本
val xiaoMinElement:Element = document
  .getRootElement
  .element("student")             //默认进第一个
val text = xiaoMinElement
  .elementText("name")

//println(text)   //小明

//读取某个元素的某个属性
val attribute:Attribute = xiaoMinElement
  .attribute("id")

//println(attribute.getName)     //id
//println(attribute.getData)    //1001

//遍历节点,得到每个节点的XML
val nodes:mutable.HashMap[String,Any] = mutable.HashMap()
for(i <- Range(0,xiaoMinElement.nodeCount())){
  val node:Node = xiaoMinElement.node(i)
  nodes.put(node.getName,node.asXML())
}

//println(nodes.mkString("|"))
//name -> <name>小明</name>|sex -> <sex>男</sex>|null -> (这有一个\n)

//遍历各个元数
val iterator = xiaoMinElement.elementIterator()    //得到可迭代对象
while (iterator.hasNext){
  val data:String = iterator
    .next()
    .asInstanceOf[Element]
    .getData
    .toString
  println(data)
}

/*
* 小明
* 男
* */

修改

//修改/添加某个元素的属性(在dom4j 1.6+被弃用)
document
  .getRootElement
  .element("student")
  .setAttributeValue("id","2001")   //当属性不存在时将创建
//另一种没被弃用的方法,就是不能新增属性
val attribute = document
  .getRootElement
  .element("student")
  .attribute("id")
attribute
  .setValue("2011")
//添加需要用另一个方法
document
  .getRootElement
  .element("student")
  .addAttribute("type","寄宿生")

//还可以通过节点来做,会更方便,不过遇到点技术问题,无法使用xPath
//    val attribute_2 = document
//      .selectNodes("//student[@id='1002']/@id")
//      .iterator()
//      .next()
//      .asInstanceOf[DefaultAttribute]
//
//    attribute_2
//      .setValue("2012")


/*......
<student id="2011" type="寄宿生">
    <name>小明</name>
    <sex>男</sex>
</student>
......*/

//修改文本内容
document
  .getRootElement
  .element("student")
  .element("name")
  .setText("小徐")

/*......
<student id="2011" type="寄宿生">
    <name>小徐</name>
    <sex>男</sex>
</student>
......*/
//理论上用节点也行

//添加元素
//各个学生都添加一个元素,元素名为class,根据id第一位生成
val iterator = document
  .getRootElement
  .elements()
  .iterator()

while (iterator.hasNext){
  val element:Element = iterator.next().asInstanceOf[Element]
  val stuClass:String = element.attribute("id").getValue.head + "班"
  element   //新增空元素
    .addElement("class")
  element   //设置元素文本
    .element("class")
    .setText(stuClass)
}

/*<school>
<student id="2011" type="寄宿生">
    <name>小徐</name>
    <sex>男</sex>
<class>2班</class></student>
<student id="1002">
    <name>小花</name>
    <sex>女</sex>
<class>1班</class></student>
<student id="1003">
    <name>小军</name>
    <sex>男</sex>
<class>1班</class></student>
<student id="1004">
    <name>小鹏</name>
    <sex>男</sex>
<class>1班</class></student>
</school>
*/


val tmp = document
  .node(0)
  .asXML()
println(tmp)

保存

写入到CSV

文件会被复写,不用担心写入问题

val writer:Writer = new FileWriter("./newData.xml")
    document
      .write(writer)
    writer.close()                  //记得释放资源,不然写不进去

  • 1