在项目里,咱们每每会把一些配置信息放到xml文件里,或者各部门间会经过xml文件来交换业务数据,因此有时候咱们会遇到“解析xml文件”的需求。通常来说,有基于DOM树和SAX的两种解析xml文件的方式,在这部分里,将分别给你们演示经过这两种方式解析xml文件的通常步骤。java
1 XML的文件格式node
XML是可扩展标记语言(Extensible Markup Language)的缩写,在其中,开始标签和结束标签必须配套地出现,咱们来看下book.xml这个例子。 编程
1 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 <books> 3 <book id="01"> 4 <name>Java</name> 5 <price>15</price> 6 <memo>good book</memo> 7 </book> 8 <book id="02"> 9 <name>FrameWork</name> 10 <price>20</price> 11 <memo>new book</memo> 12 </book> 13 </books>
整个xml文件是一个文档(document),其中第1行表示文件头,在第2和第13行里,咱们能看到配套出现的books标签,从标签头到标签尾的部分那咱们称之为元素(element)。dom
因此咱们能够这样说,在books元素里,咱们分别于第3到第7行和第8到第12行定义了2个book元素,在每一个book元素,好比从第4到第6行,又包含着3个元素,好比第一本书的name元素是<name>Java</name>,它的name元素值是Java。函数
在第3行里,咱们还能看到元素里的属性(attribute),好比这个book元素具备id这个属性,具体id的属性值是01。性能
DOM是Document Object Model(文档对象模型)的缩写,在基于DOM树的解析方式里,解析代码会先把xml文档读到内存里,并整理成DOM树的形式,随后再读取。根据以前部分里给出的book.xml文档,咱们能够绘制出以下形式的DOM树。ui
其中,books属于根(root)结点,也叫根元素,因为它包含着两个book元素,因此第二层是两个book结点,每一个book元素包含着3个元素,因此第三层是6个元素。在下面的ParserXmlByDom.java的代码里,咱们来看下经过DOM树方式解析book.xml文档的详细步骤。this
1 //省略import相关类库的代码 2 public class ParserXmlByDom { 3 public static void main(String[] args) { 4 //建立DOM工厂 5 DocumentBuilderFactory domFactory=DocumentBuilderFactory.newInstance(); 6 InputStream input = null; 7 try { 8 //经过DOM工厂得到DOM解析器 9 DocumentBuilder domBuilder=domFactory.newDocumentBuilder(); 10 //把XML文档转化为输入流 11 input=new FileInputStream("src/book.xml"); 12 //解析XML文档的输入流,获得一个Document 13 Document doc=domBuilder.parse(input);
从第5行到第13行,咱们完成了用DOM树解析XML文件的准备工做,具体包括,在第5行里建立了DOM工厂,在第9行经过DOM工厂建立了解析xml文件DocumentBuilder类型对象,在第11行把待解析的xml文件放入到一个InputStream类型的对象里,在第13行经过parse方法把xml文档解析成一个基于DOM树结构的Document类型对象。 spa
14 //获得XML文档的根节点,只有根节点是Element类型 15 Element root=doc.getDocumentElement(); 16 // 获得子节点 17 NodeList books = root.getChildNodes();
整个XML文件包含在第13行定义的doc对象里,在第15行里,咱们经过getDocumentElement方法获得了根节点(也就是books节点),在第17行,经过getChildNoes方法获得该books节点下的全部子节点,随后开始解析整个xml文档。xml
须要说明的是,在解析前,咱们会经过观察xml文档来了解其中的元素名和属性名,因此在后继的代码里,咱们会针对元素名和属性名进行编程。
18 if(books!=null){ 19 for(int i=0;i<books.getLength();i++){ 20 Node book=books.item(i); 21 //获取id属性 22 if(book.getNodeType()==Node.ELEMENT_NODE){ 23 String id=book.getAttributes().getNamedItem("id").getNodeValue(); 24 System.out.println("id is:" + id); 25 //遍历book下的子节点 26 for(Node node=book.getFirstChild(); node!=null;node=node.getNextSibling()){ 27 if(node.getNodeType()==Node.ELEMENT_NODE){ 28 //依次读取book里的name,price和memo三个子元素 29 if(node.getNodeName().equals("name")){ 30 String name=node.getFirstChild().getNodeValue(); 31 System.out.println("name is:" + name); 32 } 33 if(node.getNodeName().equals("price")){ 34 String price=node.getFirstChild().getNodeValue(); 35 System.out.println("price is:" + price); 36 } 37 if(node.getNodeName().equals("memo")){ 38 String memo=node.getFirstChild().getNodeValue(); 39 System.out.println("memo is:" + memo); 40 } 41 } 42 } 43 } 44 } 45 }
第19行的for循环里,咱们是遍历book元素经过观察xml文件,咱们发现book元素出现了2次,全部这个循环会运行两次,并且,book元素有1个id属性,全部咱们须要经过第23行的代码,获得id属性的值。
在文档里,book元素有3个子节点,分别是name,price和memo,因此在代码的26行里,再次使用for循环遍历其中的子节点。在遍历时,咱们经过29到32行的代码获取到了book元素里name的值,经过相似的代码后继的33到40行代码里获得了price和memo这两个元素的值。
46 } catch (ParserConfigurationException e) { 47 e.printStackTrace(); 48 } catch (FileNotFoundException e) { 49 e.printStackTrace(); 50 } catch (IOException e) { 51 e.printStackTrace(); 52 } catch (SAXException e) { 53 e.printStackTrace(); 54 } catch (Exception e) { 55 e.printStackTrace(); 56 } 57 //在finally里关闭io流 58 finally{ 59 try { 60 input.close(); 61 } catch (IOException e) { 62 e.printStackTrace(); 63 } 64 } 65 } 66 }
一样地,在解析完成后,在finally从句里,咱们关闭了以前用到的IO流(input对象)。
SAX是Simple API for XML的缩写,不一样于DOM的文档驱动,它是事件驱动的,也就是说,它是一种基于回调(callback)函数的解析方式,好比开始解析xml文档时,会调用咱们本身定义的startDocument函数,从下表里,咱们能看到基于SAX方式里的各类回调函数以及它们被调用的时间点。
函数名 |
调用时间点 |
startDocument |
开始解析xml文档时(解析xml文档第一个字符时)会被调用 |
endDocument |
当解析完xml文档时(解析到xml文档最后一个字符时)会被调用 |
startElement |
当解析到开始标签时会被调用,好比在解析“<name>FrameWork</name>”这个element时,当读到开始标签“<name>”时,会被调用 |
endElement |
当解析到结束标签时会被调用,好比在解析“<name>FrameWork</name>”这个element时,当读到结束标签“</name>”时,会被调用 |
characters |
1行开始后,遇到开始或结束标签以前存在字符,则会调用 2两个标签之间,存在字符,则会调用,好比在解析“<name>FrameWork</name>”时,发现存在FrameWork,则会被调用 3标签和行结束符以前存在字符,则会调用 |
从上表里咱们能看到characters方法会在多个场合被回调,但咱们最指望的调用场景是第2种,这就要求咱们最好在解析xml文档前整理下它的格式,尽可能避免第1和第3种状况。在ParserXmlBySAX.java这个案例中,咱们经过了编写上述的回调函数,实现了SAX方式解析xml文档的功能。
1 //省略import的代码 2 //基于SAX的解析代码须要继承DefaultHandler类 3 public class ParserXmlBySAX extends DefaultHandler{ 4 // 记录当前解析到的节点名 5 private String tagName; 6 //主方法 7 public static void main(String[] argv) { 8 String uri = "src/book.xml"; 9 try { 10 SAXParserFactory parserFactory = SAXParserFactory.newInstance(); 11 ParserXmlBySAX myParser = new ParserXmlBySAX(); 12 SAXParser parser = parserFactory.newSAXParser(); 13 parser.parse(uri, myParser); 14 } catch (IOException ex) { 15 ex.printStackTrace(); 16 } catch (SAXException ex) { 17 ex.printStackTrace(); 18 } catch (ParserConfigurationException ex) { 19 ex.printStackTrace(); 20 } catch (FactoryConfigurationError ex) { 21 ex.printStackTrace(); 22 } 23 }
在main方法的第8行里,咱们指定了待解析xml文档的路径和文件名,在第10行里,咱们建立了SAXParserFactory这个类型的SAX解析工厂对象。在第12行,咱们经过SAX解析工厂对象,建立了SAXParser这个类型的解析类。在第13行,经过了parse方法启动了解析。
在上文里咱们就已经知道,在SAX的方式里,是经过调用各类回调函数来完成解析的,因此在代码里,咱们还得自定义各个回调函数,代码以下。
// 处理到文档结尾时,直接输出,不作任何动做 25 public void endDocument() throws SAXException { 26 System.out.println("endDocument"); 27 } 28 // 处理到结束标签时,把记录当前标签名的tagName设置成null 29 public void endElement(String uri, String localName, String qName) throws SAXException { 30 tagName = null; 31 } 32 // 开始处理文档时,直接输出,不作任何动做 33 public void startDocument() throws SAXException { 34 System.out.println("startDocument"); 35 } 36 // 处理开始标签 37 public void startElement(String uri, String localName, String name,Attributes attributes) throws SAXException { 38 if ("book".equals(name)) { //解析book标签的属性 39 for (int i = 0; i < attributes.getLength(); i++) { 40 System.out.println("attribute name is:" + attributes.getLocalName(i) + " attribute value:" + attributes.getValue(i)); 41 } 42 } 43 //把当前标签的名字记录到tagName这个变量里 44 tagName = name; 45 } 46 //经过这个方法解析book的三个子元素的值 47 public void characters(char[] ch, int start, int length) 48 throws SAXException { 49 if(this.tagName!=null){ 50 String val=new String(ch,start,length); 51 //若是是name,price或memo,则输出它们的值 52 if("name".equals(tagName)) 53 { System.out.println("name is:" + val); } 54 if("price".equals(tagName)) 55 { System.out.println("price is:" + val); } 56 if("memo".equals(tagName)) 57 { System.out.println("memo is:" + val); } 58 } 59 } 60 }
咱们用tagName来保存当前的标签名,是为了解析book元素的name,price和memo这三个子元素。
<name>FrameWork</name>
好比当解析到name这个开始标签时,在第44行里,startElement会把tagname值设置成name,当解析到FramWork时,因为它包含在两个标签之间,因此会被触发第47行的characters方法,在其中的第52行的if判断里,因为得知当前的标签名是name,因此会输出FrameWork这个name元素的值,当解析到</name>这个结束标签时,会触发第29行的endElement方法,在其中的30行里,会把tagName值清空。
这段代码的输出结果以下,其中第1行和第10行分别是在开始解析和完成解析时输出的。
第2行针对id属性的输出是在startElement方法的第40行里被打印的,第3到第5行针对3个book子元素的输出是在characters方法里被打印的。
第2到第5行是针对第一个book元素的输出,而第6到第9行是针对第2个book。
1 startDocument 2 attribute name is:id attribute value:01 3 name is:Java 4 price is:15 5 memo is:good book 6 attribute name is:id attribute value:02 7 name is:FrameWork 8 price is:20 9 memo is:new book 10 endDocument
在基于DOM的方式里,因为咱们会把整个xml文档以DOM树的方式装载到内存里,因此能够边解析边修改,并且还能再次解析已经被解析过的内容。
而在SAX的方式里,因为咱们是以基于回调函数的方式来解析,因此并不须要把整个文档载入到内存,这样能节省内存资源。
因此说,选择 DOM 仍是 SAX,这取决于以下三个个因素。
第一,若是咱们在解析时还打算更新xml里的数据,那么建议使用DOM方式。
第二,若是待解析的文件过大,把它所有装载到内存时可能会影响到内存性能,那么建议使用SAX的方式。
第三,若是咱们对解析的速度有必定的要求,那么建议使用SAX方式,由于它比DOM方式要快些。