将 XML 数据读入 DOM

在本节中,您将通过读取现有的 XML 文件来构造文档对象模型。


注意-可扩展样式表语言转换中,您将看到如何将 DOM 作为 XML 文件写出。 (您还将看到如何相对轻松地将现有数据文件转换为 XML.)


创建程序

文档对象模型提供了 API,可用于创建,修改,删除和重新排列节点。在try创建 DOM 之前,了解 DOM 的结构很有帮助。这一系列示例将通过名为DOMEcho的示例程序使 DOM 内部可见,在安装 JAXP API 之后,您将在目录INSTALL_DIR/jaxp\-version/samples/dom中找到该示例程序。

创建骨架

首先,构建一个简单的程序以将 XML 文档读入 DOM,然后再次将其写回。

从应用程序的常规基本逻辑开始,并检查以确保在命令行上提供了参数:

public class DOMEcho {

    static final String outputEncoding = "UTF-8";

    private static void usage() {
        // ...
    }

    public static void main(String[] args) throws Exception {
        String filename = null;
    
        for (int i = 0; i < args.length; i++) {
            if (...) { 
                // ...
            } 
            else {
                filename = args[i];
                if (i != args.length - 1) {
                    usage();
                }
            }
        }

        if (filename == null) {
            usage();
        }
    }
}

此代码执行所有基本设置操作。 DOMEcho的所有输出均使用 UTF-8 编码。如果未指定任何参数,则调用usage\(\)方法只是告诉您DOMEcho需要什么参数,因此此处未显示代码。还声明了filename字符串,它将是DOMEcho解析为 DOM 的 XML 文件的名称。

导入所需的类

在本节中,所有类都是单独命名的,以便在您要引用 API 文档时可以看到每个类的来源。在示例文件中,导入语句以更短的形式生成,例如javax\.xml\.parsers\.\*

这些是DOMEcho使用的 JAXP API:

package dom;
import javax.xml.parsers.DocumentBuilder; 
import javax.xml.parsers.DocumentBuilderFactory;

这些类用于解析 XML 文档时可能引发的异常:

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException; 
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.*

这些类读取示例 XML 文件并 管理 输出:

import java.io.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

最后,为 DOM,DOM 异常,实体和节点导入 W3C 定义:

import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Entity;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

Handle Errors

接下来,添加错误处理逻辑。最重要的一点是,当解析 XML 文档时遇到麻烦时,需要 JAXP 兼容的文档构建器报告 SAX 异常。 DOM 解析器实际上不必在内部使用 SAX 解析器,但是由于 SAX 标准已经存在,因此可以使用它来报告错误。结果,DOM 应用程序的错误处理代码与 SAX 应用程序的错误处理代码非常相似:

private static class MyErrorHandler implements ErrorHandler {
     
    private PrintWriter out;

    MyErrorHandler(PrintWriter out) {
        this.out = out;
    }

    private String getParseExceptionInfo(SAXParseException spe) {
        String systemId = spe.getSystemId();
        if (systemId == null) {
            systemId = "null";
        }

        String info = "URI=" + systemId + " Line=" + spe.getLineNumber() +
                      ": " + spe.getMessage();
        return info;
    }

    public void warning(SAXParseException spe) throws SAXException {
        out.println("Warning: " + getParseExceptionInfo(spe));
    }
        
    public void error(SAXParseException spe) throws SAXException {
        String message = "Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }

    public void fatalError(SAXParseException spe) throws SAXException {
        String message = "Fatal Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }
}

如您所见,DomEcho类的错误处理程序使用PrintWriter实例生成其输出。

实例化工厂

接下来,将以下代码添加到main\(\)方法中,以获得可以为我们提供文档构建器的工厂实例。

public static void main(String[] args) throws Exception {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    // ...
}

获取解析器并解析文件

现在,将以下代码添加到main\(\)中,以获取构建器的实例,并使用它来解析指定的文件。

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder(); 
Document doc = db.parse(new File(filename));

解析的文件由在main\(\)方法开始时声明的filename变量提供,该变量在程序运行时作为参数传递给DOMEcho

配置工厂

默认情况下,工厂返回一个不验证名称空间的非验证解析器。要获得一个验证解析器,或者一个了解名称空间(或两者兼而有之)的解析器,您可以使用以下代码将工厂配置为设置这两个选项中的一个或两个。

public static void main(String[] args) throws Exception {

    String filename = null;
    boolean dtdValidate = false;
    boolean xsdValidate = false;
    String schemaSource = null;
        
    for (int i = 0; i < args.length; i++) {
        if (args[i].equals("-dtd"))  { 
            dtdValidate = true;
        } 
        else if (args[i].equals("-xsd")) {
            xsdValidate = true;
        } 
        else if (args[i].equals("-xsdss")) {
            if (i == args.length - 1) {
                usage();
            }
            xsdValidate = true;
            schemaSource = args[++i];
        }
        else {
            filename = args[i];
            if (i != args.length - 1) {
                usage();
            }
        }
    }

    if (filename == null) {
        usage();
    }

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    dbf.setNamespaceAware(true);
    dbf.setValidating(dtdValidate || xsdValidate);

    // ...

    DocumentBuilder db = dbf.newDocumentBuilder();
    Document doc = db.parse(new File(filename));
}

如您所见,设置了命令行参数,以便您可以通知DOMEcho对 DTD 或 XML Schema 执行验证,并且将工厂配置为可识别名称空间并执行用户指定的任何验证类型。


注意- 即使参考解析器支持,也不需要 JAXP 兼容解析器支持这些选项的所有组合。如果指定的选项无效组合,则工厂在try获取解析器实例时会生成ParserConfigurationException


使用 XMLPattern 进行验证提供了有关如何使用名称空间和验证的更多信息,其中将描述上述摘录中缺少的代码。

处理验证错误

根据 SAX 标准的规定,对验证错误的默认响应是不执行任何操作。 JAXP 标准要求抛出 SAX 异常,因此您使用与用于 SAX 应用程序完全相同的错误处理机制。特别是,您可以使用DocumentBuilder类的setErrorHandler方法向其提供实现 SAX ErrorHandlerinterface的对象。


注意- DocumentBuilder也可以使用setEntityResolver方法。


以下代码将文档构建器配置为使用Handle Errors中定义的错误处理程序。

DocumentBuilder db = dbf.newDocumentBuilder();
OutputStreamWriter errorWriter = new OutputStreamWriter(System.err,
                                         outputEncoding);
db.setErrorHandler(new MyErrorHandler (new PrintWriter(errorWriter, true)));
Document doc = db.parse(new File(filename));

到目前为止,您所看到的代码已经设置了文档构建器,并将其配置为根据请求执行验证。错误处理也就位。但是,DOMEcho尚未执行任何操作。在下一节中,您将看到如何显示 DOM 结构并开始探索它。例如,您将看到 DOM 中的实体引用和 CDATA 部分的外观。也许最重要的是,您将看到文本节点(包含实际数据)如何驻留在 DOM 中的元素节点下。

显示 DOM 节点

要创建或操纵 DOM,有助于清楚了解 DOM 中的节点的结构。本教程的这一部分将介绍 DOM 的内部结构,以便您可以查看其中包含的内容。 DOMEcho示例通过回显 DOM 节点,然后使用适当的缩进在屏幕上将它们打印出来,以使节点层次结构清晰可见,来实现此 Object。这些节点类型的规范可以在Node的规范下的DOM 2 级核心规范中找到。以下Table 3-1是根据该规范改编的。

表 3-1 节点类型

NodenodeNamenodeValueAttributes
Attr属性名称属性值null
CDATASection\#cdata\-sectionCDATA 部分的内容null
Comment\#commentComment 内容null
Document\#documentnullnull
DocumentFragment\#documentFragmentnullnull
DocumentType文件类型名称nullnull
ElementTag namenullnull
EntityEntity namenullnull
EntityReference所引用实体的名称nullnull
NotationNotation namenullnull
ProcessingInstructionTarget整个内容(不包括目标)null
Text\#text文本节点的内容null

该表中的信息非常有用。在使用 DOM 时,您将需要它,因为所有这些类型都混合在 DOM 树中。

获取节点类型信息

DOM 节点元素类型信息是通过调用org\.w3c\.dom\.Node类的各种方法获得的。以下代码回显了由DOMEcho公开的节点属性。

private void printlnCommon(Node n) {
    out.print(" nodeName=\"" + n.getNodeName() + "\"");

    String val = n.getNamespaceURI();
    if (val != null) {
        out.print(" uri=\"" + val + "\"");
    }

    val = n.getPrefix();

    if (val != null) {
        out.print(" pre=\"" + val + "\"");
    }

    val = n.getLocalName();
    if (val != null) {
        out.print(" local=\"" + val + "\"");
    }

    val = n.getNodeValue();
    if (val != null) {
        out.print(" nodeValue=");
        if (val.trim().equals("")) {
            // Whitespace
            out.print("[WS]");
        }
        else {
            out.print("\"" + n.getNodeValue() + "\"");
        }
    }
    out.println();
}

每个 DOM 节点至少都有一个类型,一个名称和一个值,该值可能为空也可能不是空。在上面的示例中,Nodeinterface的getNamespaceURI\(\)getPrefix\(\)getLocalName\(\)getNodeValue\(\)方法返回并打印回显节点的名称空间 URI,名称空间前缀,本地限定名称和值。请注意,对getNodeValue\(\)返回的值调用trim\(\)方法,以确定该节点的值是否为空白,并相应地打印一条消息。

有关Node方法的完整列表以及它们返回的不同信息,请参阅Node的 API 文档。

接下来,定义一种方法来设置节点在打印时的缩进量,以便可以轻松看到节点层次结构。

private void outputIndentation() {
    for (int i = 0; i < indent; i++) {
        out.print(basicIndent);
    }
}

通过将以下突出显示的行添加到DOMEcho构造函数类中来定义basicIndent常量,该常量定义DOMEcho显示节点树层次结构时使用的缩进的基本单位。

public class DOMEcho {
    static final String outputEncoding = "UTF-8";

    private PrintWriter out;
    private int indent = 0;
    private final String basicIndent = " ";

    DOMEcho(PrintWriter out) {
        this.out = out;
    }
}

Handle Errors中定义的错误处理程序一样,DOMEcho程序会将其输出创建为PrintWriter实例。

Lexical Controls

词汇信息是重构 XML 文档原始语法所需的信息。保留词法信息对于编辑应用程序很重要,在该应用程序中,您想保存一个文档,该文档准确地反映了原始完整内容,包括 注解,实体引用以及它可能一开始就包含的所有 CDATA 部分。

但是,大多数应用程序仅关注 XML 结构的内容。他们有能力忽略 注解,也不必关心数据是在 CDATA 节中编码还是以纯文本格式编写,或者是否包含实体引用。对于此类应用程序,需要最少的词汇信息,因为它简化了应用程序必须准备检查的 DOM 节点的数量和种类。

以下DocumentBuilderFactory方法使您可以控制在 DOM 中看到的词汇信息。

  • setCoalescing\(\)

    • 要将CDATA个节点转换为Text个节点并追加到相邻的Text个节点(如果有)。
  • setExpandEntityReferences\(\)

    • 扩展实体参考节点。
  • setIgnoring注解\(\)

    • 忽略 Comment。
  • setIgnoringElementContentWhitespace\(\)

    • 忽略不是元素内容重要部分的空格。

所有这些属性的默认值为 false,这保留了以原始形式重建传入文档所需的所有词汇信息。将它们设置为 true 可以构造最简单的 DOM,以便应用程序可以专注于数据的语义内容,而不必担心词法语法细节。 Table 3-2总结了设置的效果。

表 3-2 词法控制设置

API保留词法信息专注于内容
setCoalescing\(\)FalseTrue
setExpandEntityReferences\(\)FalseTrue
setIgnoring注解\(\)FalseTrue
setIgnoringElementContent Whitespace\(\)FalseTrue

DomEcho示例的 main 方法中的这些方法的实现如下所示。

// ...

dbf.setIgnoring注解(ignore注解);
dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
dbf.setCoalescing(putCDATAIntoText);
dbf.setExpandEntityReferences(!createEntityRefs);

// ...

布尔变量ignore注解ignoreWhitespaceputCDATAIntoTextcreateEntityRefs在主方法代码的开头声明,并且在运行DomEcho时由命令行参数设置。

public static void main(String[] args) throws Exception {
    // ...

    boolean ignoreWhitespace = false;
    boolean ignore注解 = false;
    boolean putCDATAIntoText = false;
    boolean createEntityRefs = false;

    for (int i = 0; i < args.length; i++) {
        if (...) {  // Validation arguments here
           // ... 
        } 
        else if (args[i].equals("-ws")) {
            ignoreWhitespace = true;
        } 
        else if (args[i].startsWith("-co")) {
            ignore注解 = true;
        }
        else if (args[i].startsWith("-cd")) {
            putCDATAIntoText = true;
        } 
        else if (args[i].startsWith("-e")) {
            createEntityRefs = true;

            // ...
        } 
        else {
            filename = args[i];

            // Must be last arg
            if (i != args.length - 1) {
                usage();
            }
        }
    }

    // ...
}

打印 DOM 树节点

DomEcho应用程序使您可以查看 DOM 的结构,并演示由哪些节点组成 DOM 以及如何安排它们。通常,DOM 树中的绝大多数节点将是ElementText个节点。


注意- 文本节点位于 DOM 中 元素下 ,并且数据始终存储在文本节点中。 DOM 处理中最常见的错误也许是导航到元素节点,并期望它包含存储在该元素中的数据。不是这样!即使是最简单的元素节点,其下都有一个包含数据的文本节点。


下面显示了使用适当的缩进打印出 DOM 树节点的代码。

private void echo(Node n) {
    outputIndentation();
    int type = n.getNodeType();

    switch (type) {
        case Node.ATTRIBUTE_NODE:
            out.print("ATTR:");
            printlnCommon(n);
            break;

        case Node.CDATA_SECTION_NODE:
            out.print("CDATA:");
            printlnCommon(n);
            break;

        case Node.COMMENT_NODE:
            out.print("COMM:");
            printlnCommon(n);
            break;

        case Node.DOCUMENT_FRAGMENT_NODE:
            out.print("DOC_FRAG:");
            printlnCommon(n);
            break;

        case Node.DOCUMENT_NODE:
            out.print("DOC:");
            printlnCommon(n);
            break;

        case Node.DOCUMENT_TYPE_NODE:
            out.print("DOC_TYPE:");
            printlnCommon(n);
            NamedNodeMap nodeMap = ((DocumentType)n).getEntities();
            indent += 2;
            for (int i = 0; i < nodeMap.getLength(); i++) {
                Entity entity = (Entity)nodeMap.item(i);
                echo(entity);
            }
            indent -= 2;
            break;

        case Node.ELEMENT_NODE:
            out.print("ELEM:");
            printlnCommon(n);

            NamedNodeMap atts = n.getAttributes();
            indent += 2;
            for (int i = 0; i < atts.getLength(); i++) {
                Node att = atts.item(i);
                echo(att);
            }
            indent -= 2;
            break;

        case Node.ENTITY_NODE:
            out.print("ENT:");
            printlnCommon(n);
            break;

        case Node.ENTITY_REFERENCE_NODE:
            out.print("ENT_REF:");
            printlnCommon(n);
            break;

        case Node.NOTATION_NODE:
            out.print("NOTATION:");
            printlnCommon(n);
            break;

        case Node.PROCESSING_INSTRUCTION_NODE:
            out.print("PROC_INST:");
            printlnCommon(n);
            break;

        case Node.TEXT_NODE:
            out.print("TEXT:");
            printlnCommon(n);
            break;

        default:
            out.print("UNSUPPORTED NODE: " + type);
            printlnCommon(n);
            break;
    }

    indent++;
    for (Node child = n.getFirstChild(); child != null;
         child = child.getNextSibling()) {
        echo(child);
    }
    indent--;
}

此代码首先使用 switch 语句以适当的缩进打印出不同的节点类型和任何可能的子节点。

节点属性不作为子级包含在 DOM 层次结构中。相反,它们是通过Nodeinterface的getAttributes方法获得的。

DocTypeinterface是w3c\.org\.dom\.Node的扩展。它定义了getEntities方法,可用于获取Entity节点-定义实体的节点。像Attribute节点一样,Entity节点不会显示为 DOM 节点的子节点。

Node Operations

本节将快速介绍您可能要应用于 DOM 的一些操作。

  • Creating nodes

  • Traversing nodes

  • 搜索节点

  • 获取节点内容

  • Creating attributes

  • 删除和更改节点

  • Inserting nodes

Creating Nodes

您可以使用Documentinterface的方法创建不同类型的节点。例如createElementcreateCommentcreateCDATAsectioncreateTextNode等。 org.w3c.dom.Document的 API 文档中提供了用于创建不同节点的方法的完整列表。

Traversing Nodes

org\.w3c\.dom\.Nodeinterface定义了可用于遍历节点的多种方法,包括getFirstChildgetLastChildgetNextSiblinggetPreviousSiblinggetParentNode。这些操作足以从树中的任何位置到达树中的任何其他位置。

搜索节点

当您搜索具有特定名称的节点时,还有一点需要考虑。尽管很想获得第一个孩子并检查它是否合适,但是搜索必须考虑到以下事实:子列表中的第一个孩子可能是 注解 或处理指令。如果 XML 数据尚未通过验证,则它甚至可能是包含可忽略空格的文本节点。

本质上,您需要浏览子节点列表,忽略那些无关紧要的子节点,并检查您关心的子节点。这是在 DOM 层次结构中搜索节点时需要编写的例程类型的示例。它在此处完整显示(带有 注解),因此您可以在应用程序中将其用作模板。

/**
 * Find the named subnode in a node's sublist.
 * <ul>
 * <li>Ignores comments and processing instructions.
 * <li>Ignores TEXT nodes (likely to exist and contain
 *         ignorable whitespace, if not validating.
 * <li>Ignores CDATA nodes and EntityRef nodes.
 * <li>Examines element nodes to find one with
 *        the specified name.
 * </ul>
 * @param name  the tag name for the element to find
 * @param node  the element node to start searching from
 * @return the Node found
 */
public Node findSubNode(String name, Node node) {
    if (node.getNodeType() != Node.ELEMENT_NODE) {
        System.err.println("Error: Search node not of element type");
        System.exit(22);
    }

    if (! node.hasChildNodes()) return null;

    NodeList list = node.getChildNodes();
    for (int i=0; i < list.getLength(); i++) {
        Node subnode = list.item(i);
        if (subnode.getNodeType() == Node.ELEMENT_NODE) {
           if (subnode.getNodeName().equals(name)) 
               return subnode;
        }
    }
    return null;
}

有关此代码的更详细说明,请参见何时使用 DOM中的增加复杂性。还要注意,您可以使用Lexical Controls中描述的 API 来修改解析器构造的 DOM 的类型。但是,关于此代码的好处是,它将适用于几乎所有 DOM。

获取节点内容

当要获取节点包含的文本时,再次需要遍历子节点列表,忽略无关紧要的条目,并累积在TEXT个节点,CDATA个节点和EntityRef个节点中找到的文本。这是可用于该过程的例程类型的示例。

/**
  * Return the text that a node contains. This routine:
  * <ul>
  * <li>Ignores comments and processing instructions.
  * <li>Concatenates TEXT nodes, CDATA nodes, and the results of
  *     recursively processing EntityRef nodes.
  * <li>Ignores any element nodes in the sublist.
  *     (Other possible options are to recurse into element 
  *      sublists or throw an exception.)
  * </ul>
  * @param    node  a  DOM node
  * @return   a String representing its contents
  */
public String getText(Node node) {
    StringBuffer result = new StringBuffer();
    if (! node.hasChildNodes()) return "";

    NodeList list = node.getChildNodes();
    for (int i=0; i < list.getLength(); i++) {
        Node subnode = list.item(i);
        if (subnode.getNodeType() == Node.TEXT_NODE) {
            result.append(subnode.getNodeValue());
        }
        else if (subnode.getNodeType() == Node.CDATA_SECTION_NODE) {
            result.append(subnode.getNodeValue());
        }
        else if (subnode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
            // Recurse into the subtree for text
            // (and ignore comments)
            result.append(getText(subnode));
        }
    }

    return result.toString();
}

有关此代码的更详细说明,请参见何时使用 DOM中的增加复杂性。同样,您可以通过使用Lexical Controls中描述的 API 修改解析器构造的 DOM 的类型来简化此代码。但是,此代码的优点是,它将几乎可用于任何 DOM。

Creating Attributes

org\.w3c\.dom\.Elementinterface扩展了 Node,它定义了setAttribute操作,该操作向该节点添加了一个属性。 (从 Java 平台的角度来看,更好的名字是addAttribute.该属性不是类的属性,而是创建了一个新对象.)您还可以使用DocumentcreateAttribute操作创建Attribute的实例,然后使用setAttributeNode方法添加它。

删除和更改节点

要删除节点,请使用其父节点的removeChild方法。要对其进行更改,可以使用父节点的replaceChild操作或节点的setNodeValue操作。

Inserting Nodes

创建新节点时要记住的重要一点是,当您创建元素节点时,指定的唯一数据就是名称。实际上,该节点为您提供了一个挂东西的钩子。通过将项目添加到子节点列表中,可以将其挂在钩子上。例如,您可以添加文本节点,CDATA节点或属性节点。在构建时,请记住本教程中看到的结构。切记:层次结构中的每个节点都非常简单,仅包含一个数据元素。

运行 DOMEcho 示例

要运行DOMEcho示例,请按照以下步骤操作。

  • 导航到samples目录. % cd install-dir/jaxp-1_4_2-release-date/samples.

  • 编译示例类. % javac dom/*

  • 在 XML 文件上运行DOMEcho程序.

data目录中选择一个 XML 文件,然后在其上运行DOMEcho程序。在这里,我们选择在文件personal\-schema\.xml上运行程序。

% java dom/DOMEcho data/personal-schema.xml

XML 文件personal\-schema\.xml包含一家小公司的人员文件。在其上运行DOMEcho程序时,应该看到以下输出。

DOC: nodeName="#document"
 ELEM: nodeName="personnel" 
       local="personnel"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="person" 
       local="person"
 ATTR: nodeName="id" 
       local="id" 
       nodeValue="Big.Boss"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="name" 
       local="name"
 ELEM: nodeName="family" 
       local="family"
 TEXT: nodeName="#text" 
       nodeValue="Boss"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="given" 
       local="given"
 TEXT: nodeName="#text" 
       nodeValue="Big"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="email" 
       local="email"
 TEXT: nodeName="#text" 
       nodeValue="[email protected]"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="link" 
       local="link"
 ATTR: nodeName="subordinates" 
       local="subordinates" 
       nodeValue="one.worker two.worker 
                  three.worker four.worker
                  five.worker"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="person" 
       local="person"
 ATTR: nodeName="id" 
       local="id" 
       nodeValue="one.worker"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="name" 
       local="name"
 ELEM: nodeName="family" 
       local="family"
 TEXT: nodeName="#text" 
       nodeValue="Worker"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="given" 
       local="given"
 TEXT: nodeName="#text" 
       nodeValue="One"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="email" 
       local="email"
 TEXT: nodeName="#text" 
       nodeValue="[email protected]"
 TEXT: nodeName="#text" 
       nodeValue=[WS]
 ELEM: nodeName="link" 
       local="link"
 ATTR: nodeName="manager" 
       local="manager" 
       nodeValue="Big.Boss"
 TEXT: nodeName="#text"
       nodeValue=[WS]

[...]

如您所见,DOMEcho打印出文档中不同元素的所有节点,并带有正确的缩进以显示节点层次结构。