最近看到一个面试题,是这样的:
面试官:怎么动态判断服务器返回的数据是 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 的区别:
JSON | XML |
---|---|
是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,分别是:
- XmlPullParser
- Dom Parser
- 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 文件中,对应关系如下图所示:
具体实现步骤为:
- 实例化一个 XmlPullParser 解析器, 并设置该解析器可以处理xml命名空间
- 为解析器通过 setInput() 方法输入文件数据流
- 判断事件类型,调用 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 文件中的对应关系如下:
具体实现步骤:
- 实例化一个 DocumentBuilder 对象,通过 parse() 方法,将文档数据流输入
- 通过 getDocumentElement() 方法获取文档子节点,并通过 getElementsByTagName(TAG),拿到该标签下的 NodeList
- 循环 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 对象。
- 重写一个 DefaultHandler, 实现startElement, endElement, characters方法。
- 将文档输入流与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)
}
}
}
}
其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。
另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~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