11.3--解析XML 格式数据

通常情况下,每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器上获取数据。不过这个时候就出现了一个问题,这些数据到底要以什么样的格式在网络上传输呢?随便传递一段文本肯定是不行的,因为另一方根本就不会知道这段文本的用途是什么。因此,一般我们都会在网络上传输一些格式化后的数据,这种数据会有一定的结构规格和语义,当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而取出他想要的那部分内容。

在网络上传输数据时最常用的格式有两种:XML 和 JSON,下面我们就来一个一个地进行学习,本节首先学习ー下如何解析 XML 格式的数据。

在开始之前我们还需要先解决一个问题,就是从哪儿才能获取一段 XML 格式的数据呢?这里我准备教你搭建一个最简单的 Web 服务器,在这个服务器上提供一段 XML 文本,然后我们在程序里去访问这个服务器,再对得到的 XML 文本进行解析。

搭建 Web 服务器其实非常简单,有很多的服务器类型可供选择,这里我准备使用 Apache 服务器。另外,这里只会演示Windows 系统下的搭建过程,因为Mac 和 Ubuntu 系统都是默认安装好Apache 服务器的,只需要启动一下即可。如果你使用的是这两种系统,可以自行搜索一下具体的操作方法。

下面我们来看Windows 系统下的搭建过程。首先你需要去下载一个 Apache 服务器的安装包,官方下载地址是:http: /httpd. apache.org。如果你在这个网址中找不到 Windows 版的安装包,也可以直接在百度上搜索“Apache 服务器下载”,将会找到很多下载链接。下载完成后双击就可以进行安装了。一直Next 即可,中途会提示让输入自己的域名,我们随便输入一个域名就可以了,还有一个就是提示安装路径,我们自己选择,然后就可以完成安装了。安装成功之后服务器会自动启动,你可以打开的浏览器来验证一下。在地址栏输入 127.0.0.1,如果出现了如图所示的界面,就说明服务器已经启动成功了。

 

接下来进入到 C:\ Apache\htdocs 目录下,在这里新建一个名为 get_data.xml 的文件,然后编辑这个文件,并加入如下 XML 格式的内容。

<app>
    <app>
        <id>1</id>
        <name>Google</name>
        <version>1.0</version>
    </app>
    <app>
        <id>2</id>
        <name>Google</name>
        <version>2.1</version>
    </app>
    <app>
        <id>3</id>
        <name>Google</name>
        <version>2.3</version>
    </app>
</app>

这时在浏览器中访问http://127.0.0.1/get_data.xml 这个网址,就应该出现下图所示的内容。

好了,准备工作到此结束,接下来就让我们在Android 程序里去获取并解压这段XML 数据吧。

 

11.3.1 Pull 解析方式

解析 XML 格式的数据其实也有挺多种方式的,本节中我们学习比较常用的两种,Pull 解析和 SAX 解析。那么简单起见,这里仍然是在 NetworkTest 项目的基础上继续开发,这样我们就可以重用之前网络通信部分的代码,从而把工作的重心放在 XML 数据解析上。

既然 XML 格式的数据已经提供好了,现在要做的就是从中解析出我们想要得到的那部分内容。修改 MainActivity 中的代码,如下所示:

private fun sendRequestWithOkHttp() {
        // 开启线程发起网络请求
        thread {
            try {
                val client = OkHttpClient()
                val request = Request.Builder()
                        // 访问的服务器地址是计算机本机
                    .url("http://10.0.2.2/get_data.xml")
                    .build()
                val response = client.newCall(request).execute()
                val responseData = response.body?.string()
                if (responseData != null){
                    parseXMLWithPull(responseData)
                }
            }catch (e:Exception){
                e.printStackTrace()
            }
        }
    }

    private fun parseXMLWithPull(xmlData: String) {

        try {
            val factory = XmlPullParserFactory.newInstance()
            val xmlPullParser = factory.newPullParser()
            xmlPullParser.setInput(StringReader(xmlData))
            var eventType = xmlPullParser.eventType
            var id = ""
            var name = ""
            var version = ""
            while (eventType != XmlPullParser.END_DOCUMENT){
                val nodeName = xmlPullParser.name
                when(eventType){
                    // 开始解析某个节点
                    XmlPullParser.START_TAG -> {
                        when(nodeName){
                            "id" -> id = xmlPullParser.nextText()
                            "name" -> name = xmlPullParser.nextText()
                            "version" -> version = xmlPullParser.nextText()
                        }
                    }
                    // 完成解析某个节点
                    XmlPullParser.END_TAG -> {
                        if ("app" == nodeName){
                            Log.d("MainActivity","id is $id")
                            Log.d("MainActivity","name is $name")
                            Log.d("MainActivity","version is $version")
                        }
                    }

                }
                eventType = xmlPullParser.next()
            }
        }catch (e:Exception){
            e.printStackTrace()
        }
    }

可以看到,这里首先将Http 请求的地址改成了http://10.0.2.2/get_data.xml,10.0.2.2 对于模拟器来说就是计算机甄姬的IP 地址。在得到了服务器的返回数据后,我们不再直接将其展示,而是调用了parseXMLWithPull() 方法来解析服务器返回的数据。

下面就来仔细看下,parseXMLWithPull() 方法中的代码吧。这里首先要创建一个XMLPullParserFactory 的实例,并借助这个实例得到XmlPullParser 对象,然后调用XmlPullParser 的setInput() 方法将服务器返回的XML 数据设置进去,之后就可以开始解析了。解析的过程也非常简单,通过getEventType() 方法可以得当当前的解析事件,然后在一个while 循环中不断第进行解析,如果当前的解析事件不等于 XmlPullParser.END_DOCUMENT ,说明解析的工作还没完成,调用next() 方法后可以获取下一个解析事件。

在 while 循环中,我们通过 getName()方法得到当前节点的名字,如果发现节点名等于 id、name 或 version,就调用 nextText() 方法来获取节点内具体的内容,每当解析完一个 app 节点后就将获取到的内容打印出来。

好了,整体的过程就是这么简单,不过在程序运行之前还得再进行一项额外的配置。从Android 9.0 系统开始,应用程序默认只允许使用HTTPS 类型的网络请求,HTTP 类型的网络请求因为有安全隐患默认不再被支持,而我们搭建的Apache 服务器现在使用的就是HTTP。

那么为了能让程序使用HTTP,我们还需要进行如下配置才可以。右击res 目录  → New → File ,创建一个network_config.xml 文件。然后修改newwork_config.xml 文件中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted = "true">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>

这段配置文件的意思就是允许我们以明文的方式在网络上传输数据,而且HTTP 使用的就是明文传输方式。

接下来修改Androidmanifest.xml 中的代码来启动我们刚才配置的文件(android:networkSecurityConfig = "@xml/network_config"):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.networktest">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:networkSecurityConfig="@xml/network_config"
        >
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

这就可以在程序中使用HTTP了,下面就让我们来测试一下吧。运行 NetworkTest 项目,然后点击 Send Request 按钮,观察 logcat 中的打印日志,如图所示:

可以看到,我们已经将XML 数据中的指定内容成功解析出来了。

 

11.3.2 SAX 解析方式

Pull 解析方式虽然非常好用,但它并不是我们唯一的选择。SAX 解析也是一种特别常用的 XML 解析方式,虽然它的用法比 Pul 解析要复杂一些,但在语义方面会更加清楚。

通常情况下我们都会新建一个类继承自 DefaultHandler,并重写父类的 5 个方法,如下所示:

class MyHandler : DefaultHandler() {
    override fun startDocument() {
        super.startDocument()
    }

    override fun startElement(
        uri: String?,
        localName: String?,
        qName: String?,
        attributes: Attributes?
    ) {
        super.startElement(uri, localName, qName, attributes)
    }

    override fun characters(ch: CharArray?, start: Int, length: Int) {
        super.characters(ch, start, length)
    }

    override fun endElement(uri: String?, localName: String?, qName: String?) {
        super.endElement(uri, localName, qName)
    }

    override fun endDocument() {
        super.endDocument()
    }
}

这 5 个方法一看就很清楚吧?startDocument() 方法会在开始 XML 解析的时候调用,startElement() 方法会在开始解析某个节点的时候调用,characters() 方法会在获取节点中内容的时候调用,endElement() 方法会在完成解析某个节点的时候调用,endDocument() 方法会在完成整个 XML 解析的时候调用。其中,startElement() 、characters() 和 endElement() 这 3 个方法是有参数的,从 XML 中解析出的数据就会以参数的形式传入到这些方法中。需要注意的是,在获取节点中的内容时,characters() 方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。

那么下面就让我们尝试用 SAX 解析的方式来实现和上一小节中同样的功能吧。新建一个 Contenthandler 类继承自 Defaulthandler,并重写父类的 5 个方法,如下所示:

class MyHandler : DefaultHandler() {
    private var nodeName = ""
    private lateinit var id : StringBuilder

    private lateinit var name: StringBuilder

    private lateinit var version : StringBuilder

    override fun startDocument() {
        id = StringBuilder()
        name = StringBuilder()
        version = StringBuilder()
    }

    override fun startElement(
        uri: String,
        localName: String,
        qName: String?,
        attributes: Attributes?
    ) {
        // 记录当前节点名
        nodeName = localName
        Log.d("ContentHandler","uri is $uri")
        Log.d("ContentHandler","localName is $localName")
        Log.d("ContentHandler","qName is $qName")
        Log.d("ContentHandler","attributes is $attributes")
    }

    override fun characters(ch: CharArray?, start: Int, length: Int) {
        // 根据当前节点名判断将内容添加到哪一个StringBuilder 对象中
        when(nodeName){
            "id" -> id.append(ch,start,length)
            "name" -> name.append(ch,start,length)
            "version" -> version.append(ch,start,length)
        }
    }

    override fun endElement(uri: String?, localName: String?, qName: String?) {
        if ("app" == localName){
            Log.d("ContentHandler","id is ${id.toString().trim()}")
            Log.d("ContentHandler","name is ${name.toString().trim()}")
            Log.d("ContentHandler","version is ${version.toString().trim()}")
            // 最后来将StringBuilder 清空
            id.setLength(0)
            name.setLength(0)
            version.setLength(0)
        }
    }

    override fun endDocument() {
    }
}

可以看到,我们首先给 id、name 和 verslon 节点分别定义了一个 StringBuilder 对象并在 startDocument() 方法里对它们进行了初始化。每当开始解析某个节点的时候,start Element() 方法就会得到调用,其中 localName 参数记录着当前节点的名字,这里我们把它记录下来。接着在解析节点中具体内容的时候就会调用 characters() 方法,我们会根据当前的节点名进行判断,将解析出的内容添加到哪一个 StringBuilder 对象中。最后在 endElement() 方法中进行判断,如果 app 节点已经解析完成,就打印出 id、name 和 verslon 的内容。需要注意的是,目前 id、name 和 version 中都可能是包括回车或换行符的,因此在打印之前我们还需要调用一下 trim() 方法,并且打印完成后还要将 StringBuilder 的内容清空掉,不然的话会影响下一次内容的读取。

接下来的工作就非常简单了,修改 MainActivity 中的代码,如下所示:

    private fun sendRequestWithOkHttp() {
        // 开启线程发起网络请求
        thread {
            try {
                val client = OkHttpClient()
                val request = Request.Builder()
                        // 访问的服务器地址是计算机本机
                    .url("http://10.0.2.2/get_data.xml")
                    .build()
                val response = client.newCall(request).execute()
                val responseData = response.body?.string()
                if (responseData != null){
                    parseXMLWithSAX(responseData)
                }
            }catch (e:Exception){
                e.printStackTrace()
            }
        }
    }

    private fun parseXMLWithSAX(xmlData: String) {
        try {
            val factory = SAXParserFactory.newInstance()
            val xmlReader = factory.newSAXParser().xmlReader
            val handler = MyHandler()
            // 将ContentHandler 的实例设置到 XMLReader 中
            xmlReader.contentHandler = handler
            // 开始执行解析
            xmlReader.parse(InputSource(StringReader(xmlData)))
        }catch (e:Exception){
            e.printStackTrace()
        }
    }

在得到了服务器返回的数据后,我们这次去调用 parseXMLWithSAX() 方法来解析 XML 数据。parseXMLWithSAX() 方法中先是创建了一个 SaxParserFactory 的对象,然后再获取到 XMLReader 对象,接着将我们编写的 ContentHandler 的实例设置到 XMLReader 中,最后调用 parse() 方法开始执行解析就好了。

现在重新运行一下程序,点击 Send Request 按钮后观察 Logcat I中的打印日志,你会看到和Pull 解析一样的结果。

除了 Pull 解析和 SAX 解析之外,其实还有一种 DOM 解析方式也算挺常用的,不过这里我们就不再展开进行讲解了,感兴趣的话你可以自己去査阅一下相关资料。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值