面试官:如果服务器返回的不是JSON

1,176 阅读6分钟

最近看到一个面试题,是这样的: 面试官:怎么动态判断服务器返回的数据是 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~!

参考文档: