詳解Java中的XML解析
前言
XML,全稱Extensibible Markup Language, 主要用于數據的保存或者文件傳輸,其主要特性如下所示:
- 以標簽為主的標記語言
- 支持自定義標簽,支持自我解釋
- 與具體技術無關
- 支持驗證
- 方便人類的讀寫
XML示例
為了更好的了解XML,下面我們提供一個簡單的XML文件,內容如下所示:
<?xml version="1.0" encoding="UTF-8" ?>
<!--
根元素為students
注意XML文件中有且僅有一個根元素
-->
<students>
<!--
子元素student
id屬性同樣可以作為student的子元素
為了演示方便,這里將其作為屬性
-->
<student id="123">
<!--
student有三個子元素
name、age、gender
-->
<name>xuhuanfeng</name>
<age>22</age>
<gender>male</gender>
</student>
<!--同上-->
<student id="456">
<name>Tom</name>
<age>23</age>
<gender>femal</gender>
</student>
<!--同上-->
<student id="789">
<name>Lily</name>
<age>24</age>
<gender>femal</gender>
</student>
</students>
在XML中每個元素都可以有子元素/值,元素可以有屬性,具體關于XML的內容還請查看官方的文檔,接下來的內容主要為Java對XML文件的解析。
XML解析
XML解析主要有兩種方式,一種稱為DOM解析,另外一種稱之為SAX解析。
- DOM解析:Document Object Model,簡單的來講,DOM解析就是讀取XML文件,然后在文件文檔描述的內容在內存中生成整個文檔樹。
- SAX解析:Simple API for XML,簡單的來講,SAX是基于事件驅動的流式解析模式,一邊去讀文件,一邊解析文件,在解析的過程并不保存具體的文件內容。
兩種解析方式各有千秋,也都有各自的有點和缺點,這里簡單羅列如下:
-
DOM解析:
- 優點:在內存中形成了整個文檔樹,有了文檔樹,就可以隨便對文檔中任意的節點進行操作(增加節點、刪除節點、修改節點信息等),而且由于已經有了整個的文檔樹,可以實現對任意節點的隨機訪問。
- 缺點:由于需要在內存中形成文檔樹,需要消耗的內存比較大,尤其是當文件比較大的時候,消耗的代價還是不容小視的。
-
SAX解析:
- 優點:SAX解析由于是一邊讀取文檔一邊解析的,所以所占用的內存相對來說比較小。
- 缺點:無法保存文檔的信息,無法實現隨機訪問節點,當文檔需要編輯的時候,使用SAX解析就比較麻煩了。
對XML的兩種不同解析機制有一定的了解之后,接下來我們就來具體的看下,在Java中是如何解析的。
DOM解析
關于DOM的解析,這里就不再做過多的解釋了,直接通過代碼來查看具體的操作過程
解析文檔
public void parse() {
// students的內容為上面所示XML代碼內容
File file = new File("D:/students.xml");
try {
// 創建文檔解析的對象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 解析文檔,形成文檔樹,也就是生成Document對象
Document document = builder.parse(file);
// 獲得根節點
Element rootElement = document.getDocumentElement();
System.out.printf("Root Element: %s\n", rootElement.getNodeName());
// 獲得根節點下的所有子節點
NodeList students = rootElement.getChildNodes();
for (int i = 0; i < students.getLength(); i++){
// 獲得第i個子節點
Node childNode = students.item(i);
// 由于節點多種類型,而一般我們需要處理的是元素節點
// 元素節點就是非空的子節點,也就是還有孩子的子節點
if (childNode.getNodeType() == Node.ELEMENT_NODE){
Element childElement = (Element)childNode;
System.out.printf(" Element: %s\n", childElement.getNodeName());
System.out.printf(" Attribute: id = %s\n", childElement.getAttribute("id"));
// 獲得第二級子元素
NodeList childNodes = childElement.getChildNodes();
for (int j = 0; j < childNodes.getLength(); j++){
Node child = childNodes.item(j);
if (child.getNodeType() == Node.ELEMENT_NODE){
Element eChild = (Element) child;
System.out.printf(" sub Element: %s value= %s\n", eChild.getNodeName(), eChild.getTextContent());
}
}
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
解析的結果如下所示:
Root Element: students
Element: student
Attribute: id = 123
sub Element: name value= xuhuanfeng
sub Element: age value= 22
sub Element: gender value= male
# 其余兩個student節點由于篇幅原因這里省略...
當我們需要特定的節點的數據的時候,可以根據具體的數據從上面的解析過程中進行數據的篩選即可,所以這里不演示如果進行數據的選取了(畢竟整個文檔的內容都讀取出來了:))
編輯文檔
由于DOM解析是直接在內存中生成對應的文檔樹,所以我們可以很方便地對其進行編輯,這里演示修改id = 123的子元素name的值為Huanfeng.Xu,具體代碼如下所示:
public void modify(){
try {
// 生成文檔樹的過程同前面所示,這里不進行過多的解釋
File file = new File("d:/students.xml");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(file);
Element rootElement = document.getDocumentElement();
NodeList students = rootElement.getChildNodes();
for (int i = 0; i < students.getLength(); i++){
Node tmp = students.item(i);
if (tmp.getNodeType() == Node.ELEMENT_NODE){
Element element = (Element)tmp;
// 獲得id為123的student節點
String attr = element.getAttribute("id");
if ("123".equalsIgnoreCase(attr)){
NodeList childNodes = element.getChildNodes();
for (int j = 0; j < childNodes.getLength(); j++){
Node childNode = childNodes.item(j);
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
Element childElement = (Element) childNode;
// 修改子節點name的值
if (childElement.getNodeName().equalsIgnoreCase("name")) {
childElement.setTextContent("Huanfeng.Xu");
break;
}
}
}
}
}
}
// 獲得Transformer對象,用于輸出文檔
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
// 封裝成DOMResource對象
DOMSource domSource = new DOMSource(document);
Result result = new StreamResult("d:/newStudents.xml");
// 輸出結果
transformer.transform(domSource, result);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (TransformerConfigurationException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
}
}
可以看到,基本的操作跟解析文檔是一致的,這也非常好理解,修改嘛,肯定先要解析文檔然后獲得需要修改的節點信息,這里同樣可以對節點進行刪除、增加操作,原理同上,這里就不進行演示。
SAX解析
關于SAX解析的原理,這里就不再做過多的解釋,同上面DOM的解析一樣,這里我們直接通過代碼來查看具體的操作過程
解析文檔
/**
* 由于SAX解析是基于事件機制的,也就是當遇到指定元素的時候,解析器就會自動調用
* 回調函數,所以使用SAX解析的時候,需要創建自定義的Handler并且繼承自DefaultHandler
* 并且將其傳給解析器,用于指定需要進行回調的內容
*/
class SAXHandler extends DefaultHandler{
/**
* 用于標志是否已經讀取到指定的元素
*/
private boolean isName;
private boolean isAge;
private boolean isGender;
@Override
public void startDocument() throws SAXException {
System.out.println("Starting parse the document");
}
@Override
public void endDocument() throws SAXException {
System.out.println("Ending parse the document");
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if ("student".equalsIgnoreCase(qName)){
System.out.println("student");
}else if ("name".equalsIgnoreCase(qName)){
isName = true;
}else if ("age".equalsIgnoreCase(qName)){
isAge = true;
}else if ("gender".equalsIgnoreCase(qName)){
isGender = true;
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String content = new String(ch, start, length);
if (isName){
System.out.printf(" Name: %s\n", content);
isName = false; // 這里需要額外注意,當讀取到一個節點之后,需要
// 把該節點的標志去除,不然下一次讀取會出現問題
}else if (isAge){
System.out.printf(" Age: %s\n", content);
isAge = false;
}else if (isGender){
System.out.printf(" Gender: %s\n", content);
isGender = false;
}
}
}
public void parser(){
try {
File file = new File("d:/students.xml");
// 創建一個SAX解析器
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
javax.xml.parsers.SAXParser parser = saxParserFactory.newSAXParser();
// 解析對應的文件
parser.parse(file, new SAXHandler());
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
對應的輸出結果如下所示:
student
Name: xuhuanfeng
Age: 22
Gender: male
# 這里由于篇幅原因,省略其他兩個輸出內容
由于SAX解析本身不利于節點的保存以及編輯,所以這里就不演示器編輯的過程。
第三方類庫解析
上面的內容就是XML解析的最基本的操作了,不過,由于原生API操作不方便,加上效率不怎么高,所以就出現了許多的第三方的解析類庫,最常使用的包括了JDOM、StAX、XPath、DOM4j等,下面我們將逐個演示其操作
JDOM解析
JDOM是我們所要接觸的第一個第三方解析類庫,其操作的原理是基于DOM解析操作,不過JDOM的解析效率比原生操作高,內存占用相對低,使用的時候需要導入JDOM的jar文件,下載地址
解析文檔
public void parse(){
try {
File file = new File("d:/students.xml");
// 獲得一個解析器
SAXBuilder saxBuilder = new SAXBuilder();
Document document = saxBuilder.build(file);
// 獲得根元素
Element rootElement = document.getRootElement();
System.out.printf("Root Element %s\n", rootElement.getName());
List<Element> elements = rootElement.getChildren();
for (Element e : elements){
System.out.printf(" %s\n", e.getName());
System.out.printf(" Name: %s\n", e.getChild("name").getTextTrim());
System.out.printf(" Age: %s\n", e.getChild("age").getTextTrim());
System.out.printf(" Gender: %s\n", e.getChild("gender").getTextTrim());
}
} catch (JDOMException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
可以看到使用JDOM進行解析是比較方便的,而且由于JDOM使用了List等容器類,更加方便操作了。
StAX解析
StAx是我們要使用的第二個第三方解析類庫,StAX的實現原理為SAX操作,不過StAX提供了比原生SAX解析更加方便的操作,使用時同樣需要導入其jar文件,下載地址
解析文檔
public void parse(){
boolean isName = false;
boolean isAge = false;
boolean isGender = false;
try {
File file = new File("d:/students.xml");
// 獲得解析器
XMLInputFactory factory = XMLInputFactory.newFactory();
XMLEventReader reader = factory.createXMLEventReader(new FileReader(file));
while (reader.hasNext()){
// 獲得事件
XMLEvent event = reader.nextEvent();
switch (event.getEventType()){
// 解析事件的類型
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
String qName = startElement.getName().getLocalPart();
if ("name".equalsIgnoreCase(qName)){
isName = true;
}else if ("age".equalsIgnoreCase(qName)){
isAge = true;
}else if ("gender".equalsIgnoreCase(qName)){
isGender = true;
}
break;
case XMLStreamConstants.CHARACTERS:
Characters characters = event.asCharacters();
if (isName){
System.out.printf(" Name: %s\n", characters.getData());
isName = false;
}else if (isAge){
System.out.printf(" Age: %s\n", characters.getData());
isAge = false;
}else if (isGender){
System.out.printf(" Gender: %s\n", characters.getData());
isGender = false;
}
break;
}
}
} catch (XMLStreamException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
XPath
XPath從嚴格意義上來講并不是一種解析方式,不過XPath提供了一種定位節點的方式,XPath表達式,通過該表達式,我們可以定位到指定特性的一個或者一組節點
常用的XPath表達式如下所示:
/ :從根節點開始查找
//:從當前節點開始查找
. :選擇當前節點
..:選擇當前節點的父節點
@:指定元素
還有其他一些表達式,可以參考XPath表達式
解析文檔
public void parse(){
try {
File file = new File("d:/students.xml");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 創建xpath對象
XPath xPath = XPathFactory.newInstance().newXPath();
Document document = builder.parse(file);
// 編寫xpath表達式
String expression = "/students/student";
NodeList students = (NodeList)xPath.compile(expression).evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < students.getLength(); i++){
Node node = students.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE){
Element element = (Element) node;
System.out.printf(" Element: %s\n", element.getNodeName());
System.out.printf(" Name: %s\n", element.getElementsByTagName("name").item(0).getTextContent());
System.out.printf(" Age: %s\n", element.getElementsByTagName("age").item(0).getTextContent());
System.out.printf(" Gender: %s\n", element.getElementsByTagName("gender").item(0).getTextContent());
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (XPathExpressionException e) {
e.printStackTrace();
}
}
可以看到,使用XPath技術本質上還是使用DOM解析,只不過借助XPath表達式,可以很方便地定位到指定元素
DOM4J解析
DOM4J是一個比較優秀的解析類庫,也是目前使用得比較多的庫類,使用的時候可以配合XPath技術來輔助定位某一個節點,使用的時候需要導入對應的jar文件,下載地址,注意使用DOM4J的時候需要導入兩個jar文件,DOM4J本身的jar文件以及jaxen文件
解析文檔
public void parse() throws DocumentException {
File file = new File("d:/students.xml");
// 加載文檔
SAXReader reader = new SAXReader();
Document document = reader.read(file);
Element rootElement = document.getRootElement();
System.out.printf("Root Element: %s\n", rootElement.getName());
// 使用XPath表達式來定位節點
List<Node> students = document.selectNodes("/students/student");
for (Node n: students){
System.out.printf("Element: %s\n", n.getName());
System.out.printf("Name: %s\n", n.selectSingleNode("name").getText());
System.out.printf("Age: %s\n", n.selectSingleNode("age").getText());
System.out.printf("Gender: %s\n", n.selectSingleNode("gender").getText());
}
}
可以看到,使用DOM4J解析文檔是非常方便的,不僅如此,使用DOM4J生成文檔也是非常方便的
生成文檔
public void create() throws IOException {
Document document = DocumentHelper.createDocument();
Element root = document.addElement("students");
Element student = root.addElement("student");
student.addElement("name")
.addText("xuhuanfeng");
student.addElement("age")
.addText("22");
OutputFormat format = OutputFormat.createPrettyPrint();
XMLWriter writer = new XMLWriter(System.out);
writer.write(document);
}
總結
本節我們學習了XML解析的機制,包括了DOM解析以及SAX解析,并且通過具體實例使用不同解析技術進行解析,還了解了幾個常用的XML解析類庫,包括了JDOM、StAX、XPath、DOM4J等,并且通過具體操作更加具體地了解了其操作的過程。