1 소개
Java에는 XML 파일을 구문 분석하는 방법이 많이 있으며, 그 중 가장 일반적인 방법은 아마도 DOM, SAX, JDOM 및 DOM4J일 것입니다. 그 중 XML 파일을 파싱하는 두 가지 방법인 DOM과 SAX는 jdk 고유의 API를 갖고 있어 추가적인 타사 jar 패키지를 도입할 필요가 없습니다. 반대로 JDOM과 DOM4J 구문 분석 방법은 모두 타사 오픈 소스 프로젝트이므로 이 두 가지 방법을 사용하여 XML 파일을 구문 분석하는 경우 관련 jar 패키지를 추가로 도입해야 합니다
(1) DOM
DOM은 플랫폼 및 언어 독립적인 방식으로 XML 문서를 표현하기 위한 공식 W3C 표준입니다. DOM은 개발자가 트리에서 특정 정보를 찾을 수 있도록 계층 구조로 구성된 노드 또는 정보 조각의 모음입니다. 이 구조를 분석하려면 일반적으로 작업을 수행하기 전에 전체 문서를 로드하고 계층 구조를 구성해야 합니다.
따라서 DOM을 사용하여 XML 파일을 구문 분석할 때 파서는 전체 XML 파일을 메모리로 읽어야 하며, 트리 구조를 형성하면 후속 작업
장점: 전체 문서 트리가 메모리에 있고 작동하기 쉽습니다. 삭제, 수정, 재배치 및 기타 작업을 지원합니다
단점: 전체 문서를 메모리로 전송합니다(쓸모 없는 노드 포함). , 시간과 메모리 낭비입니다. XML이 너무 크면 메모리 오버플로 문제가 발생하기 쉽습니다.
(2) SAX
DOM은 전체 XML 파일을 한 번에 읽어야 하기 때문에 파일에는 따라서 이 문제를 해결하기 위해 이벤트 기반 구문 분석 방법인 SAX가 등장했습니다.
SAX는 XML 파일을 위에서 아래로 지속적으로 로드하여 구문 분석합니다. 파서가 시작 플래그, 종료 플래그, 텍스트, 문서 시작 플래그, 문서 종료 플래그 및 기타 관련 플래그를 찾으면 해당 이벤트를 실행하기만 하면 됩니다. 얻은 데이터를 저장하기 위한 코드
장점: 전체 문서를 미리 로드할 필요가 없으며 리소스(메모리)를 덜 차지합니다. SAX 구문 분석을 사용하여 작성된 코드가 DOM 구문 분석을 사용하여 작성된 코드보다 낫습니다
단점: 이벤트가 발생한 후에는 지속되지 않습니다. 데이터가 저장되지 않으면 이벤트에서 텍스트를 가져올 수만 있지만 텍스트가 속한 요소는 알 수 없습니다.
(3) JDOM
JDOM을 사용하여 XML 파일을 구문 분석하는 것은 DOM을 사용하여 구문 분석하는 것과 비슷합니다. JDOM은 두 가지 주요 측면에서 DOM과 다릅니다. 첫째, JDOM은 인터페이스 대신 구체적인 클래스만 사용하므로 일부 측면에서는 API가 단순화되지만 유연성도 제한됩니다. 둘째, JDOM의 API는 Collections 클래스를 광범위하게 사용하여 이러한 클래스에 이미 익숙한 Java 개발자의 사용을 단순화합니다.
장점: DOM보다 이해하기 쉬운 오픈 소스 프로젝트
단점: JDOM 자체에는 파서가 포함되어 있지 않습니다. 일반적으로 SAX2 파서를 사용하여 입력 XML 문서를 구문 분석하고 검증합니다.
(4) DOM4J
DOM4J는 뛰어난 성능, 강력한 기능 및 극도의 사용 편의성을 갖춘 매우 뛰어난 Java XML API입니다. 기능을 갖추고 있으며 오픈 소스 소프트웨어이기도 합니다. 요즘에는 점점 더 많은 Java 소프트웨어가 DOM4J를 사용하여 XML을 읽고 쓰는 것을 볼 수 있습니다
DOM4J는 성능 및 코드 작성 측면에서 매우 강력하기 때문에 특히 XML 파일이 큰 경우 DOM4J를 사용하여 구문 분석하는 것도 가능합니다. 더 효율적이 되십시오. 따라서 일상생활에서 XML 파일을 구문 분석해야 하는 경우에는 DOM4J를 사용하여 최대한 구문 분석하는 것을 고려해 볼 수 있습니다. 물론 파일이 매우 작다면 DOM을 사용하여 구문 분석하는 것도 가능합니다.
장점:
오픈 소스 프로젝트
DOM4J는 JDOM의 지능형 분기입니다. 기본 이상의 요구사항을 병합한 XML 문서의 기능
우수한 성능, 우수한 유연성, 단순성 및 사용 편의성을 특징으로 함
두 번째 DOM 구문 분석 XML 파일
(1) 코드를 작성하고 테스트하기 전에 여기서 준비한 파일은 다음과 같습니다: deco1.xml
demo1.xml:
<?xml version="1.0" encoding="UTF-8" ?> <employees> <user id="1"> <name>zifangsky</name> <age>10</age> <sex>male</sex> <contact>https://www.zifangsky.cn</contact> </user> <user id="2"> <name>admin</name> <age>20</age> <sex>male</sex> <contact>https://www.tar.pub</contact> </user> </employees>
(2) 코드 예:
package cn.zifangsky.xml; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; public class DomParseTest { public static void main(String[] args) { DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder dBuilder = dFactory.newDocumentBuilder(); // 加载一个xml文件 Document document = dBuilder .parse("src/cn/zifangsky/xml/demo1.xml"); // 获取user节点集合 NodeList userList = document.getElementsByTagName("user"); int userListLength = userList.getLength(); System.out.println("此xml文件一共有" + userListLength + "个'user'节点\n"); // 遍历 for (int i = 0; i < userListLength; i++) { // 通过item方法获取指定的节点 Node userNode = userList.item(i); // *********************解析属性*********************** // 获取该节点的所有属性值,如:id="1" NamedNodeMap userAttributes = userNode.getAttributes(); System.out.println("'user'节点" + i + "有" + userAttributes.getLength() + "个属性:"); /** * 1 在不清楚有哪些属性的情况下可以遍历所有属性, * 并获取每个属性对应的属性名和属性值 * */ for (int j = 0; j < userAttributes.getLength(); j++) { // 'user'节点的每个属性组成的节点 Node attrnNode = userAttributes.item(j); System.out.println("属性" + j + ": 属性名: " + attrnNode.getNodeName() + " ,属性值: " + attrnNode.getNodeValue()); } /** * 2 在知道有哪些属性值的情况下,可以获取指定属性名的属性值 * */ Element userElement = (Element) userList.item(i); System.out.println("属性为'id'的对应值是: " + userElement.getAttribute("id")); // *********************解析子节点************************ NodeList childNodes = userNode.getChildNodes(); System.out.println("\n该节点一共有" + childNodes.getLength() + "个子节点,分别是:"); // 遍历子节点 for (int k = 0; k < childNodes.getLength(); k++) { Node childNode = childNodes.item(k); // 从输出结果可以看出,每行后面的换行符也被当做了一个节点,因此是:4+5=9个子节点 // System.out.println("节点名: " + childNode.getNodeName() + // ",节点值: " + childNode.getTextContent()); // 仅取出子节点中的'ELEMENT_NODE',换行符组成的Node是'TEXT_NODE' if (childNode.getNodeType() == Node.ELEMENT_NODE) { // System.out.println("节点名: " + childNode.getNodeName() // + ",节点值: " + childNode.getTextContent()); // 最低一层是文本节点,节点名是'#text' System.out.println("节点名: " + childNode.getNodeName() + ",节点值: " + childNode.getFirstChild().getNodeValue()); } } System.out.println("***************************"); } } catch (Exception e) { e.printStackTrace(); } } }
위 코드에서 볼 수 있듯이 DOM을 사용하여 XML 파일을 구문 분석할 때 일반적으로 다음 단계를 수행해야 합니다.
문서 빌더 팩토리 만들기( DocumentBuilderFactory) 인스턴스
위의 DocumentBuilderFactory를 통해 새로운 문서 빌더(DocumentBuilder)를 생성합니다
위의 DocumentBuilder를 사용하여 XML 파일을 파싱(parse)하고 문서 트리(Document)를 생성합니다
Document를 통해 또는 노드를 기반으로 지정된 ID를 가진 노드를 가져옵니다. 조건을 충족하는 모든 노드의 집합을 가져옵니다.
각 노드를 탐색하여 속성, 속성 값 및 기타 관련 매개 변수를 가져옵니다. 노드
노드에 하위 노드도 있는 경우 위 방법을 사용할 수 있습니다. 계속해서 모든 하위 노드를 순회합니다.
(3) 위 코드 출력은 다음과 같습니다.
此xml文件一共有2个'user'节点 'user'节点0有1个属性: 属性0: 属性名: id ,属性值: 1 属性为'id'的对应值是: 1 该节点一共有9个子节点,分别是: 节点名: name,节点值: zifangsky 节点名: age,节点值: 10 节点名: sex,节点值: male 节点名: contact,节点值: https://www.zifangsky.cn *************************** 'user'节点1有1个属性: 属性0: 属性名: id ,属性值: 2 属性为'id'的对应值是: 2 该节点一共有9个子节点,分别是: 节点名: name,节点值: admin 节点名: age,节点值: 20 节点名: sex,节点值: male 节点名: contact,节点值: https://www.tar.pub ***************************
SAX 구문 분석된 XML 파일 3개
在进行本次测试时,并不引入其他XML文件,仍然使用上面的demo1.xml文件
由于SAX解析XML文件跟DOM不同,它并不是将整个文档都载入到内存中。解析器在解析XML文件时,通过逐步载入文档,从上往下一行行的解析XML文件,在碰到文档开始标志、节点开始标志、文本文档、节点结束标志、文档结束标志时进行对应的事件处理。因此,我们首先需要构造一个这样的解析处理器来申明:当解析到这些标志时,我们需要进行怎样的自定义处理
(1)解析处理器SAXParseHandler.java:
package cn.zifangsky.xml; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class SAXParseHandler extends DefaultHandler { /** * 用来遍历XML文件的开始标签 * */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); //解析'user'元素的属性值 // if(qName.equals("user")) // System.out.println("'user'元素的id属性值是:" + attributes.getValue("id")); //遍历并打印元素的属性 int length = attributes.getLength(); if(length > 0){ System.out.println("元素'" + qName + "'的属性是:"); for(int i=0;i<length;i++){ System.out.println(" 属性名:" + attributes.getQName(i) + ",属性值: " + attributes.getValue(i)); } System.out.println(); } System.out.print("<" + qName + ">"); } /** * 用来遍历XML文件的结束标签 * */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); System.out.println("<" + qName + "/>"); } /** * 文本内容 * */ public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); String value = new String(ch, start, length).trim(); if(!value.equals("")) System.out.print(value); } /** * 用来标识解析开始 * */ @Override public void startDocument() throws SAXException { System.out.println("SAX解析开始"); super.startDocument(); } /** * 用来标识解析结束 * */ @Override public void endDocument() throws SAXException { System.out.println("SAX解析结束"); super.endDocument(); } }
关于上面代码的一些含义我这里就不再做解释了,可以自行参考注释内容
(2)测试:
SAXParseTest.java文件:
package cn.zifangsky.xml; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; public class SAXParseTest { public static void main(String[] args) { SAXParserFactory sFactory = SAXParserFactory.newInstance(); try { SAXParser saxParser = sFactory.newSAXParser(); //创建自定义的SAXParseHandler解析类 SAXParseHandler saxParseHandler = new SAXParseHandler(); saxParser.parse("src/cn/zifangsky/xml/demo1.xml", saxParseHandler); } catch (Exception e) { e.printStackTrace(); } } }
从上面的代码可以看出,使用SAX解析XML文件时,一共传递进去了两个参数,分别是:XML文件路径和前面定义的解析处理器。有了具体的XML文件以及对应的处理器来处理对应的标志事情,因此SAX这种解析方式就可以顺利地进行解析工作了
(3)上面测试的输出如下:
SAX解析开始 <employees>元素'user'的属性是: 属性名:id,属性值: 1 <user><name>zifangsky<name/> <age>10<age/> <sex>male<sex/> <contact>https://www.zifangsky.cn<contact/> <user/> 元素'user'的属性是: 属性名:id,属性值: 2 <user><name>admin<name/> <age>20<age/> <sex>male<sex/> <contact>https://www.tar.pub<contact/> <user/> <employees/> SAX解析结束
四 JDOM解析XML文件
跟前面两种解析方式不同的是,使用JDOM来解析XML文件需要下载额外的jar包
(1)下载jar包并导入到项目中:
下载地址:http://www.jdom.org/downloads/index.html
目前最新版本是:JDOM 2.0.6
然后将下载得到的“jdom-2.0.6.jar”文件导入到测试项目中
注:关于如何在一个Java项目中导入额外的jar,这里将不多做解释,不太会的童鞋可以自行百度
(2)测试代码:
JDOMTest.java:
package cn.zifangsky.xml; import java.util.List; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.input.SAXBuilder; public class JDOMTest { /** * @param args */ public static void main(String[] args) { SAXBuilder saxBuilder = new SAXBuilder(); try { Document document = saxBuilder.build("src/cn/zifangsky/xml/demo1.xml"); //获取XML文件的根节点 Element rootElement = document.getRootElement(); // System.out.println(rootElement.getName()); List<Element> usersList = rootElement.getChildren(); //获取子节点 for(Element u : usersList){ // List<Attribute> attributes = u.getAttributes(); // for(Attribute attribute : attributes){ // System.out.println("属性名:" + attribute.getName() + ",属性值:" + attribute.getValue()); // } System.out.println("'id'的值是: " + u.getAttributeValue("id")); } }catch (Exception e) { e.printStackTrace(); } } }
从上面的代码可以看出,使用JDOM来解析XML文件,主要需要做以下几个步骤:
新建一个SAXBuilder
通过SAXBuilder的build方法传入一个XML文件的路径得到Document
通过Document的getRootElement方法获取根节点
通过getChildren方法获取根节点的所有子节点
然后是遍历每个子节点,获取属性、属性值、节点名、节点值等内容
如果该节点也有子节点,然后同样可以通过getChildren方法获取该节点的子节点
后面的步骤跟上面一样,不断递归到文本节点截止
(3)上面测试的输出如下:
'id'的值是: 1 'id'的值是: 2
五 DOM4J解析XML文件
jar包下载地址:https://sourceforge.net/projects/dom4j/files/
同样,在使用DOM4J解析XML文件时需要往项目中引入“dom4j-1.6.1.jar”文件
(1)一个简单实例:
i)DOM4JTest.java:
package cn.zifangsky.xml; import java.io.File; import java.util.Iterator; import java.util.List; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; public class DOM4JTest { public static void main(String[] args) { SAXReader reader = new SAXReader(); try { Document document = reader.read(new File("src/cn/zifangsky/xml/demo1.xml")); //获取XML文件的根节点 Element rootElement = document.getRootElement(); System.out.println(rootElement.getName()); //通过elementIterator方法获取迭代器 Iterator<Element> iterator = rootElement.elementIterator(); //遍历 while(iterator.hasNext()){ Element user = iterator.next(); //获取属性并遍历 List<Attribute> aList = user.attributes(); for(Attribute attribute : aList){ System.out.println("属性名:" + attribute.getName() + ",属性值:" + attribute.getValue()); } //子节点 Iterator<Element> childList = user.elementIterator(); while(childList.hasNext()){ Element child = childList.next(); // System.out.println(child.getName() + " : " + child.getTextTrim()); System.out.println(child.getName() + " : " + child.getStringValue()); } } } catch (Exception e) { e.printStackTrace(); } } }
从上面的代码可以看出,跟前面的JDOM解析方式流程是差不多的,并且关键地方也有注释,因此这里就不多做解释了
ii)上面的代码输出如下:
employees 属性名:id,属性值:1 name : zifangsky age : 10 sex : male contact : https://www.zifangsky.cn 属性名:id,属性值:2 name : admin age : 20 sex : male contact : https://www.tar.pub
(2)将XML文件解析成Java对象:
i)为了方便测试,这里准备一个新的XML文件:
demo2.xml:
<?xml version="1.0" encoding="UTF-8" ?> <user id="2"> <name>zifangsky</name> <age>100</age> <sex>男</sex> <contact>https://www.zifangsky.cn</contact> <ownPet id="1">旺财</ownPet> <ownPet id="2">九头猫妖</ownPet> </user>
ii)同时准备一个Java实体类,恰好跟上面的XML文件中的属性相对应:
User.java:
package cn.zifangsky.xml; import java.util.List; public class User { private String name; private String sex; private int age; private String contact; private List<String> ownPet; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getContact() { return contact; } public void setContact(String contact) { this.contact = contact; } protected List<String> getOwnPet() { return ownPet; } protected void setOwnPet(List<String> ownPet) { this.ownPet = ownPet; } @Override public String toString() { return "User [name=" + name + ", sex=" + sex + ", age=" + age + ", contact=" + contact + ", ownPet=" + ownPet + "]"; } }
iii)测试代码:
XMLtoJava.java:
package cn.zifangsky.xml; import java.io.File; import java.util.ArrayList; import java.util.List; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; public class XMLtoJava { public User parseXMLtoJava(String xmlPath){ User user = new User(); List<String> ownPet = new ArrayList<String>(); SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new File(xmlPath)); Element rootElement = document.getRootElement(); //获取根节点 List<Element> children = rootElement.elements(); //获取根节点的子节点 //遍历 for(Element child : children){ String elementName = child.getName(); //节点名 String elementValue = child.getStringValue(); //节点值 switch (elementName) { case "name": user.setName(elementValue); break; case "sex": user.setSex(elementValue); break; case "age": user.setAge(Integer.valueOf(elementValue)); break; case "contact": user.setContact(elementValue); break; case "ownPet": ownPet.add(elementValue); break; default: break; } } user.setOwnPet(ownPet); } catch (Exception e) { e.printStackTrace(); } return user; } public static void main(String[] args) { XMLtoJava demo = new XMLtoJava(); User user = demo.parseXMLtoJava("src/cn/zifangsky/xml/demo2.xml"); System.out.println(user); } }
经过前面的分析之后,上面这个代码也是很容易理解的:通过遍历节点,如果节点名跟Java类中的某个属性名相对应,那么就将节点值赋值给该属性
iv)上面的代码输出如下:
User [name=zifangsky, sex=男, age=100, contact=https://www.zifangsky.cn, ownPet=[旺财, 九头猫妖]]
(3)解析一个XML文件并尽可能原样输出:
DOM4JTest2:
package cn.zifangsky.xml; import java.io.File; import java.util.List; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; public class DOM4JTest2 { /** * 解析XML文件并尽可能原样输出 * * @param xmlPath * 待解析的XML文件路径 * @return null * */ public void parse(String xmlPath) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new File(xmlPath)); Element rootElement = document.getRootElement(); print(rootElement, 0); } catch (Exception e) { e.printStackTrace(); } } /** * 打印一个XML节点的详情 * * @param element * 一个XML节点 * @param level * 用于判断xml节点前缩进多少的标识,每深入一层则多输出4个空格 * @return null * */ public void print(Element element, int level) { List<Element> elementList = element.elements(); // 当前节点的子节点List // 空格 StringBuffer spacebBuffer = new StringBuffer(""); for (int i = 0; i < level; i++) spacebBuffer.append(" "); String space = spacebBuffer.toString(); // 输出开始节点及其属性值 System.out.print(space + "<" + element.getName()); List<Attribute> attributes = element.attributes(); for (Attribute attribute : attributes) System.out.print(" " + attribute.getName() + "=\"" + attribute.getText() + "\""); // 有子节点 if (elementList.size() > 0) { System.out.println(">"); // 遍历并递归 for (Element child : elementList) { print(child, level + 1); } // 输出结束节点 System.out.println(space + "</" + element.getName() + ">"); } else { // 如果节点没有文本则简化输出 if (element.getStringValue().trim().equals("")) System.out.println(" />"); else System.out.println(">" + element.getStringValue() + "</" + element.getName() + ">"); } } public static void main(String[] args) { DOM4JTest2 test2 = new DOM4JTest2(); test2.parse("src/cn/zifangsky/xml/demo3.xml"); } }
这段代码同样没有什么新的东西,原理就是利用递归来不断进行解析输出,注意一下不同层次的节点的缩进即可。刚开始测试时建议用一些结构比较简单的代码,如上面的demo1.xml和demo2.xml文件。在测试没问题时可以选择一些复杂的XML文件来测试是否能够正常输出,比如:
demo3.xml:
<?xml version="1.0" encoding="UTF-8"?> <application xmlns="http://wadl.dev.java.net/2009/02" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <grammars /> <resources base="http://localhost:9080/Demo/services/json/checkCode"> <resource path="/"> <resource path="addCheckCode"> <method name="POST"> <request> <representation mediaType="application/octet-stream" /> </request> <response> <representation mediaType="application/xml"> <param name="result" style="plain" type="xs:int" /> </representation> <representation mediaType="application/json"> <param name="result" style="plain" type="xs:int" /> </representation> </response> </method> </resource> <resource path="findCheckCodeByProfileId"> <method name="POST"> <request> <representation mediaType="application/octet-stream"> <param name="request" style="plain" type="xs:long" /> </representation> </request> <response> <representation mediaType="application/xml" /> <representation mediaType="application/json" /> </response> </method> </resource> </resource> </resources> </application>
为什么我在标题上说的是尽可能原样输出,其原因就是上面那段解析代码在碰到下面这种XML节点时,输出就不一样了:
<dc:creator><![CDATA[admin]]></dc:creator> <category><![CDATA[运维]]></category> <category><![CDATA[zabbix]]></category> <category><![CDATA[端口]]></category>
这段XML文档节点最后输出如下:
<creator>admin</creator> <category>运维</category> <category>zabbix</category> <category>端口</category>