詳解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表達式如下所示:

/ :從根節點開始查找
//:從當前節點開始查找
. :選擇當前節點
..:選擇當前節點的父節點
@:指定元素<br/>
還有其他一些表達式,可以參考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等,並且通過具體操作更加具體地了解了其操作的過程。