XML详解:第三部分 XML解析

XML解析

如果不同厂商开发的XML解析器提供的API都依赖于自向的产品,那么应用程序要使用解析器,就只能使用特定于厂商的API。假如以后应用程序需要更换解析器,那么就只能重新编写代码了。庆幸的是,目前几乎所有的解析器都对两套标准的API提供了支持,这两套API就是DOMSAX

 

DOMDocument Object Model)的缩写,即文档对象模型,是W3C组织推荐的处理XML的标准接口。SAXSimple API for XML的缩写,它不是某个官方机构的标准,但它是XML社区事实上的标准。虽然只是“民间”的标准,但是它在XML中的应用不比DOM少,几乎所有XML解析器都支持它。

 

DOMSAX只是定义了一些接口,以及某些接口的默认实现(什么事情也不做),一个应用程序要想利用DOMSAX访问XML文档,还需要一个实现了DOMSAX的解析器,即实现DOMSAX中定义的接口,提供DOMSAX定义的功能。

 

ApacheXerces是一个使用非常广泛的解析器,它提供了DOMSAX的调用接口。SAX的标准解析接口为org.xml.sax.XMLReader,而Xerces中提供的解析接口的实现类为org.apache.xerces.parsers.SAXParser,在应用程序中可以采用如下方式访问XML文档:

org.xml.sax.XMLReader sp = new org.apache.xerces.parsers.SAXParser();

现有一个问题,虽然我们使用的是标准的DOMSAX接口,但不同解析器的实现类是不同的,如果要使用另外一种解析器,仍然需要修改应用程序,只不过修改的代码量较少。有没有办法让我们在更换解析器时,不用对已发布程序做任何改变呢?JAXP API可以帮我们实现这一点。为了屏蔽具体厂商的实现,让开发人员以一种标准的方式对XML进行编程,SUN制定了JAXPJava API for XML Processing规范。JAXP没有提供解析XML的新方法,也没有为XML的处理提供新的功能,它只是在解析器之上封闭了一个抽象层,允许开发人员以独立于厂商的API调用访问XML数据。

 

JAXP开发包由javax.xml包及其子包、org.w3c.dom包及其子包、org.xml.sax包及其子包组成。在javax.xml.parsers包中,定义了几个工厂类,用于加载DOMSAX的实现类,JAXP由接口、抽象类和一些辅助类组成,符合JAXP规范的解析器实现其中的接口和抽象类,开发人员只要使用JAXPAPI编程,底层的解析器就可以任意更换。

应用程序àJAXP的接口与抽象类àXercesJAXP实现àXercesDOMSAX解析器

有了JAXP,开发人员就可以随意更换解析器的实现,而不需要修改应用程序。

使用DOM解析XML

DOM是独立于程序语言的,W3C组织以IDL(接口中定义语言)的形式定义了DOM中接口。某种语言要实现DOM,需要将DOM接口转换为本语言中的对应结构。W3C提借了JavaECMAScript这两种语言的实现。

 

 

Node对象是DOM结构中最基本的对象,代表了文档树中的一个节点,通常直接使用Node很少,一般使用DocumentElementAttrTextNode对象的子对象来操作文档。虽然在Node接口中定义了对节点进行操作的通用方法,但是有一些Node对象的子对象,如Text对象并不存在子节点。

image003

1、  Node接口主要是对它的子节点进行增、删、获取。

2、  Node接口中定义了各种节点的类型常量,可以用它们来判断是哪种节点。

3、  void normalize():将该节点所有的后代文本节点,包括属性节点,调整为规范化的形式,这仅是以结构(如:元素、注释、处理指令、CDATA段、实体引用)来分隔文本节点,也就是说,在节点所在的这棵树下,既不存在相邻的文本节点,也不存在空的文本节点。

 

 

Node节点的getNodeNamegetNodeValue getAttributes 的值将根据以下节点类型的不同而不同。

Interface(节点类型)

getNodeName(节点名字)

getNodeValue(节点值)

getAttributes(节点属性)

Attr

Attr.name 相同

Attr.value 相同

null

CDATASection

"#cdata-section"

CharacterData.data 相同,CDATA 节的内容

null

Comment

"#comment"

CharacterData.data 相同,该注释的内容

null

Document

"#document"

null

null

DocumentFragment

"#document-fragment"

null

null

DocumentType

DocumentType.name 相同

null

null

Element

Element.tagName 相同

null

NamedNodeMap

Entity

entity name

null

null

EntityReference

引用的实体名称

null

null

Notation

notation name

null

null

ProcessingInstruction

ProcessingInstruction.target 相同

ProcessingInstruction.data 相同

null

Text

"#text"

CharacterData.data 相同,该文本节点的内容

null

DOM树中的节点类型

XML中最常见的节点类型是:文档、元素、文本和属性节点,在DOM API中对应的接口是DocumentElementTextAttr

文档节点Document

文档节点是文档树的根节点,也是文档中其他所有节点的父节点。要注意的是,文档节点并不是XML文档的根元素,因为在XML文档中,处理指令、注释等内容可以出现在根元素以外,所以在构造DOM树时,根元素并不适合作为根节点,于是就有了文档节点,而根元素则作为文档节点的子节点。image004

4、  通过 Element getDocumentElement()方法得到XML文档的根元素。

5、  通过createXX相应的方法创建不同类型的节点。

6、  Element getElementById(String elementId):通过给出的ID类型的属性值elementId来查找对应用的元素。一个ID类型的属性值唯一标识了XML文档中的一个元素。除非特别定义,名字为“ID”或者“id”的属性,其类型并不是ID类型。

7、  NodeList getElementsByTagName(String tagname):以文档顺序返回标签名字为tagname的所有元素。如果参数为“*”,则返回所有的元素。

8、  NodeList getElementsByTagNameNS(String namespaceURI,String localName):按照指定的名称空间URI和元素的本地名返回所有匹配的元素。如果参数namespaceURI为“*”,则表示所有的名称空间,同样,如果localName为“*”,则匹配所有元素。

元素节点Element

image005

文本节点Text

image006

属性节点Attr

属性实际上是属于某个元素的,所以属性节点不是元素的子节点。因而在DOM中,属性节点没有被作为文档树的一部分,所以在属性节点上调用getParentNodegetPreviousSiblinggetNextSibling时返回null

image007

NodeList接口

image008

Node

NodeList getChildNodes();

 

DocumentElement

NodeList getElementsByTagName(String tagname);

NodeList getElementsByTagNameNS(String namespaceURI, String localName);

NamedNodeMap接口

image009

Node

NamedNodeMap getAttributes();

 

DocumentType

NamedNodeMap getEntities();

NamedNodeMap getNotations();

DOM解析器工厂和DOM解析器

javax.xml.parsers包中,定义了DOM解析器工厂类DocumentBuilderFactory,用来产生DOM解析器。DocumentBuilderFactory工厂类是一个抽象类,在这个类中提供了一个静态方法newInstance()方法,用来创建一个工厂类的实例。

 

DocumentBuilderFactory是个抽象类,那它的newInstance()产生的是哪个实现呢?采用JAXP编程可以任意更换解析器的关键就在于这个工厂类。DocumentBuilderFactory抽象类的实现由遵照JAXP规范的解析器提供商来给出的。解析器提供商为自身的解析器编写一个从DocumentBuilderFactory类继承的工厂类,然后由这个工厂类实例负责产生解析器对象。

 

那么DocumentBuilderFactorynewInstance()方法是如何找到解析器提供商给出的工厂类呢?可通过下面3种途径依次查找解析器工厂类:

1、  首先查看是否设置了javax.xml.parsers.DocumentBuilderFactory系统属性。这又可通过两种方式来设置这个系统属性:

    System.setProperty("javax.xml.parsers.DocumentBuilderFactory",

           "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");

建议不要使用此种方式,因为如果更换解析器,程序就要修改。另一种是在用java.exe执行程序时,通过-D选项来设置系统属性:

java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocument DOMTest

2、  查找JRE目录中lib子目录下的jaxp.properties文件,如果存在,则读取该配置文件:

javax.xml.parsers.DocumentBuilderFactory=org.apache.xerces.jaxp.DocumentBuilderFactoryImpl

3、  classpath环境变量所指定JAR文件中,查找META-INF/services目录下的javax.xml.parsers.DocumentBuilderFactory文件,使用这个文件中所指定的工厂类名来构造工厂的实例,这种方式被多数解析器提供商所采用,在他们的发布的包包含解析器的JAR包中,往往会提供这个的文件,我们来看看Apache提供的解析器实现包xercesImpl.jar

image010

其中javax.xml.parsers.DocumentBuilderFactory文件的内容如下:

org.apache.xerces.jaxp.DocumentBuilderFactoryImpl

如果上面3种途径都没有找到解析器工厂类,就使用平台默认的解析器工厂。从JAXP1.2开始,SUN公司对ApacheXerces解析器重新包装了一下,并将org.apache.xerces包名改为了com.sun.org.apache.xerces.internal,然后在JAXP的开发包中一起提供,作为默认的解析器。

 

在得到工厂实例后,就可以通过DocumentBuilderFactorynewDocumentBuilder()方法来创建DOM解析器实例了:

DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();

DocumentBuilder db = dbf.newDocumentBuilder();//得到具体厂商的XML解析器

Document d = db.parse("text.xml");//通过解析器解析得到文档对象

 

DocumentBuilderFactory另外几个重要的方法:

l  public void setValidating(boolean validating):指定解析器是否难被解析的文档,默认为false。注,该方法只能DTD验证有效。如果使用Schema验证,则可设为false后,使用setSchema(Schema)方法将一个模式与解析关联。

l  public void setSchema(Schema schema):如果要使用模式对XML文档进行验证,需要使用该方法。Schema对象由javax.xml.validation.SchemaFactory工厂类创建。

l  public void setIgnoringElementContentWhitespace(boolean whitespace):是否要忽略元素内容中的空白。默认是false。按照XML1.0的推荐标准,元素内容中的空白必须由解析器保留,但当根据DTD进行验证时,解析器可以知道文档的特定部分不会支持空格(如具有元素型内容的元素),所以这一区域的任何空格都“可忽略的”。注,这一标志只有通过setValidating打开验证功能后才有效。

使用DOM解析XML文档的实例

--students.xml

<?xml version="1.0" encoding="GB2312"?>

 

<?xml-stylesheet type="text/xsl" href="students.xsl"?>

 

<students>

    <student sn="01">

        <name>张三</name>

        <age>18</age>

    </student>

   

    <student sn="02">

        <name>李四</name>

        <age>20</age>

    </student>

</students>

--end

遍历文档

publicclass DOMPrinter {

    /**

     * 输出节点的类型、名字和值。

     */

    publicstaticvoid printNodeInfo(String nodeType, Node node) {

       System.out.println(nodeType + "\t" + node.getNodeName() + " : "

              + node.getNodeValue());

    }

    /**

     * 采用递归调用,输出给定节点下的所有后代节点。

     * 注:为了简单起见,只对处理指令节点、元素节点、

     * 属性节点和文本节点进行了处理。

     */

    publicstaticvoid traverseNode(Node node) {

       short nodeType = node.getNodeType();

       switch (nodeType) {

       case Node.PROCESSING_INSTRUCTION_NODE:

           printNodeInfo("处理指令", node);

           break;

       case Node.ELEMENT_NODE:

           printNodeInfo("元素", node);

           NamedNodeMap attrs = node.getAttributes();

           int attrNum = attrs.getLength();

           for (int i = 0; i < attrNum; i++) {

              Node attr = attrs.item(i);

              printNodeInfo("属性", attr);

           }

           break;

       case Node.TEXT_NODE:

           printNodeInfo("文本", node);

           break;

       default:

           break;

       }

       Node child = node.getFirstChild();

       while (child != null) {

           // 递归调用

           traverseNode(child);

           child = child.getNextSibling();

       }

    }

    publicstaticvoid main(String[] args) {

       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

       try {

           DocumentBuilder db = dbf.newDocumentBuilder();

           Document doc = db.parse(new File("students.xml"));

           // Document接口提供了三个方法,分别用于获取XML声明的三个部分:

           // 版本、文档编码、独立文档。

           // 调用这三个方法需要的JDK版本最小是1.5

           System.out.println("<?xml='" + doc.getXmlVersion() + "' encoding='"

                  + doc.getXmlEncoding() + "' standalone='"

                  + doc.getXmlStandalone() + "'?>");

           traverseNode(doc);

       } catch (ParserConfigurationException e) {

           e.printStackTrace();

       } catch (SAXException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       }

    }

}

添加、删除、修改和保存

publicclass DOMConvert {

    publicstaticvoid main(String[] args) {

       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

       try {

           DocumentBuilder db = dbf.newDocumentBuilder();

           Document doc = db.parse("students.xml");

 

           // ------------------添加节点------------------

           // 创建表示一个学生信息的各元素节点。

           Element eltStu = doc.createElement("student");

           Element eltName = doc.createElement("name");

           Element eltAge = doc.createElement("age");

 

           // 创建<student>元素的sn属性节点。

           Attr attr = doc.createAttribute("sn");

           attr.setValue("03");

 

           // 创建代表学生信息的文本节点。

           Text txtName = doc.createTextNode("王五");

           Text txtAge = doc.createTextNode("19");

 

           // 将文本节点添加为对应的元素节点的子节点。

           eltName.appendChild(txtName);

           eltAge.appendChild(txtAge);

 

           // nameage节点添加为student节点的子节点。

           eltStu.appendChild(eltName);

           eltStu.appendChild(eltAge);

 

           // <student>元素添加sn属性节点。

           eltStu.setAttributeNode(attr);

 

           // 得到XML文档的根元素。

           Element eltRoot = doc.getDocumentElement();

 

           // student节点添加为根元素的子节点。

           eltRoot.appendChild(eltStu);

 

           NodeList nl = doc.getElementsByTagName("student");

 

           // ------------------删除节点------------------

           Node nodeDel = nl.item(0);

           nodeDel.getParentNode().removeChild(nodeDel);

 

           // ------------------修改节点------------------

           // 注意:NodeList对象是活动的,所以前面删除节点的操作会影响到NodeList对象,

           // NodeList中的节点对象会重新进行排列,此时,索引为0的节点是

           // 先前节点列表中索引为1的节点。

           Element eltChg = (Element) nl.item(0);

           Node nodeAgeChg = eltChg.getElementsByTagName("age").item(0);

           nodeAgeChg.getFirstChild().setNodeValue("22");

 

           // 输出修改后的学生信息。

           for (int i = 0; i < nl.getLength(); i++) {

              Element elt = (Element) nl.item(i);

 

              Node nodeName = elt.getElementsByTagName("name").item(0);

              Node nodeAge = elt.getElementsByTagName("age").item(0);

 

              String name = nodeName.getFirstChild().getNodeValue();

              String age = nodeAge.getFirstChild().getNodeValue();

 

              System.out.println("-----------------学生信息-----------------");

              System.out.println("编号:" + elt.getAttribute("sn"));

              System.out.print("姓名:");

              System.out.println(name);

 

              System.out.print("年龄:");

              System.out.println(age);

              System.out.println();

           }

          

           // ------------------保存修改结果------------------

           // 利用文档节点创建一个DOM输入源。

           DOMSource source = new DOMSource(doc);

 

           // converted.xml文件构造一个StreamResult对象,用于保存转换后结果。

           StreamResult result = new StreamResult(new File("converted.xml"));

 

           // 得到转换器工厂类的实例。

           TransformerFactory tff = TransformerFactory.newInstance();

           // 创建一个新的转换器,用于执行恒等转换,

           // 即直接将DOM输入源的内容复制到结果文档中。

           Transformer tf = tff.newTransformer();

           tf.setOutputProperty(OutputKeys.INDENT, "yes");//缩进

           tf.setOutputProperty(OutputKeys.ENCODING, "gb2312");//编码

           // 执行转换。

           tf.transform(source, 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解析XML

XMLReader(解析器接口)

SAX解析器接口和事件处理器接口在org.xml.sax包中定义,XMLReaderSAX2.0解析器必须实现的接口(SAX1.0解析器实现Parser接口,该接口已不再使用)。

l  setEntityResolver(EntityResolver resolver):注册一个实体解析器。

l  setDTDHandler(DTDHandler handler):注册一个DTD事件处理器。

l  setContentHandler(ContentHandler handler):注册一个内容事件处理器。

l  parse(InputSource input):解析XML文档。

ContentHandler(内容事件处理器接口)

SAXAPI定义了许多事件,这些事件分别由事件处理器中相应的方法去响应。在SAX1.0前使用的是DocumentHandler接口,SAX2.0中已被ContentHandler取代。

 

startPrefixMapping(String prefix, String uri):在一个前缀URI名称空间映射范围的开始时被调用。

<students xmlns:stu="http://www.sunin.org/students">

       …

</stuents>

SAX解析器解析到<stuedents>元素时,就会调用startPrefixMapping方法,将stu传递给prefiex参数,将http://www.sunin.org/students传递给uri参数,然后产生<students>元素的startElement事件。在产生<students>元素的endElement事件后,解析器将调用endPrefixMapping方法。

SAX解析器工厂

Dom类似,JAXP也为SAX解析器提供了工厂类——SAXParserFactory类。与DOM解析器工厂类不同的是,SAXParserFactory类的newInstance()方法查找的工厂类属性为:java.xml.parsers.SAXParserFactory,如果我们使用ApacheXerces解析器,可以配置如下:

java.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl

 

JAXP中定义的SAX解析器类是SAXParser,获取SAXParser类的实例与获取DocumentBuilder类的实例相似,在得到工厂类的实例后,通过SAXParserFactory 实现类实例的newSAXParser()方法得到 SAX解析器实例:public abstract SAXParser newSAXParser()

 

你可以调用SAXParser或者XMLReader中的parser()方法业解析文档,效果是完全一样的。不过在SAXParser中的parse()方法能接受更多的参数。可以对不同的XML文档数据源进行解析,因此使用起来比XMLReader要方便一些。

 

另外,与DOM不同的是,SAX本身也提供了创建XMLReader对象的工厂类,在org.xml.sax.helpers包中提供了XMLReaderFactory类,该类createXMLReader用于创建XMLReader对象。

 

实际上,SAXParserJAXPXMLReader实现类的一个包装类,在SAXPars中定义了getXMLReader()方法,用于返回它内部的XMLReader实例:abstract  XMLReader getXMLReader()

使用SAX解析XML

解析并完整输出XML文档

/**

* 使用SAX解析XML文档,实际上就是编写事件处理器。

* 为了简化事件处理器接口的实现,我们让SAXPrinter类继承DefaultHandler帮助类

*/

publicclass SAXPrinter extends DefaultHandler {

    // SAX解析器开始解析文档时,将会调用这个方法

    publicvoid startDocument() throws SAXException {

       // 输出XML声明。

       System.out.println("<?xml version='1.0' encoding='GB2312'?>");

    }

 

    // SAX解析器读取了处理指令,将会调用这个方法

    publicvoid processingInstruction(String target, String data)

           throws SAXException {

       // 输出文档中的处理指令。

       System.out.println("<?" + target + " " + data + "?>");

    }

 

    // SAX解析器读取了元素的开始标签后,将会调用这个方法

    publicvoid startElement(String uri, String localName, String qName,

           Attributes attrs) throws SAXException {

       // 输出元素的开始标签及其属性。

       System.out.print("<" + qName);

       int len = attrs.getLength();

       for (int i = 0; i < len; i++) {

           System.out.print(" ");

           System.out.print(attrs.getQName(i));

           System.out.print("=\"");

           System.out.print(attrs.getValue(i));

           System.out.print("\"");

       }

       System.out.print(">");

    }

 

    // SAX解析器读取了字符数据后,将会调用这个方法

    publicvoid characters(char[] ch, int start, int length)

           throws SAXException {

       // 输出元素的字符数据内容。

       System.out.print(new String(ch, start, length));

    }

 

    // SAX解析器读取了元素的结束标签后,将会调用这个方法

    publicvoid endElement(String uri, String localName, String qName)

           throws SAXException {

       // 输出元素的结束标签。

       System.out.print("</" + qName + ">");

    }

 

    publicstaticvoid main(String[] args) {

       SAXParserFactory spf = SAXParserFactory.newInstance();

       SAXParser sp = null;

       try {

           sp = spf.newSAXParser();

           File file = new File("students.xml");

           sp.parse(file, new SAXPrinter());

       } catch (ParserConfigurationException e) {

           e.printStackTrace();

       } catch (SAXException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       }

    }

}

使用ErrorHandler处理解析中的错误

DefaultHandler既现实了ContentHandler接口,又实现了ErrorHandler接口。

 

publicclass ErrorProcessor extends DefaultHandler {

    publicvoid warning(SAXParseException ex) throws SAXException {

       System.err.println("[Warning] " + getLocationString(ex) + ": "

              + ex.getMessage());

    }

 

    publicvoid error(SAXParseException ex) throws SAXException {

       System.err.println("[Error] " + getLocationString(ex) + ": "

              + ex.getMessage());

    }

 

    publicvoid fatalError(SAXParseException ex) throws SAXException {

       System.err.println("[Fatal Error] " + getLocationString(ex) + ": "

              + ex.getMessage());

    }

 

    /**

     * 获取导致错误或者警告的文本结束位置的行号和列号。

     * 如果是实体引发错误,还获取它的公共标识符和系统标识符。

     */

    private String getLocationString(SAXParseException ex) {

       StringBuffer str = new StringBuffer();

 

       String publicId = ex.getPublicId();

       if (publicId != null) {

           str.append(publicId);

           str.append(" ");

       }

 

       String systemId = ex.getSystemId();

       if (systemId != null) {

           str.append(systemId);

           str.append(':');

       }

 

       str.append(ex.getLineNumber());

       str.append(':');

       str.append(ex.getColumnNumber());

 

       return str.toString();

    }

 

    /**

     * 输出元素的结束标签,以便于查看不同类型的错误对文档解析的影响。

     */

    publicvoid endElement(String uri, String localName, String qName)

           throws SAXException {

       System.out.println("</" + qName + ">");

    }

 

    publicstaticvoid main(String[] args) {

       try {

           /*

            *  利用XMLReaderFactory工厂类,创建XMLReader对象。

            *  注,这里没有使用JAXP中定义的SAX解析器工厂类和解析器类,

            *  而是使用了SAX本身定义的XMLReaderFactory工厂类与XMLRader

            *  解析器。当然最好是使用JAXP中的工厂类。

            */

           XMLReader xmlReader = XMLReaderFactory.createXMLReader();

           // 打开解析器的验证功能。

           xmlReader

                  .setFeature("http://xml.org/sax/features/validation", true);

 

           ErrorProcessor ep = new ErrorProcessor();

           xmlReader.setErrorHandler(ep);

           xmlReader.setContentHandler(ep);

 

           // InputSource类位于org.xml.sax包中,表示XML实体的输入源。

           // 它可以用java.io.InputStream对象来构造,更多信息请参看JDKAPI文档

           InputSource is = new InputSource(

                  new FileInputStream("students.xml"));

           xmlReader.parse(is);

       } catch (SAXException e) {

           System.out.println(e.toString());

       } catch (IOException e) {

           System.out.println(e.toString());

       }

    }

}

如果被解析的XML不符合对应的模式文档(DTDSchema)或不存在模式文档时,都会报错

posted @ 2015-02-13 21:36  江正军  阅读(1631)  评论(0编辑  收藏  举报