xml解析详解

项目地址:
https://github.com/liaozhoubei/HttpAndParse

XML是一种非常流行的用来传输和存储数据的语言,因此在日常工作中会经常用到它,是我们必须要掌握的知识之一。

XML指可扩展标记语言(eXtensible Markup Language),XML是被设计用来传输和存储数据。

xml是一种一种树结构,也就是有开头必有结尾,如果开头为<book>,那么结尾就是<\book>,如果<book>是最外层的结构,那么其他的数据就被嵌套在其中,格式如下:

<book id="1">
    <name>冰与火之歌</name>
    <author>乔治马丁</author>
    <year>2014</year>
    <price>89</price>
</book>

其中每个<book></book>又叫做节点,每个节点里面可以嵌套节点,而每个节点也可以有自己的属性如<book id="1">,其中的id就是属性。

那以上面的book为例,它有多少个节点呢?答案是 9 个节点。可是明明数起来只有5个节点:book、name、author、year、price呀。

那是因为空白处也算作是一个节点,如下图:

节点个数.png

所以这样算起来就有9个节点了(开始与闭合标签为同一个节点)。那么回过头来,XML有有哪些常用的节点类型呢?如下图:

它有三种节点类型,Element、Attr、Text这三种,其中Element表示节点;Attr表示节点中的属性(即上面的book的id);Text代表每个节点的内容类型(空白处也是Text类型)。

XML的只是就介绍到这里,现在我们开始解析XML吧,

xml之DOM解析

Dom解析是java中原生的对xml的解析方法,它的特点是简单易用,缺点是会见解析的内容存到内存中去,比较占用资源。

现在我们使用DOM来解析books.xml,这个xml文件位于安卓项目中的res - raw文件夹中。

同样的,我们在XMLParseAndSet添加使用DOM解析的方法

public static ArrayList<Book> parseXMLWithDOM(InputStream inputStream) {
    ArrayList<Book> booklists = new ArrayList<Book>();
    // 创建一个DocumentBuilderFactory的对象
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    // 创建一个DocumentBuilder的对象
    try {
        // 创建DocumentBuilder对象
        DocumentBuilder db = dbf.newDocumentBuilder();
        // 通过DocumentBuilder对象的parser方法加载books.xml文件到当前项目下
        Document document = db.parse(inputStream);
        // 获取所有book节点的集合
        NodeList bookList = document.getElementsByTagName("book");
        // 遍历每一个book节点
        for (int i = 0; i < bookList.getLength(); i++) {
            
            Book books = new Book();
            
            // 通过 item(i)方法 获取一个book节点,nodelist的索引值从0开始
            Node book = bookList.item(i);
            // 获取book节点的所有属性集合
            NamedNodeMap attrs = book.getAttributes();
            // 遍历book的属性id
            for (int j = 0; j < attrs.getLength(); j++) {
                // 通过item(index)方法获取book节点的某一个属性
                Node attr = attrs.item(j);
                // 获取属性名id
                System.out.print("属性名:" + attr.getNodeName());
                // 获取属性值id的数值
                System.out.println("--属性值" + attr.getNodeValue());
                books.setId(attr.getNodeValue());
            }
            // 解析book节点的子节点
            NodeList childNodes = book.getChildNodes();
            // 遍历childNodes获取每个节点的节点名和节点值
            System.out.println("第" + (i + 1) + "本书共有" + childNodes.getLength() + "个子节点");
            for (int k = 0; k < childNodes.getLength(); k++) {
                // 区分出text类型的node以及element类型的node
                if (childNodes.item(k).getNodeType() == Node.ELEMENT_NODE) {
                    
                    // 获取了element类型节点的节点名
                    String nodeName = childNodes.item(k).getNodeName();
                    // 获取了element类型节点的节点值
                    String nodeValue = childNodes.item(k).getFirstChild().getNodeValue();
                    
                    if (nodeName.equals("name")) {
                        books.setName(nodeValue);
                    }
                    // 获得book对象的参数
                    ····
                }
            }
            booklists.add(books);
            System.out.println("======================结束遍历第" + (i + 1) + "本书的内容=================");
        }
        return booklists;
    } 
    ·······
    return null;
}

在这段代码中,我们把xml中的数据解析出来,然后存到ArrayList<Book>集合中,返回到Activity显示到界面中。

解释完这段代码的功能,我们开始详细解析DOM解析xml的步骤吧。

1、首先必须要创建DocumentBuilderFactory即DOM工厂类对象
2、通过工厂类的静态方法获得DocumentBuilder对象
3、将传入的流对象放入DocumentBuilder的parse()方法中,获得Document对象
4、通过Document的getElementsByTagName()方法,直接从标签"book"开始解析xml(解析最外层的bookstore没有任何意义),获得NodeList节点结合。
5、通过对NodeList的for循环不断的得到Node,即每个节点对象,然后通过节点对象获取节点名;
6、若节点Node下仍有子节点,可通过getChildNodes()获得所有的子节点的集合对象。

相信前4步大家都不会有过多的疑问,而第4步开始却会产生疑问。所以让我们回到第4步,代码如下:
// 获取所有book节点的集合
NodeList bookList = document.getElementsByTagName("book");
// 遍历每一个book节点
for (int i = 0; i < bookList.getLength(); i++) {

            // 通过 item(i)方法 获取一个book节点,nodelist的索引值从0开始
            Node book = bookList.item(i);
            // 获取book节点的所有属性集合
            NamedNodeMap attrs = book.getAttributes();
            // 遍历book的属性id
            for (int j = 0; j < attrs.getLength(); j++) {
                // 通过item(index)方法获取book节点的某一个属性
                Node attr = attrs.item(j);
                ···
            }
    }

我们在getElementsByTagName("book")这个方法中获得了整个子节点的集合对象bookList,然后操作就跟普通集合一般,要获得里面的参数就必须使用for循环。

然后我们通过for循环,得到Node对象book,这是我们就可以理解为NodeList是Node的集合,而Node则有两个参数,分别为NodeValue节点值和NodeName节点名。但是但节点类型不为Attr和Text的时候,使用getNodeValue()方法只能获得null。

突然间,我们发现book节点中有Attr属性id,想要获取里面的值怎么办?只需要执行Node的getAttributes()方法就可以获得Attr属性的集合NamedNodeMap(一个节点可拥有多个属性),然后通过for循环获得里面的值。

当然,上面获得节点属性的值的方法是当不知道属性名称的时候使用,如果确定属性名称,并知道要获得哪个属性,可用以下方法:

            // Element book = (Element) bookList.item(i);
            // //通过getAttribute("id")方法获取属性值
            // String attrValue = book.getAttribute("id");

紧接着我们要获得book节点下的子节点,如name、author这些节点的信息怎么办?

Node类有getChildNodes()的方法获得子节点的集合NodeList,然后又是一次for循环,代码如下:

            NodeList childNodes = book.getChildNodes();
            // 遍历childNodes获取每个节点的节点名和节点值                
            for (int k = 0; k < childNodes.getLength(); k++) {
                // 区分出text类型的node以及element类型的node
                if (childNodes.item(k).getNodeType() == Node.ELEMENT_NODE) {
                    
                    // 获取了element类型节点的节点名name、author等
                    String nodeName = childNodes.item(k).getNodeName();            
                    
                    ····
                }
            }

这时我们以为已经走到了尽头,可以获取name节点中的值,如《冰与火之歌》这样的书名的时候,去发现直接使用getNodeValue()获得了null!!

上面我们已经说过节点类型不为Attr和Text的时候,使用getNodeValue()方法只能获得null,而name的节点类型刚好是Element。

那么该怎么办呢?只有继续使用getChildNodes()方法获取子节点,然后使用getNodeValue()方法了。

但是请等一等,为什么要使用getChildNodes()获取子节点,为什么name里面的书名《冰与火之歌》不属于它自身,而属于子节点呢?

这里就要说一下xml的特点了,xml中每个节点嵌套的数据都是一个子节点,这其中也包括他们中间的value,如果你使用getNodeName()获得value节点名,就会获得Text元素的默认名#text。

写到了这里,相信大家对DOM解析XML已经有了一个比较直观的认识了。使用DOM解析xml就等于是解析一个循环的集合,只要这个集合中还有这子节点,那么就可以使用for循环继续执行下去!

xml之SAX解析

SAX解析xml相对与dom解析来说稍显复杂一点,因为dom解析只需要不断的使用for循环就可以了,但是SAX解析却需要按照步骤来,从xml的开始即<root>到结尾<\root>,而且还需要一个helper类来帮助解析。

SAX与dom相比,它速度更快,更有效,而且一遍扫描一边解析,对内存压力教小。但是SAX解析是逐行解析,也就是从从开头到结尾,次序不能乱,你无法解析到第二行的时候让它解析第五行。

介绍完优缺点之后,我们就来看看如何使用SAX解析吧。

我们仍然使用DOM解析时的xml数据。

首先我们要建一个帮助类SAXParserHandler,它继承DefaultHandler,这个帮助类用于帮助解析XML,并且获得xml中的数据。

要想解析xml并获得数据,需要重写DefaultHandler的五个方法,分别为:

@Override
public void startDocument() throws SAXException {        
    super.startDocument();        
}
@Override
public void endDocument() throws SAXException {
    // TODO Auto-generated method stub
    super.endDocument();
}


@Override
public void startElement(String uri, String localName, String qName,
        Attributes attributes) throws SAXException {
    //调用DefaultHandler类的startElement方法
    super.startElement(uri, localName, qName, attributes);
        
}

@Override
public void endElement(String uri, String localName, String qName)
        throws SAXException {
    //调用DefaultHandler类的endElement方法
    super.endElement(uri, localName, qName);
}


@Override
public void characters(char[] ch, int start, int length)
        throws SAXException {
    // TODO Auto-generated method stub
    super.characters(ch, start, length);
    
}

这些方法有什么用呢?

startDocument()表示xml文档开始解析,而endDocument()已经结束解析了。如果在这两个方法打标签,就会发现,在解析的时候首先输出startDocument()的表情,然后再输出endDocument(),它们都只会被调用一次,由于它们是标识开始与结束的表情,因此一般不需要理会

startElement()和endElement()两个方法,结合XML中有Element类型,很容易得知它们一个节点的开始和节点的结束时使用的。

同时startElement(String uri, String localName, String qName, Attributes attributes)中的几个参数也需要了解一下

uri:xml的命名空间的uri
localName:如果uri命名空间没有被执行,那么为空
qName:节点的名称
attributes:节点的属性名

characters()方法也是一个重要的方法,它是获取节点中数据的方法,也就是获取<name>冰与火之歌</name>中的《冰与火之歌》书名的方法。

介绍完我们就开始正式解析xml吧。

首先我们要判断当前运行到哪个节点,因此修改startElement(),代码如下:

@Override
public void startElement(String uri, String localName, String qName,
        Attributes attributes) throws SAXException {
    //调用DefaultHandler类的startElement方法
    super.startElement(uri, localName, qName, attributes);
    if (qName.equals("book")) {
        bookIndex++;
        //不知道book元素下属性的名称以及个数,如何获取属性名以及属性值
        int num = attributes.getLength();
        for(int i = 0; i < num; i++){
            System.out.print("book元素的第" + (i + 1) +  "个属性名是:"
                    + attributes.getQName(i));
            System.out.println("---属性值是:" + attributes.getValue(i));
            if (attributes.getQName(i).equals("id")) {
                book.setId(attributes.getValue(i));
            }
        }
    }
    else if (!qName.equals("name") && !qName.equals("bookstore")) {
        System.out.print("节点名是:" + qName + "---");
    }
}

判断运行到那个节点很简单,只需要用qName与节点名比较,是否在同一个地址就行了。

然后获取节点中的数据,如运行到name这个节点,要获取书名该怎么办?这时只要重写characters(),设置一个全局变量就能够获取书名

@Override
public void characters(char[] ch, int start, int length)
        throws SAXException {
    // TODO Auto-generated method stub
    super.characters(ch, start, length);
    value = new String(ch, start, length);
    if (!value.trim().equals("")) {
        System.out.println("节点值是:" + value);
    }
}

最后运行到endElement(),可以再进行一次判断,判断当前的结束节点标签是不是我们想要的结束节点标签,然后将characters()方法中获得的value变量设置进去。

我们再来看看这五个方法的运行顺序吧,

startDocument() --》 startElement() --》characters() --》 endElement() --》endDocument()

这样帮助类就写好了,那么如何使用这个SAX解析的帮助类呢?

我们继续在XMLParseAndSet中添加SAX解析的方法,代码如下:

public static ArrayList<Book> parseXMLWithSAX(InputStream inputStream) {
    //获取一个SAXParserFactory实例
    SAXParserFactory factory = SAXParserFactory.newInstance();
    // 通过factory获取ȡSAXParser实例
    try {
        SAXParser parser = factory.newSAXParser();
        //创建SAXParserHandler对象
        SAXParserHandler handler = new SAXParserHandler();
        parser.parse(inputStream, handler);
        System.out.println("一共有" + handler.getBookList().size()
                + "本书");
        ···
        return handler.getBookList();
    } 
    ···
    return null;
}

这样我们从外部传入inputStream流就能够解析到books.xml了。

Android中的pull解析

前面是使用java中自带的解析xml的api,现在我们使用android中提供的xml解析器来解析xml吧。

这次解析的数据并不是books.xml,而是存放在assets目录下的backupsms.xml,它的数据形势如下:

<Sms id = "1">
    <num>110</num>
    <msg>来警局做个笔录</msg>
    <date>2015-08-29</date>
</Sms>

如果不太关心xml中的各个标题,可以用以下方法:

public static ArrayList<SmsBean> parseXMLWithPull(Context context) {
    ArrayList<SmsBean> arrayList = null;
    SmsBean smsBean = null;
    try {
        // 1.通过Xml获取一个XmlPullParser对象
        XmlPullParser xpp = Xml.newPullParser();
        // 2.设置XmlPullParser对象的参数,需要解析的是哪个xml文件,设置一个文件读取流

        // 通过context获取一个资产管理者对象
        AssetManager assets = context.getAssets();
        // 通过资产管理者对象能获取一个文件读取流
        InputStream inputStream = assets.open("backupsms.xml");
        xpp.setInput(inputStream, "utf-8");            
        // 3.获取当前xml行的事件类型
        int type = xpp.getEventType();
        // 4.判断事件类型是否是文档结束的事件类型
        while (type != XmlPullParser.END_DOCUMENT) {
            // 5.如果不是,循环遍历解析每一行的数据。解析一行后,获取下一行的事件类型

            String currentTagName = xpp.getName();
            // 判断当前行的事件类型是开始标签还是结束标签
            switch (type) {
            case XmlPullParser.START_TAG:
                if (currentTagName.equals("Smss")) {
                    // 如果当前标签是Smss,需要初始化一个集合
                    arrayList = new ArrayList<SmsBean>();
                } else if (currentTagName.equals("Sms")) {

                    smsBean = new SmsBean();
                    smsBean.id = Integer.valueOf(xpp.getAttributeValue(null, "id"));

                } else if (currentTagName.equals("num")) {
                    smsBean.num = xpp.nextText();
                } else if (currentTagName.equals("msg")) {
                    smsBean.msg = xpp.nextText();
                } else if (currentTagName.equals("date")) {
                    smsBean.date = xpp.nextText();
                }
                break;
            case XmlPullParser.END_TAG:
                // 当前结束标签是Sms的话,一条短信数据封装完成, 可以加入list中
                if (currentTagName.equals("Sms")) {
                    arrayList.add(smsBean);
                }
                break;
            default:
                break;
            }

            type = xpp.next();// 获取下一行的事件类型
        }
        return arrayList;

    } 
    ···
    return null;
}

当然上面那是解析数据时的代码,精简一下,代码会是这样:
public static void parseXMLWithPull(InputStream inputStream) {

    try {
        // 1.通过Xml获取一个XmlPullParser对象
        XmlPullParser xpp = Xml.newPullParser();            
        xpp.setInput(inputStream, "utf-8");            
        // 3.获取当前xml行的事件类型
        int type = xpp.getEventType();
        // 4.判断事件类型是否是文档结束的事件类型
        while (type != XmlPullParser.END_DOCUMENT) {
            // 5.如果不是,循环遍历解析每一行的数据。解析一行后,获取下一行的事件类型
            String currentTagName = xpp.getName();
            // 判断当前行的事件类型是开始标签还是结束标签
            switch (type) {
            case XmlPullParser.START_TAG:
                // 开始标签的处理
                ···
                break;
            case XmlPullParser.END_TAG:
                // 结束标签的处理
                ···
                break;
            default:
                break;
            }

            type = xpp.next();// 获取下一行的事件类型
        }            
    }         
}

这是一个精简的模板,可以直接拿去修改使用。

当然,更详细的PULL解析xml的方法在Android开发者官网,这里给出地址:

https://developer.android.com/training/basics/network-ops/xml.html

项目地址:
https://github.com/liaozhoubei/HttpAndParse

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容