詳解Java中的XML解析

詳解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等,并且通過具體操作更加具體地了解了其操作的過程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容