项目地址:
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呀。
那是因为空白处也算作是一个节点,如下图:
所以这样算起来就有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