解析 XML
文档
解析 XML 文档有多种方式(请参阅本系列的第 1 部分,那篇教程讨论了体系结构),不过 SAX
解析器和 DOM 解析器是最基本的。第 1 部分从较高层次上比较了这两种方法(请参阅 参考资料)。
XML 实例文档 本教程使用一个
DVD 商店的库存目录作为示例文档。从概念上说,目录是 DVD 以及关于每个 DVD 的信息的集合。实际的文档很短,只有四张 DVD,但是对于学习
XML 处理(包括验证)而言已经足够复杂了。清单 1 显示了该文件。
清单 1. DVD 目录的 XML 实例文档
<?xml version="1.0"?>
<!DOCTYPE catalog SYSTEM "dvd.dtd"> <!--
DVD inventory --> <catalog> <dvd code="_1234567">
<title>Terminator 2</title> <description>
A shape-shifting cyborg is sent back from the future
to kill the leader of the resistance. </description>
<price>19.95</price> <year>1991</year>
</dvd> <dvd code="_7654321"> <title>The
Matrix</title> <price>12.95</price>
<year>1999</year> </dvd> <dvd code="_2255577"
genre="Drama"> <title>Life as a House</title>
<description>
When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son. </description>
<price>15.95</price> <year>2001</year>
</dvd> <dvd code="_7755522" genre="Action">
<title>Raiders of the Lost Ark</title> <price>14.95</price>
<year>1981</year> </dvd> </catalog> |
使用 SAX 解析器
本系列教程的第 1 部分已经讨论过,SAX 解析器是一种基于事件的解析器。这意味着,解析器在解析文档的时候向回调方法发送事件(如
图 1 所示)。为了简化起见,图 1 中没有显示发生的所有事件。
图 1. SAX 解析器事件
随着解析器读入文档内容,这些事件被实时地推给应用程序。这种处理模型的好处是可以用相对较少的内存处理很大的文档。缺点是处理所有这些事件要做更多的工作。
org.xml.sax 包提供了一组接口。其中之一提供了解析器的 XMLReader 接口。可以这样建立解析:
try {
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.parse( "myDocument.xml" ); //complete path
} catch ( SAXParseException e ) {
//document is not well-formed
} catch ( SAXException e ) {
//could not find an implementation of XMLReader
} catch ( IOException e ) {
//problem reading document file
} |
技巧:重用解析器实例是可能的。创建解析器需要大量资源。如果运行多个线程,可以从资源池中重用解析器实例。
这样当然好,但是应用程序如何从解析器得到事件呢?很高兴您能问这个问题。
处理 SAX 事件
为了从解析器接收事件,需要实现 ContentHandler 接口。可以实现该接口的很多方法以便处理文档。或者,如果只需要处理一两个回调,也可以实现
DefaultHandler 的子类,该类实现了所有 ContentHandler 方法(什么也不做),然后覆盖需要的方法。
无论哪种方式,都要编写逻辑,从而在收到 startElement、characters、endDocument
和 SAX 解析器激活的其他回调方法时进行所需要的处理。XML in a Nutshell, Third Edition(请参阅
参考资料)的第 351-355 页列出了文档中可能出现的所有方法调用。
这些回调事件是文档解析过程中的正常 事件。还可以实现 ErrorHandler 来处理有效性
回调。介绍有效性之后我再讨论这个话题,暂时先放一放。
要进一步了解 SAX 解析,请阅读 XML in a Nutshell, Third Edition
的第 20 章或者 “Serial Access with the Simple API for XML (SAX)”(请参阅
参考资料)。
SAX 解析器异常处理
在默认情况下,SAX 解析器忽略错误。为了应付无效的或者非结构良好的文档,必须实现 ErrorHandler(要注意,DefaultHandler
同时实现了该接口和 ContentHandler 接口)并定义 error() 方法:
public class SAXEcho extends
DefaultHandler {
...
//Handle validity errors
public void error( SAXParseException e ) {
echo( e.getMessage() );
echo( "Line " + e.getLineNumber() + " Column
" + e.getColumnNumber();
} |
然后必须打开验证特性:
parser.setFeature( "http://xml.org/sax/features/validation",
true ); |
最后调用下列代码:
parser.setErrorHandler(
saxEcho ); |
要记住,parser 是 XMLReader 的一个实例。如果文档违反了模式(DTD 或 XML
Schema)规则,解析器就会调用 error() 方法。
回显 SAX 事件 作为上面所学
SAX 解析器应用技巧的一个练习,使用 清单 2 中的 SAXEcho.java 代码输出 catalog.xml 文件的解析器事件。
清单 2. 回显 SAX 事件
package
com.xml.tutorial;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* A handler for SAX parser events that outputs certain event
* information to standard output.
*
* @author mlorenz
*/
public class SAXEcho extends DefaultHandler {
public static final String XML_DOCUMENT_DTD = "catalogDTD.xml";
//validates via catalog.dtd
public static final String XML_DOCUMENT_XSD = "catalogXSD.xml";
//validates via catalog.xsd
public static final String NEW_LINE = System.getProperty("line.separator");
protected static Writer writer;
/**
* Constructor
*/
public SAXEcho() {
super();
}
/**
* @param args
*/
public static void main(String[] args) {
//-- Set up my instance to handle SAX events
DefaultHandler eventHandler = new SAXEcho();
//-- Echo to standard output
writer = new OutputStreamWriter( System.out );
try {
//-- Create a SAX parser
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler( eventHandler );
parser.setErrorHandler( eventHandler );
parser.setFeature(
"http://xml.org/sax/features/validation", true );
//-- Validation via DTD --
echo( "=== Parsing " + XML_DOCUMENT_DTD + "
===" + NEW_LINE );
//-- Parse my XML document, reporting DTD-related errors
parser.parse( XML_DOCUMENT_DTD );
//-- Validation via XSD --
parser.setFeature(
"http://apache.org/xml/features/validation/schema",
true );
echo( NEW_LINE + NEW_LINE + "=== Parsing " +
XML_DOCUMENT_XSD + " ===" + NEW_LINE );
//-- Parse my XML document, reporting XSD-related errors
parser.parse( XML_DOCUMENT_XSD );
} catch (SAXException e) {
System.out.println( "Parsing Exception occurred"
);
e.printStackTrace();
} catch (IOException e) {
System.out.println( "Could not read the file" );
e.printStackTrace();
}
System.exit(0);
}
//--Implement SAX callback events of interest (default is
do nothing) --
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String,
* java.lang.String, java.lang.String, org.xml.sax.Attributes)
* @see org.xml.sax.ContentHandler interface
* Element and its attributes
*/
@Override
public void startElement( String uri,
String localName,
String qName,
Attributes attributes)
throws SAXException {
if( localName.length() == 0 )
echo( "<" + qName );
else
echo( "<" + localName );
if( attributes != null ) {
for( int i=0; i < attributes.getLength(); i++ ) {
if( attributes.getLocalName(i).length() == 0 ) {
echo( " " + attributes.getQName(i) +
"=\"" + attributes.getValue(i) + "\""
);
}
}
}
echo( ">" );
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String,
* java.lang.String, java.lang.String)
* End tag
*/
@Override
public void endElement(String uri, String localName, String
qName)
throws SAXException {
echo( "</" + qName + ">" );
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#characters(char[],
int, int)
* Character data inside an element
*/
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
String s = new String(ch, start, length);
echo(s);
}
//-- Add additional event echoing at your discretion --
/**
* Output aString to standard output
* @param aString
*/
protected static void echo( String aString ) {
try {
writer.write( aString );
writer.flush();
} catch (IOException e) {
System.out.println( "I/O error during echo()" );
e.printStackTrace();
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
* @see org.xml.sax.ErrorHandler interface
*/
@Override
public void error(SAXParseException e) throws SAXException
{
echo( NEW_LINE + "*** Failed validation ***" + NEW_LINE
);
super.error(e);
echo( "* " + e.getMessage() + NEW_LINE +
"* Line " + e.getLineNumber() +
" Column " + e.getColumnNumber() + NEW_LINE +
"*************************" + NEW_LINE );
try {
Thread.sleep( 10 );
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
} |
可以利用 SAXEcho.java 中的代码观察 SAX 解析是如何进行的。要注意,这段代码没有处理所有的事件,因此并非原始文档中的所有事件都回显出来(请参阅
清单 3)。观察 ContentHandler 接口了解可能遇到的其他消息。
清单 3. 执行 SAXEcho 的输出
=== Parsing
catalogDTD.xml ===
<catalog><dvd><title>Terminator 2</title><description>
A shape-shifting cyborg is sent back from the future to kill
the leader of the resistance.
</description><price>19.95</price><year>1991</year>
</dvd><dvd><title>The Matrix</title><price>10.95</price>
<year>1999</year></dvd><dvd><title>Life
as a House</title><description>
When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son.
</description><price>15.95</price><year>2001</year>
</dvd><dvd><title>Raiders of the Lost Ark</title><price>
14.95</price><year>1981</year></dvd></catalog>
=== Parsing catalogXSD.xml ===
<catalog>
<dvd>
<title>Terminator 2</title>
<description>
A shape-shifting cyborg is sent back from the future to kill
the leader of the resistance.
</description>
<price>19.95</price>
<year>1991</year>
</dvd>
<dvd>
<title>The Matrix</title>
<price>10.95</price>
<year>1999</year>
</dvd>
<dvd>
<title>Life as a House</title>
<description>
When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son.
</description>
<price>15.95</price>
<year>2001</year>
</dvd>
<dvd>
<title>Raiders of the Lost Ark</title>
<price>14.95</price>
<year>1981</year>
</dvd>
</catalog> |
使用 DOM 解析器
和 SAX 解析器不同,DOM 解析器根据 XML 文档内容创建树结构(如 图 2 所示)。为了简化起见,部分解析动作没有显示出来。
图 2. DOM 解析树
DOM 没有为 XML 解析器指定接口,因此不同的厂商有不同的解析器类。我将继续使用 Xerces
解析器,它包含一个 DOMParser 类。
可以像下面这样建立 DOM 解析器:
DOMParser parser = new DOMParser();
try {
parser.parse( "myDocument.xml" );
Document document = parser.getDocument();
} catch (DOMException e) {
// take validity action here
} catch (SAXException e) {
// well-formedness action here
} catch (IOException e) {
// take I/O action here
} |
遍历 DOM 树
由于要构造整个文档树,DOM 需要更多的时间和内存。这些开销的好处是可以利用树结构通过多种方式遍历和操纵文档的内容。图
3 显示了 DVD catalog 文档的一部分。
图 3. 遍历 DOM 树
树有一个根,可以通过 Document.getDocumentElement() 方法来访问它。从任何
Node 出发,都可使用 Node.getChildNodes() 获得当前 Node 的所有孩子的 NodeList。要注意,属性不
被看作包含它的 Node 的孩子。可以创建新的 Node、追加、插入、按名查找和删除节点。这些还仅仅是所有功能的一小部分。
一个更强大的方法是 Document.getElementsByTagName(),它返回一个
NodeList,包含后代元素中与 Node 匹配的所有孩子。DOM 既可在客户机上使用,也可在服务器上使用。
客户机遍历
可以在客户机上遍历 DOM 树,而且可以在浏览器中通过 JavaScript 验证 XHTML
页面上的动作。比如,客户机可能需要确定是否存在特定名称的 Node:
//-- make sure a new DVD's
title is unique
var titles = document.getElementsByTagName("title");
var newTitleValue = newTitle.getNodeValue();
var nextTitle;
for( i=0; i < titles.getLength(); i++ ) {
nextTitle = titles.item(i); //NodeList access by index
if( nextTitle.getNodeValue().equals( newTitleValue ) {
//take some action
}
} |
服务器遍历
在服务器上肯定会需要操纵树,比如在一个 Node 中增加新的孩子:
//-- add a new DVD with
aName and description
public void createNewDvd( String aName, String description )
{
Element catalog = document.getDocumentElement(); //root
Element newDvd = document.createElement( aName );
Element dvdDescription =
document.createTextNode( description );
newDvd.appendChild( dvdDescription );
catalog.appendChild( newDvd ); //as last element
} |
忠告: 一定要使用 DOM 接口,如 NodeList 或 NamedNodeMap 来操纵树。DOM
树是动态的,就是说随着所做的修改立刻更新,因此如果使用本地变量,缓冲的数据有可能是错误的。比如,在调用 removeChild()
之后, Node.getLength() 将返回不同的值。
DOM 解析器异常处理
如果在解析过程中遇到问题,DOM 解析器将抛出 DOMException。这是一种 RuntimeException,虽然有些语言不支持检查异常,但在
Java 代码中应该始终捕获并抛出异常。 为了确定操作中出现的问题,应该使用
DOMException 的 code。这些 code 说明什么地方出现了问题,比如尝试的修改使文档变得无效(DOMException.INVALID_MODIFICATION_ERR)或者找不到目标
Node(DOMException.NOT_FOUND_ERR)。Processing XML with Java: A Guide
to SAX, DOM, JDOM, JAXP, and TrAX 第 9 章的 DOMException 一节提供了完整的 DOMException
code 列表,包括有关说明(请参阅 参考资料)。
回显 DOM 树
作为上面所学 DOM 解析器使用技巧的一个练习,可以使用 清单 4 中的 DOMEcho.java
代码输出 catalog.xml 文件 DOM 树的内容。这段代码回显树的信息,然后修改并回显更新后的树。
清单 4. 回显 DOM 树
package com.xml.tutorial;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;
import org.xml.sax.SAXException;
import com.sun.org.apache.xerces.internal.parsers.DOMParser;
/**
* A handler to output certain information about a DOM tree
* to standard output.
*
* @author lorenzm
*/
public class DOMEcho {
public static final String XML_DOCUMENT_DTD =
"catalogDTD.xml"; //validates via catalog.dtd
public static final String NEW_LINE = System.getProperty("line.separator");
protected static Writer writer;
// Types of DOM nodes, indexed by nodeType value (e.g. Attr
= 2)
protected static final String[] nodeTypeNames = {
"none", //0
"Element", //1
"Attr", //2
"Text", //3
"CDATA", //4
"EntityRef", //5
"Entity", //6
"ProcInstr", //7
"Comment", //8
"Document", //9
"DocType", //10
"DocFragment", //11
"Notation", //12
};
//-- DOMImplementation features (we only need one for now)
protected static final String TRAVERSAL_FEATURE = "Traversal";
//-- DOM versions (we're using DOM2)
protected static final String DOM_2 = "2.0";
/**
* Constructor
*/
public DOMEcho() {
super();
}
/**
* @param args
*/
public static void main(String[] args) {
//Echo to standard output
writer = new OutputStreamWriter( System.out );
//use the Xerces parser
try {
DOMParser parser = new DOMParser();
parser.setFeature( "http://xml.org/sax/features/validation",
true );
parser.parse( XML_DOCUMENT_DTD ); //use DTD grammar for validation
Document document = parser.getDocument();
echoAll( document );
//-- add description for Indiana Jones movie
//---- find parent Node
Element indianaJones = document.getElementById("_7755522");
//---- insert a description before the price
// (anywhere else would be invalid)
NodeList prices = indianaJones.getElementsByTagName("price");
Node desc = document.createElement("description");
desc.setTextContent(
"Indiana Jones is hired to find the Ark of the Covenant");
indianaJones.insertBefore( desc, prices.item(0) );
//-- now, echo the document again to see the change
echoAll( document );
} catch (DOMException e) { //handle invalid manipulations
short code = e.code;
if( code == DOMException.INVALID_MODIFICATION_ERR ) {
//take action when invalid manipulation attempted
} else if( code == DOMException.NOT_FOUND_ERR ) {
//take action when element or attribute not found
} //add more checks here as desired
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Echo all the Nodes, in preorder traversal order, for aDocument
* @param aDocument
*/
protected static void echoAll(Document aDocument) {
if( aDocument.getImplementation().hasFeature(
TRAVERSAL_FEATURE,DOM_2) ) {
echo( "=== Echoing " + XML_DOCUMENT_DTD + "
===" + NEW_LINE );
Node root = (Node) aDocument.getDocumentElement();
int whatToShow = NodeFilter.SHOW_ALL;
NodeFilter filter = null;
boolean expandRefs = false;
//-- depth first, preorder traversal
DocumentTraversal traversal = (DocumentTraversal)aDocument;
TreeWalker walker = traversal.createTreeWalker(
(org.w3c.dom.Node) root, //where to start
//(cannot go "above" the root)
whatToShow, //what to include
filter, //what to exclude
expandRefs); //include referenced entities or not
for( Node nextNode = (Node) walker.nextNode(); nextNode !=
null;
nextNode = (Node) walker.nextNode() ) {
echoNode( nextNode );
}
} else {
echo( NEW_LINE + "*** " + TRAVERSAL_FEATURE +
" feature is not supported" + NEW_LINE );
}
}
/**
* Output aNode's name, type, and value to standard output.
* @param aNode
*/
protected static void echoNode( Node aNode ) {
String type = nodeTypeNames[aNode.getNodeType()];
String name = aNode.getNodeName();
StringBuffer echoBuf = new StringBuffer();
echoBuf.append(type);
if( !name.startsWith("#") ) { //do not output duplicate
names
echoBuf.append(": ");
echoBuf.append(name);
}
if( aNode.getNodeValue() != null ) {
if( echoBuf.indexOf("ProcInst") == 0 )
echoBuf.append( ", " );
else
echoBuf.append( ": " ); //output only to first newline
String trimmedValue = aNode.getNodeValue().trim();
int nlIndex = trimmedValue.indexOf("\n");
if( nlIndex >= 0 ) //found newline
trimmedValue = trimmedValue.substring(0,nlIndex);
echoBuf.append(trimmedValue);
}
echo( echoBuf.toString() + NEW_LINE );
echoAttributes( aNode );
}
/**
* Output aNode's attributes to standard output.
* @param aNode
*/
protected static void echoAttributes(Node aNode) {
NamedNodeMap attr = aNode.getAttributes();
if( attr != null ) {
StringBuffer attrBuf = new StringBuffer();
for( int i = 0; i < attr.getLength(); i++ ) {
String type = nodeTypeNames[attr.item(i).getNodeType()];
attrBuf.append(type);
attrBuf.append( ": " + attr.item(i).getNodeName()
+ "=" );
attrBuf.append( "\"" + attr.item(i).getNodeValue()
+ "\"" +
NEW_LINE );
}
echo( attrBuf.toString() );
}
}
/**
* Output aString to standard output
* @param aString
*/
protected static void echo( String aString ) {
try {
writer.write( aString );
writer.flush();
} catch (IOException e) {
System.out.println( "I/O error during echo()" );
e.printStackTrace();
}
}
}
|
看看其中的部分逻辑:
protected static final String[]
nodeTypeNames = {
...
}; |
该数组将 Node.getNodeType() int 值映射到可能遇到的每种 Node 类型:
if( aDocument.getImplementation().hasFeature(
TRAVERSAL_FEATURE,DOM_2) ) { |
DOMEcho 利用了 DOM2(参见 DOM 1 与 DOM 2)的 TreeWalker
接口。为了安全起见,一定要检查您的解析器是否支持该特性。阅读 Processing XML with Java: A Guide
to SAX, DOM, JDOM, JAXP, and TrAX(请参阅 参考资料)的第 9 章可以了解所有这些特性。
简而言之,DOMEcho 有一个 echoAll(Document aDoc) 方法,该方法使用不带筛选的
TreeWalker 按照先序遍历顺序访问 Node(请参阅 DOM 1 与 DOM 2)。然后对每个节点调用 echoNode(Node
aNode)。接着 echoNode 对它的 Node 调用 echoAttributes(Node aNode):
//---- find parent Node
Element indianaJones = document.getElementById("_7755522");
//---- insert a description before the price
// (anywhere else would be invalid)
NodeList prices = indianaJones.getElementsByTagName("price");
Node desc = document.createElement("description");
desc.setTextContent( "Indiana Jones is hired to find
the Ark of the Covenant");
indianaJones.insertBefore( desc, prices.item(0) ); |
就是这部分代码修改了 DOM 树。它在适当的位置添加了描述来保证树按照文档模式仍然是有效的。
清单 5 显示了 DOMEcho 的输出结果。
清单 5. DOMEcho 的输出
=== Echoing catalogDTD.xml
===
Text:
Comment: DVD inventory
Text:
Element: dvd
Attr: code="_1234567"
Text:
Element: title
Text: Terminator 2
Text:
Element: description
Text: A shape-shifting cyborg is sent back from the future
to kill the leader of the resistance.
Text:
Element: price
Text: 19.95
Text:
Element: year
Text: 1991
Text:
Text:
Element: dvd
Attr: code="_7654321"
Text:
Element: title
Text: The Matrix
Text:
Element: price
Text: 10.95
Text:
Element: year
Text: 1999
Text:
Text:
Element: dvd
Attr: code="_2255577"
Attr: genre="Drama"
Text:
Element: title
Text: Life as a House
Text:
Element: description
Text: When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son.
Text:
Element: price
Text: 15.95
Text:
Element: year
Text: 2001
Text:
Text:
Element: dvd
Attr: code="_7755522"
Attr: genre="Action"
Text:
Element: title
Text: Raiders of the Lost Ark
Text:
Element: price
Text: 14.95
Text:
Element: year
Text: 1981
Text:
Text:
=== Echoing catalogDTD.xml ===
Text:
Comment: DVD inventory
Text:
Element: dvd
Attr: code="_1234567"
Text:
Element: title
Text: Terminator 2
Text:
Element: description
Text: A shape-shifting cyborg is sent back from the future
to kill the leader of the resistance.
Text:
Element: price
Text: 19.95
Text:
Element: year
Text: 1991
Text:
Text:
Element: dvd
Attr: code="_7654321"
Text:
Element: title
Text: The Matrix
Text:
Element: price
Text: 10.95
Text:
Element: year
Text: 1999
Text:
Text:
Element: dvd
Attr: code="_2255577"
Attr: genre="Drama"
Text:
Element: title
Text: Life as a House
Text:
Element: description
Text: When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son.
Text:
Element: price
Text: 15.95
Text:
Element: year
Text: 2001
Text:
Text:
Element: dvd
Attr: code="_7755522"
Attr: genre="Action"
Text:
Element: title
Text: Raiders of the Lost Ark
Text:
Element: description
Text: Indiana Jones is hired to find the Ark of the Covenant
Element: price
Text: 14.95
Text:
Element: year
Text: 1981
Text:
Text: |
空白
您将注意到 DOMEcho 输出(清单 6)中有大量 Text Node,其中很多看起来没有内容。为什么会这样呢?
解析器报告文档元素内容中出现的空白(多余的空白、制表符和回车换行)。
需要注意的是没有 报告的东西:元素中的空白,比如属性周围的空格。这里没有显示,但是也不会报告的是序言中的空白。要注意,description
有 一个 Text Element,但是空白被规范化 从而去掉了非空白内容之前和之后的多余字符。
由 Element 内容中的空白造成的 Text 元素称为不可忽略的 空白。不可忽略的空白是验证的一部分,如
图 4 所示。
图 4. 空白处理
验证 XML 文档
验证包括使用文法保证 XML 文档具有正确的结构和内容。可以使用 XML 模式来指定文法,形式包括
DTD 或 XML Schema 文件(请参阅 模式)。教程的这一节讨论 DTD 和 XML Schema 文件。
使用 DTD 验证 DTD
定义了应用于 XML 实例文档上的约束。这些约束与结构良好性无关。事实上,非结构良好的文档根本不被看作是 XML 文档。约束与关于内容的业务规则有关,为了保证应用程序能够使用文档,文档必须满足这些规则。
DTD 规定了为了保证有效 XML 实例文档必须包含的元素和属性。可以通过在文档开始部分包含
DOCTYPE 语句把文档和 DTD 联系起来:
<!DOCTYPE catalog SYSTEM
"catalog.dtd"> |
现在来看看 catalog.dtd 文件。为了验证该文档,需要启用验证并使用验证解析器。下面的代码启用
SAX 解析器的验证特性:
saxParser.setFeature(
"http://xml.org/sax/features/validation", true ); |
下面的代码启用 DOM 解析器的验证特性:
domParser.setFeature(
"http://xml.org/dom/features/validation", true ); |
图 5 显示了 catalog.dtd 文件。
图 5. Catalog DTD
我们逐行地来分析这个 DTD,看看规定了什么:
<!ELEMENT catalog (dvd+)> |
dvd+ 指定 <catalog> 元素包含一个或多个 <dvd>。这样做是合理的,否则就不可能卖掉多少
DVD!
<!ELEMENT dvd (title, description?, price, year)>
title, ..., year 称为序列。就是说这些命名的元素必须按照这种 顺序作为 <dvd> 元素的孩子出现。description
后面的问号表明 <dvd> 可以有零个或一个描述元素 —— 换句话说,它是可选的,但如果指定的话也只能有一个(星号表示零个或多个,加号表示
1 个或多个)。
<!ATTLIST dvd code ID #REQUIRED>
ID 类型的属性在文档中的名称必须是惟一的。您将看到 catalog.xml 文件中的 ID
都是以下划线开始的。XML 名称不能以数字开始,但是下划线(以及字母或者其他非数字字符)可以。一个元素只能有一个 ID 类型的属性。您可能已经猜到了,REQUIRED
意味着 <dvd> 必须 有一个 code。
<!ATTLIST dvd genre ( Drama | Comedy | SciFi
| Action | Romance ) #IMPLIED>
这是一个枚举。由于指定为 IMPLIED,因而是可选的。但是,如果在文档中出现 了,则必须是枚举值中的一个(读作
“Drama 或 Comedy 或……”)。
<!ELEMENT title (#PCDATA)>
<!ELEMENT description (#PCDATA)> <!ELEMENT price
(#PCDATA)> <!ELEMENT year (#PCDATA)> |
剩下的行都用于指定可解析字符数据。这些元素都不能有孩子。
现在尝试修改实例文档以便保证这些规则能够正常工作。首先增加一个 <description>,但是将其放在
<dvd> 的最后。如您所料,结果得到了一条错误消息(如图 6 所示)。
图 6. Description 错误
现在增加一个 genre(如 图 7 所示)。
图 7. Genre 错误
为什么不行呢?列表中有科幻小说啊!不过,您知道,XML 是大小写敏感的,因此 "scifi"
是无效的,而必须是 "SciFi"。
现在看看 ID 是否必须是惟一的。将 code 复制到另一个 <dvd> 中(如
图 8 所示)。
图 8. ID 错误
毫无疑问都看到了适当的错误。您应该清楚了。可使用这里的 DTD 和 XML 文件尝试其他修改(源文件请参阅
下载 部分)。
DTD 异常处理
为了处理 DTD 操作错误必须启用验证。对于 Xerces,只要将模式验证特性设置为 true:
parser.setFeature(
"http://apache.org/xml/features/validation/schema",
true ); |
通过 Apache Software Foundation 网站(请参阅 参考资料)可以了解
Xerces 解析器的各种特性。关于 DTD 验证的更多信息,请参阅 XML in a Nutshell, Third Edition(参见
参考资料)的第 3 章。
用 SAXEcho 验证
现在来看看验证。注释掉 XML 文档中 Life as a House dvd 的 price,再看看结果如何,分别使用
DTD 和 XSD 文件进行验证。清单 6 显示了输出结果。
清单那 6. 执行 SAXEcho 的输出
=== Parsing
catalogDTD.xml ===
<catalog><dvd><title>Terminator 2</title><description>
A shape-shifting cyborg is sent back from the future to kill
the leader of the resistance.
</description><price>19.95</price><year>1991</year>
</dvd><dvd><title>The Matrix</title><price>10.95
</price><year>1999</year></dvd><dvd><title>Life
as a House</title><description>
When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son.
</description><year>2001</year>
*** Failed validation ***
* The content of element type "dvd" must match "(title,description?,price,year)".
*************************
</dvd><dvd><title>Raiders of the Lost Ark</title><price>14.95</price><year>1981</year></dvd></catalog>
=== Parsing catalogXSD.xml ===
<catalog>
<dvd>
<title>Terminator 2</title>
<description>
A shape-shifting cyborg is sent back from the future to kill
the leader of the resistance.
</description>
<price>19.95</price>
<year>1991</year>
</dvd>
<dvd>
<title>The Matrix</title>
<price>10.95</price>
<year>1999</year>
</dvd>
<dvd>
<title>Life as a House</title>
<description>
When a man is diagnosed with terminal cancer,
he takes custody of his misanthropic teenage son.
</description>
*** Failed validation ***
* cvc-complex-type.2.4.a: Invalid content was found starting
with element 'year'. One of '{"":price}' is expected.
*************************
<year>2001</year>
</dvd>
<dvd>
<title>Raiders of the Lost Ark</title>
<price>14.95</price>
<year>1981</year>
</dvd>
</catalog>
|
使用 XML 模式验证
您也许会奇怪,既然可以用 DTD 保证文档结构和内容的有效性,为何需要验证文档的其他 方式呢?下面给出几个理由:
- 对元素和属性值的控制粒度:XML Schema 允许指定格式、长度和数据类型。
- 复杂数据类型:XML Schema 支持从已有类型创建新的数据类型和规范。
- 元素频次:使用 XML Schema,可以在更细的粒度上控制元素。
- 名称空间:XML Schema 使用名称空间,名称空间对于和其他组织打交道的组织来说越来越重要。
- XML Schema 语言比 DTD 语言更强大,因此也更复杂。好的方面是 XML Schemas
用 XML 编写,而 DTD 不是。
我们来验证一下 DTD 验证所用的同一个 XML 实例文档(如 清单 1 所示)。清单 7 显示了
XML Schema:
清单 7. Catalog XML Schema
<?xml
version="1.0" encoding="UTF-8"?>
<xs:schema elementFormDefault="qualified" xml:lang="EN"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- Our DVD catalog contains four or more DVDs -->
<xs:element name="catalog">
<xs:complexType>
<xs:sequence minOccurs="4" maxOccurs="unbounded">
<xs:element ref="dvd"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- DVDs have a title, an optional description, a price,
and a release year -->
<xs:element name="dvd">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="xs:string"/>
<xs:element name="description" type="descriptionString"
minOccurs="0"/>
<xs:element name="price" type="priceValue"/>
<xs:element name="year" type="yearString"/>
</xs:sequence>
<xs:attribute name="code" type="xs:ID"/>
<!-- requires a unique ID -->
<xs:attribute name="genre"> <!-- default
= optional -->
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="Drama"/>
<xs:enumeration value="Comedy"/>
<xs:enumeration value="SciFi"/>
<xs:enumeration value="Action"/>
<xs:enumeration value="Romance"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<!-- Descriptions must be between 10 and 120 characters
long -->
<xs:simpleType name="descriptionString">
<xs:restriction base="xs:string">
<xs:minLength value="10"/>
<xs:maxLength value="120"/>
</xs:restriction>
</xs:simpleType>
<!-- Price must be < 100.00 -->
<xs:simpleType name="priceValue">
<xs:restriction base="xs:decimal">
<xs:totalDigits value="4"/>
<xs:fractionDigits value="2"/>
<xs:maxExclusive value="100.00"/>
</xs:restriction>
</xs:simpleType>
<!-- Year must be 4 digits, between
1900 and 2099 -->
<xs:simpleType name="yearString">
<xs:restriction base="xs:string">
<xs:pattern value="(19|20)\d\d"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
|
要注意,XML Schema 与对应的 DTD 相比要长得多。事实上,即便去掉注释和空白,这个模式仍然超过了
50 行,而 DTD 模式只有九行。(当然,这个模式检查的细节要比 DTD 多。)因此,控制的粒度越精细,代码也越复杂,而且复杂得多。这意味着,如果验证不需要
XML Schema,则使用 DTD。
除了和前面 DTD 中所用的可比较的约束以外,看看 XML Schemas 增加了什么以及为
DVD catalog 文档带来了哪些好处:
对元素和属性值控制的粒度大小:和允许任何字符值的 DTD 不同,XSD 约束了 descriptions(20
到 120 个字符)、prices(0.00 到 100.00)和 years(1900 到 2999)的值。
复杂数据类型: 创建了将来可重用、可扩展的新数据类型:dvd、descriptionString、priceValue 和 yearString。
元素出现频次:因为本教程使用的例子文档很小,我把 DVD 的数量设置为四个或更多以便使该文档有效。实际上,最小值可能是一个很大的数,不过通过这个例子可以看到这种约束是可能的。
名称空间:仅对 XML Schema 类型使用了名称空间,但是由于 XML Schemas 是名称空间感知的,因而可以增加更多名称空间来控制名称冲突。
我们更详细地讨论一下 XML Schema 以便理解其内容:
xs:complexType
和 xs:simpleType。complexType 元素可以包含其他元素或属性:
<xs:element name="dvd">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="xs:string"/>
...
|
simpleType 元素只能包含文本和它自己的属性值:
<xs:simpleType name="yearString">
<xs:restriction base="xs:string"> <xs:pattern
value="(19|20)\d\d"/> </xs:restriction>
</xs:simpleType> |
这个具体的例子中定义了一种新类型 yearString,它必须包含四位数字,并且最高两位是
“19” 或 “20”。使用 xs:restriction 元素从已有的(基)类型派生新的、受限制的类型。使用 xs:pattern
刻面元素来比较值,检查是否和指定的表达式匹配(请参阅 刻面)。
xs:sequence. 孩子元素必须按照所列顺序出现(虽然 minOccurs 可以把元素变成可选的,如前所述):
<xs:sequence>
<xs:element name="title" type="xs:string"/>
<xs:element name="description" type="descriptionString"
minOccurs="0"/> <xs:element name="price"
type="priceValue"/> <xs:element name="year"
type="yearString"/> </xs:sequence> |
sequence 声明,有效文档中的 dvd 必须有一个 title,后面可以跟 10 到
120 个字符长的 description,后跟小于 US$100 形如 “nn.nn” 的 price,最后是 year。
注意:XML Schemas 验证需要 XMLBuddy Pro。
现在做一些修改看看约束是否生效了。 为 Adventure 增加 genre,输入长度超过 120
字符的 description、设置重复的 dvd code(如 图 9 所示)。
图 9. XSD 错误
可以发现 genre、惟一的 ID 和 description 长度都是强制性的。
XML Schema 还能做更多。下面强调几点:
- xs:choice:必须出现其中的一个孩子。
- xs:all:列出的每个孩子都必须出现一次,但是对顺序没有要求。
- xs:group:可以定义和引用一组元素的组名(通过 ref=groupName)。
- xs:attributeGroup:和用于元素的 xs:group 一样,这个相对应的指示符用于属性。
- xs:date:这是 ISO 8601 定义的格里高利历法日期,格式为 YYYY-MM-DD。
- xs:time:hh:mm:ss 形式的时间,用 "Z" 表示 UTC
相对时间。
- xs:duration:一定数量的年、月、日、时、分。
可以看到,编写 XML Schema 时可以利用很多内建的强大功能。如果找不到需要的类型,可以创建新的类型。
数据类型
XML Schema 的一个强大特性是能够创建新的数据类型。在 catalog.xsd 文件中可以看到使用了大量新建数据类型,包括
yearString 和 priceValue 类型。在该例中,这些类型只在 dvd 类型中使用,但是可以文档中出现 year
或 price 的任何地方使用。
这些类型扩展原有的小数和字符串类型:
<!--
Price must be < 100.00 -->
<xs:simpleType name="priceValue">
<xs:restriction base="xs:decimal">
<xs:totalDigits value="4"/>
<xs:fractionDigits value="2"/>
<xs:maxExclusive value="100.00"/>
</xs:restriction>
</xs:simpleType>
<!-- Year must be 4 digits, between
1900 and 2099 -->
<xs:simpleType name="yearString">
<xs:restriction base="xs:string">
<xs:pattern value="(19|20)\d\d"/>
</xs:restriction>
</xs:simpleType>
|
如前所述,可以结合使用 restriction 元素和一种或更多刻面来实现已有类型的特化。如果有多个刻面,可结合起来确定哪些值有效,哪些值无效。
范式匹配
pattern 刻面元素支持一种类似 Perl 的丰富的表达式语法。前面在 yearString
中用到了它,范式 “ (19|20)\d\d” 应读作 “字符串必须以 19 或 20 开始并且后跟两位数字”。表 1 列出了几个范式。
表 1. XML Schema 范式匹配表达式
范式 |
匹配 |
(A|B) |
匹配 A 或 B 的字符串 |
A? 和 A |
匹配的字符串出现零次或一次 |
A* 和 A |
匹配的字符串出现零次或多次 |
A+ 和 A |
匹配的字符串出现一次或多次 |
[abcd] |
和指定字符之一匹配的单个字符 |
[^abc] |
和指定字符之外的字符匹配的单个字符 |
\t |
制表符 |
\\ |
反斜杠 |
\c |
XML 名称字符 |
\s |
空格、制表符、回车换行或者新行字符 |
. |
回车换行或新行字符外的任何字符 |
更多表达式请参阅 XML in a Nutshell, Third Edition 的第 427-429
页或者阅读 XML Bible, Second Edition 在线版(请参阅 参考资料)第 24 章的表 24-5。
XSD 异常处理
为了处理 XML Schema 操作异常,必须启用验证。对于 Xerces 只要将模式验证特性设置为
true:
parser.setFeature(
"http://apache.org/xml/features/validation/schema",
true ); |
可以通过 Apache Software Foundation 网站(请参阅 参考资料)了解
Xcerse 解析器的各种特性。
前面讨论过由于操纵问题可能造成的 DOMException。DOMException 的 code
表明发生了什么类型的问题。
回顾 DOMEcho
改变 DOMEcho.java 逻辑引发一个 DOMException。新的逻辑如下:
//---- find parent Node
Element indianaJones = document.getElementById("_7755522");
//---- insert a description before the price
// (anywhere else would be invalid)
NodeList years = indianaJones.getElementsByTagName("price");
Node desc = document.createTextNode( "Indiana Jones
is hired to find the Ark of the Covenant");
// This change will now fail validation.
indianaJones.insertBefore( desc, indianaJones ); |
从而执行下列代码:
short code = e.code;
...
} else if( code == DOMException.NOT_FOUND_ERR ) {
//take action when element or attribute not found
echo( "*** Element not found" );
System.exit(code);
} |
关于 XML Schemas 验证的更多信息,请阅读 XML in a Nutshell, Third
Edition 的第 17 章、W3Schools 或者 “Interactive XML tutorials”(请参阅 参考资料)。
使用 XQuery
XML Query(XQuery)是一种用于编写表达式的语言,表达式从 XML 数据(通常是数据库)中返回匹配的结果。其功能类似于操作非
XML 内容的 SQL:
“与 SQL 相似,XQuery 包括从多个数据集中提取、汇总、聚合和连接数据的功能。”——
“Java theory and practice: Screen-scraping with XQuery”,Brian Goetz(请参阅
参考资料)
XQuery 扩展了 XPath 表达式,后者将在本系列教程的第四部分 XML 转换 中详细讨论。XPath 表达式也是有效的
XQuery 表达式。那么为什么要用 XQuery 呢?XQuery 的价值在于它在表达式中增加的子句,从而能够实现和 SQL
中的 SELECT 语句功能类似的更复杂的表达式。
XQuery 子句
XQuery 包含多个子句,用缩写词 FLWOR 表示:for、let、where、order
by、return。表 2 说明了各个部分。
表 2. FLWOR 子句
子句
|
说明
|
for |
使用这种循环结构将值赋给其他子句中使用的变量。变量使用美元符号声明,比如 $name,然后从搜索结果中获得赋给它们的值。
|
let |
使用 let 将值赋给 for 以外的变量。 |
where |
和 SQL 相似,使用 where 子句根据某种条件对结果进行筛选。 |
order by |
使用该子句确定如何对结果集进行排序(ascending 或 descending)。
|
return |
使用 return 子句确定查询要输出的内容。内容可以包括文字、XML 文档内容、HTML
标记或者其他任何东西。 |
XQuery 包含由结果为 true 或 false 的条件组成 FLWOR 子句中的检索条件。下面看一些例子。可用
清单 8 中所示的 dvd.xml 作为 XML 实例文档。
清单 8. dvd.xml
<?xml version="1.0"?>
<!-- DVD inventory --> <catalog> <dvd
code="1234567"> <title>Terminator 2</title>
<price>19.95</price> <year>1991</year>
</dvd> <dvd code="7654321"> <title>The
Matrix</title> <price>12.95</price>
<year>1999</year> </dvd> <dvd code="2255577">
<title>Life as a House</title> <price>15.95</price>
<year>2001</year> </dvd> <dvd code="7755522">
<title>Raiders of the Lost Ark</title> <price>14.95</price>
<year>1981</year> </dvd> </catalog>
|
为了试验,我使用了 Saxon XQuery 工具。所有文件都放到解压 Saxon 的目录中。为了使用
XQuery 创建一个 HTML 页面按升序列出所有 DVD 的标题,我使用 清单 9 中所示的 dvdTitles.xq 文件,其中也显示了输出结果。执行该查询使用了下面的命令:
java -cp saxon8.jar net.sf.saxon.Query
-t dvdTitles.xq > dvdTitles.html |
清单 9. 按升序列出 DVD 标题的 XQuery
dvdTitles.xq:
<html>
<body>
Available DVDs:
<br/>
<ol>
{
for $title in doc("dvd.xml")/catalog/dvd/title
order by $title
return <li>{data($title)}</li>
}
</ol>
</body>
</html>
dvdTitles.html:
<?xml version="1.0" encoding="UTF-8"?>
<html>
<body>
Available DVDs:
<br/>
<ol>
<li>Life as a House</li>
<li>Raiders of the Lost Ark</li>
<li>Terminator 2</li>
<li>The Matrix</li>
</ol>
</body>
</html> |
仔细观察 清单 9 中的 XQuery 逻辑。首先,查询必须用花括号(“{}”)包围起来。可以看到,该例中使用了三个子句(for、order
by 和 return)。使用 doc() 函数打开一个 XML 文档。$title 是一个变量,在循环中设置为每个搜索结果。具体到该例中,它表示
/catalog/dvd/title 表达式的结果,即 DVD 的标题。返回子句中的 data() 函数输出 XML 中的值而不包含标记。如果仅仅使用
$title,就会得到 “<title>value</title>”,这不是希望出现在 HTML 输出中的结果。要注意
XQuery 包围在完成该网页所需要的全部 HTML 代码中。
现在,假设需要按降序输出超过 15 美元的 DVD 的价格。清单 10 显示了 XQuery
及输出。
清单 10. 按降序输出超过 15 美元的 DVD 价格
dvdPriceThreshold.hq
<html>
<body>
DVDs prices below $15.00:
<br/>
<ol>
{
for $price in doc("dvd.xml")/catalog/dvd/price
where $price < 15.00
order by
$price
descending
return <li>{data($price)}</li>
}
</ol>
</body>
</html>
dvdPrices.html
<?xml version="1.0" encoding="UTF-8"?>
<html>
<body>
DVDs prices below $15.00:
<br/>
<ol>
<li>14.95</li>
<li>12.95</li>
</ol>
</body>
</html>
|
该查询中主要的区别是指定了 where 子句。仅仅为了好玩一点儿而改变了排列顺序。
显然,还可以做很多实验来学习 XQuery 的强大功能,不过我已经介绍得够多了。如果想进一步了解,请参阅
“XQuery” 和 “Five Practical XQuery Applications”(请参阅 参考资料)。
结束语
XML 的核心是解析和验证。了解如何充分利用这些功能对于在项目中能否成功地引入 XML 至关重要。
结束语
本教程介绍了 XML 处理,主要涉及:
- 使用 SAX2 和 DOM2 解析器解析 XML 文档
- 用 DTD 和 XML Schemas 验证 XML 文档
- 使用 XQuery 从数据库中访问 XML 内容
|