Android Xml解析

XML与JSON解析全攻略

最近看到一个面试题,是这样的:
面试官:怎么动态判断服务器返回的数据是 Xml 还是 Json?(答案见下)

由此有了这一篇文章。

1. 什么是XML

XML (Extensible markup language): XML是一种标记语言,用于存储与传输数据。是常用的数据传输方式,区分大小写,文件扩展名为.xml。XML定义了一组用于以人类可读和机器可读的格式编码文档的规则。XML的设计目标集中在Internet的简单性,通用性和可用性上。它是一种文本数据格式,并通过Unicode对不同的人类语言提供了强大的支持,被W3C所推荐。
例如:

<?xml version="1.0" encoding="utf-8"?>

<Users> 
  <user> 
    <name>James</name>  
    <age>18</age> 
  </user>  
  <user> 
    <name>Mike</name>  
    <age>19</age> 
  </user>  
  <user> 
    <name>John</name>  
    <age>20</age> 
  </user>  
  <user> 
    <name>Peter</name>  
    <age>21</age> 
  </user> 
</Users>

1.1 XML 与 JSON 的区别

JSON (JavaScript Object Notation): 是一种轻量级的数据交换格式,它完全独立于语言。它基于JavaScript编程语言,易于理解和生成。文件扩展名为.json。
例如:

{
   "Users":[
      {
         "name":"James",
         "age":18
      },
      {
         "name":"Mike",
         "age":19
      },
      {
         "name":"John",
         "age":20
      },
      {
         "name":"Peter",
         "age":21
      }
   ]
}

XML 与 JSON 的区别:

JSONXML
是JavaScript对象表示法是可扩展的标记语言
基于JavaScript语言源自SGML
这是一种表示对象的方式是一种标记语言,并使用标签结构表示数据项
JSON类型:字符串,数字,数组,布尔值所有XML数据应为字符串
不使用结束标签具有开始和结束标签
仅支持UTF-8编码支持各种编码
与XML相比,其文件非常易于阅读与理解其文档相对难以阅读和理解
不提供对名称空间的任何支持它支持名称空间
它的安全性较低它比JSON更安全
不支持评论支持评论

参考自:JSON vs XML: What’s the Difference?

OK,这里回到上面的面试题:

面试官:怎么动态判断服务器返回的数据是 Xml 还是 Json?
答: XML 文档必须以 “<” 作为开头,所以只需要根据这个条件来判断即可。
如:if(s.startsWith("<")

2. Android 中怎么解析 XML

在 Android 一般用三种方法来解析 XML,分别是:

  1. XmlPullParser
  2. Dom Parser
  3. SAX Parser

下面详细介绍一下三种解析方法,并实践去解析 user_info.xml 文件,解析出内容,并且显示出来。
user_info.xml 文件如下所示:

<?xml version="1.0" encoding="utf-8"?>

<Users>
    <user>
        <name>James</name>
        <age>18</age>
    </user>
    <user>
        <name>Mike</name>
        <age>19</age>
    </user>
    <user>
        <name>John</name>
        <age>20</age>
    </user>
    <user>
        <name>Peter</name>
        <age>21</age>
    </user>
</Users>

将xml内容解析出来后,显示出来,效果如下所示:

解析效果图

来看看这三种方法的具体实现吧。

2.1 XmlPullParser

Android 建议我们使用 XMLPullParser 来解析xml文件,因为它的解析速度比 SAX 和 DOM 快。

方法:

  • getEventType():返回当前事件的类型(START_TAG,END_TAG,TEXT等)
  • getName():对于 START_TAG 或 END_TAG 事件,启用命名空间后,将返回当前元素的(本地)名称,如"Users"、“user”、“name”、“age”。禁用名称空间处理后,将返回原始名称。

事件类型:

  • START_DOCUMENT:表示解析器位于文档的开头,但尚未读取任何内容。只有在第一次调用next()、nextToken() 或 nextTag() 方法之前调用 getEvent() 才能观察到此事件类型。
  • END_DOCUMENT:表示解析器位于文档末尾。
  • START_TAG:开始标签。
  • END_TAG:结束标签。
  • TEXT:标签内容。

在 user_info.xml 文件中,对应关系如下图所示:
XmlPullParse

具体实现步骤为:

  1. 实例化一个 XmlPullParser 解析器, 并设置该解析器可以处理xml命名空间
  2. 为解析器通过 setInput() 方法输入文件数据流
  3. 判断事件类型,调用 next() 方法循环解析,并通过 getText() 方法提取标签内容,直到解析到文档末尾 END_DOCUMENT

具体代码如下:

class XmlPullParseTestActivity : AppCompatActivity() {
    private val userList: ArrayList<User> = ArrayList()
    private var user: User = User()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_xml_pull_parse_test)

        xmlParseData()
    }

    private fun xmlParseData() {
        try {
            val inputStream: InputStream = assets.open(USER_INFO_XML_FILE)
            val parserFactory =
                XmlPullParserFactory.newInstance()
            val parser = parserFactory.newPullParser()
            //设置XML解析器可以处理命名空间
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
            //设置解析器将要处理的输入流,将重置解析器状态并将事件类型设置为初始值START_DOCUMENT
            parser.setInput(inputStream, null)

            //返回当前事件的类型
            var eventType = parser.eventType
            var parseContentText = ""

            //一直循环解析,直到解析到XML文档结束节点
            while (eventType != XmlPullParser.END_DOCUMENT) {
                //启动了命名空间,所以将返回当前元素的本地名称,如 "Users", "user", "name", "age"
                val tagName = parser.name
                when (eventType) {
                    XmlPullParser.START_TAG -> {
                        if (tagName == USER_TAG) user = User()
                    }
                    XmlPullParser.TEXT -> {
                        //以String形式返回当前事件的文本内容
                        parseContentText = parser.text
                    }
                    XmlPullParser.END_TAG -> {
                        when (tagName) {
                            NAME_TAG -> user.name = parseContentText
                            AGE_TAG -> user.age = parseContentText.toInt()
                            USER_TAG -> userList.add(user)
                        }
                    }

                }
                eventType = parser.next()
            }
            //展示解析出来的内容数据
            for (user in userList) {
                val textContent = "${xmlPullParseShowTv.text} \n\n" +
                        "Name: ${user.name} \n" +
                        "Age: ${user.age} \n" +
                        "------------------\n"
                xmlPullParseShowTv.text = textContent
            }

        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: XmlPullParserException) {
            e.printStackTrace()
        }
    }

}

2.2 DOM Parser

Dom Parser 将使用基于对象的方法来创建和解析 XML文件,直接访问 XML文档的各个部位,Dom解析器会将 XML文件加载到内存中来解析 XML文档,这也将消耗更多的内存(所以不可以用Dom解析器来解析大文件),并且将从起始节点到结束节点解析 XML文档。

在Dom 解析器中,XML文件中每一个标签都是一个元素Element,元素里面的内容又是一个节点Node,如果元素里面有多个节点,就构成了节点列表NodeList。

在 user_info.xml 文件中的对应关系如下:
Dom Parser

具体实现步骤:

  1. 实例化一个 DocumentBuilder 对象,通过 parse() 方法,将文档数据流输入
  2. 通过 getDocumentElement() 方法获取文档子节点,并通过 getElementsByTagName(TAG),拿到该标签下的 NodeList
  3. 循环 NodeList,判断 Node 类型,将 Node 转换成 Element,然后在重复上一步操作,最终将 Node.value 也就是标签内容取出

具体代码:

class DomParseTestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_dom_parse_test)

        domParseData()
    }

    private fun domParseData() {
        try {
            val inputStream = assets.open(USER_INFO_XML_FILE)
            val dbFactory = DocumentBuilderFactory.newInstance()
            //实例化一个 DocumentBuilder 对象
            val dBuilder = dbFactory.newDocumentBuilder()

            //将输入流解析成 Document
            val doc = dBuilder.parse(inputStream)
            //直接访问文档元素子节点
            val element = doc.documentElement
            //标准化子节点元素
            element.normalize()

            //拿到所有 'user' 标签下的节点
            val nodeList = doc.getElementsByTagName(USER_TAG)
            for (i in 0 until nodeList.length) {
                val node = nodeList.item(i)
                //判断是否是元素节点类型
                if (node.nodeType == Node.ELEMENT_NODE) {
                    //将 node 转换成 Element 类型,为了通过getElementsByTagName()方法获取到节点数据
                    val theElement = node as Element
                    //展示解析出来的内容数据
                    val textContent = "${domParseShowTv.text} \n\n" +
                            "Name: ${getValue(NAME_TAG, theElement)} \n" +
                            "Age: ${getValue(AGE_TAG, theElement)} \n" +
                            "------------------\n"

                    domParseShowTv.text = textContent
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun getValue(tag: String, element: Element): String {
        //根据指定的标签取出相对应的节点
        val nodeList = element.getElementsByTagName(tag).item(0).childNodes
        val node = nodeList.item(0)
        return node.nodeValue
    }
}

2.3 SAX Parser

SAX(Simple API for XML): 是基于事件的解析器。与 DOM解析器不同,SAX解析器不创建任何解析树,会按顺序接收处理元素的事件通知,从文档顶部开始,尾部结束。

优点:我们可以指示 SAX解析器在文档中途停止而不丢失已收集的数据。
缺点:不能随机访问 XML文档,因为它是以只向前的方式处理的。

那什么时候使用 SAX Parser 解析 XML 文件比较合适呢?

  • 你可以从上到下以线性方式处理XML文档。
  • 要解析的XML文档没有深度嵌套。
  • 你正在处理一个非常大的XML文档,它的DOM树将消耗太多内存。典型的DOM实现使用10字节内存来表示XML的一个字节。
  • 你要解决的问题只涉及XML文档的一部分,即你只需要解析XML文件中的一部分内容。

参考自:Java SAX Parser - Overview

我们要实现的方法为有:

  • startDocument:接收文档开始的通知。
  • endDocument:接收文档结尾的通知。
  • startElement:接收元素开始的通知。
  • endElement:接收元素结束的通知。
  • characters:接收元素内部字符数据的通知。

在 user_info.xml 文件中,对应关系如下图所示:
SAXParser

具体实现步骤为:

  1. 实例化 SAXParser 对象。
  2. 重写一个 DefaultHandler, 实现startElement, endElement, characters方法。
  3. 将文档输入流与Handler传递给 SAXParser.parse() 方法进行解析。
class SaxParseActivity : AppCompatActivity() {
    var userList: ArrayList<User> = ArrayList()
    var user: User = User()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sax_parse)

        saxParseData()
    }

    private fun saxParseData() {
        try {
            val parserFactory: SAXParserFactory = SAXParserFactory.newInstance()
            val parser: SAXParser = parserFactory.newSAXParser()

            val inputStream: InputStream = assets.open(USER_INFO_XML_FILE)
            //使用指定的DefaultHandler将给定的输入流内容解析为XML。
            parser.parse(inputStream, Handler())

            //展示解析出来的内容数据
            for (user in userList) {
                val textContent = "${saxParseShowTv.text} \n\n" +
                        "Name: ${user.name} \n" +
                        "Age: ${user.age} \n" +
                        "------------------\n"
                saxParseShowTv.text = textContent
            }

        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: ParserConfigurationException) {
            e.printStackTrace()
        } catch (e: SAXException) {
            e.printStackTrace()
        }
    }

    inner class Handler : DefaultHandler() {
        private var currentValue = ""
        var currentElement = false

        /**
         * 文档开始
         */
        override fun startDocument() {
            super.startDocument()
        }

        /**
         * 文档结尾
         */
        override fun endDocument() {
            super.endDocument()
        }

        /**
         * 元素开始
         * localName: 返回我们定义的标签,即:"Users","user", "name", "age"
         */
        override fun startElement(
            uri: String?,
            localName: String?,
            qName: String?,
            attributes: Attributes?
        ) {
            super.startElement(uri, localName, qName, attributes)
            //一个新的节点,即一个新的 user 节点
            currentElement = true
            currentValue = ""
            if (localName == USER_TAG) {
                user = User()
            }
        }

        /**
         * 当前元素结束
         * localName: 返回我们定义的标签,即:"Users","user", "name", "age"
         */
        override fun endElement(uri: String?, localName: String?, qName: String?) {
            super.endElement(uri, localName, qName)
            //当前元素解析完成
            currentElement = false
            when(localName) {
                NAME_TAG -> user.name = currentValue
                AGE_TAG -> user.age = currentValue.toInt()
                //即第一个 user 节点解析好后,加入到 userList
                USER_TAG -> userList.add(user)
            }

        }

        /**
         * 接收元素内部字符数据
         * ch: 返回标签内的内容,以 char[] 的形式存储
         * start: ch的起始Index, 即为0
         * length: ch数组的长度
         */
        override fun characters(ch: CharArray?, start: Int, length: Int) {
            super.characters(ch, start, length)
            if (currentElement) {
                //以String的形式返回标签内的内容
                currentValue += String(ch!!, start, length)
            }
        }
    }

}

以上文章中的全部完整代码见 Github:ParseXml

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。

另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!

参考文档:
Android XML Parsing using SAX Parser
Android XML Parsing using DOM Parser
Android XMLPullParser Tutorial
Android–解析XML之DOM
Android–解析XML之PULL
Android–解析XML之SAX

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值