详解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阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容