CodeGym /课程 /JAVA 25 SELF /使用 DOM、SAX:解析 XML

使用 DOM、SAX:解析 XML

JAVA 25 SELF
第 47 级, 课程 1
可用

1. DOM (Document Object Model)

有时 XML 文件太大,无法一次性加载到内存中。也可能文档结构事先未知或过于复杂。还有些场景只需处理数据的部分片段,而非整个文件,或者需要在程序运行时直接修改文档:删除节点、添加新元素或重构部分结构。

在这些情况下,经典而可靠的工具——DOM 与 SAX——就派上用场了。它们允许你在不依赖预先定义的 Java 类的前提下,直接操作 XML 的内容。

DOM 的工作原理

DOM 是一种在内存中用对象树来表示 XML 文档的方式。每个标签都会变成树中的一个节点(Node),而属性、文本值甚至注释都会成为独立的对象。加载文档后,你可以完全访问其结构:读取、修改、删除、添加元素与属性。

Java 中 DOM 的主要类

  • DocumentBuilderFactory — 用于创建解析器的工厂。
  • DocumentBuilder — 将 XML 转换为树的解析器。
  • Document — 树的根对象。
  • Element — XML 元素(标签)。
  • NodeList — 节点列表(例如,<items> 内部的所有 <item>)。

示例:使用 DOM 读取 XML 文件

假设我们需要读取如下 XML 文件:

<contacts>
    <person id="1">
        <name>Ivan</name>
        <email>ivan@example.com</email>
    </person>
    <person id="2">
        <name>Maria</name>
        <email>maria@example.com</email>
    </person>
</contacts>

代码:读取并遍历元素

import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.File;

public class DomExample {
    public static void main(String[] args) throws Exception {
        // 1. 创建工厂和解析器
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();

        // 2. 将 XML 文件加载到内存
        Document doc = builder.parse(new File("contacts.xml"));

        // 3. 获取根元素
        Element root = doc.getDocumentElement();
        System.out.println("根元素:" + root.getTagName());

        // 4. 获取所有 <person> 的列表
        NodeList persons = root.getElementsByTagName("person");

        for (int i = 0; i < persons.getLength(); i++) {
            Element person = (Element) persons.item(i);

            String id = person.getAttribute("id");
            String name = person.getElementsByTagName("name").item(0).getTextContent();
            String email = person.getElementsByTagName("email").item(0).getTextContent();

            System.out.println("id: " + id + ", name: " + name + ", email: " + email);
        }
    }
}

这里发生了什么:

  • 先创建解析器并加载 XML 文件。
  • 获取根元素(contacts)。
  • 查找所有 <person> 元素并进行遍历。
  • 对每个 <person> 读取 id 属性,以及 <name> 和 <email> 元素。

本例的 DOM 树示意

contacts
├── person (id="1")
│   ├── name ("Ivan")
│   └── email ("ivan@example.com")
└── person (id="2")
    ├── name ("Maria")
    └── email ("maria@example.com")

使用 DOM 修改 XML

DOM 不仅能读取,还能修改 XML。例如,添加一个新联系人:

// 创建新的元素 <person>
Element newPerson = doc.createElement("person");
newPerson.setAttribute("id", "3");

// <name>
Element name = doc.createElement("name");
name.setTextContent("Sergey");
newPerson.appendChild(name);

// <email>
Element email = doc.createElement("email");
email.setTextContent("sergey@example.com");
newPerson.appendChild(email);

// 添加到根元素
root.appendChild(newPerson);

// 将修改保存到文件
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File("contacts-updated.xml")));

DOM 的主要优缺点

  • 优点:适合小文件,易于做任意修改,可以在树中任意方向“游走”。
  • 缺点:整个 XML 都在内存中。对于大文件(数百 MB 甚至更大)并不合适,内存很快会耗尽。

2. SAX (Simple API for XML)

SAX 是事件驱动的解析器。它不构建树,而是“从左到右”读取 XML,并触发事件处理器:开始元素、文本内容、结束元素等。你编写自己的处理器,按需响应这些事件。

类比:
如果 DOM 像是把活页手册的所有页面摊在桌面上,SAX 则像是逐页阅读手册,只有遇到需要的页面才做笔记。

Java 中 SAX 的主要类

  • SAXParserFactory、SAXParser — 工厂与解析器。
  • DefaultHandler — 处理事件的基类。
  • 事件处理方法:startElement、characters、endElement、startDocument、endDocument。

示例:使用 SAX 读取 XML 文件

还是同一个文件 contacts.xml。我们只想打印所有人的姓名与 e-mail。

代码:SAX 解析器

import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;

public class SaxExample {
    public static void main(String[] args) throws Exception {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();

        parser.parse(new File("contacts.xml"), new ContactHandler());
    }
}

class ContactHandler extends DefaultHandler {
    private String currentElement = "";
    private String name = "";
    private String email = "";

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        currentElement = qName;
        if ("person".equals(qName)) {
            String id = attributes.getValue("id");
            System.out.println("新联系人,id: " + id);
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        String text = new String(ch, start, length).trim();
        if (text.isEmpty()) return;
        if ("name".equals(currentElement)) {
            name = text;
        } else if ("email".equals(currentElement)) {
            email = text;
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) {
        if ("person".equals(qName)) {
            System.out.println("姓名: " + name + ", email: " + email);
            name = "";
            email = "";
        }
        currentElement = "";
    }
}

现在用通俗的话解释一下这里发生了什么。当 SAX 解析器遇到一个起始标签(例如 <person>)时,会调用 startElement 方法。在那里我们立即读取 id 属性并打印出来。当遇到内部文本(例如姓名或 e-mail)时,控制权会进入 characters 方法,我们把文本暂存到临时变量中。而当解析器读到结束标签 </person> 时,会调用 endElement。此时我们已经知道了此人的姓名与 e-mail,可以打印它们,然后清空变量,以便处理下一个联系人。

核心思想是,SAX 不把整个 XML 全部保存在内存里,而是像“流式阅读器”一样,自上而下遍历文件,并对事件作出响应——标签开始、文本、标签结束。这既快又省内存,特别适合大文件。

DOM 与 SAX 的简要对比

DOM SAX
风格 树(整个结构在内存中) 事件(“即时”处理)
内存 加载整个 XML 最小内存占用
修改 可读/可改/可增 只读(通常)
数据查找 在任意位置都易于查找 需要自己跟踪“当前所在位置”
文件大小 适用于小型和中等大小文件 适用于超大文件

3. 何时使用 DOM,何时使用 SAX

DOM 很适合以下情况:

  • 文件体积较小或中等大小。
  • 需要多次访问 XML 的不同部分。
  • 需要修改文档结构。

SAX 适用于以下情况:

  • 文件非常大,无法整体加载到内存。
  • 只需快速“抽取”部分信息(例如查找所有具有特定属性的 <item> 元素)。
  • 需要高性能与最低的内存占用。
  • 不打算修改 XML,只进行读取。

建议:
在真实项目中常常两者并用:DOM 用于“更贴近人类”的设置与小型配置,SAX 用于处理日志、导出/导入和超大规模数据。

4. 实践任务:为应用编写一个小型解析器

在你的练习应用(例如“联系人簿”)中,出现了这样一个需求:快速统计使用 gmail.com 邮箱的用户数量。

DOM 方案:

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("contacts.xml"));

NodeList emails = doc.getElementsByTagName("email");
int count = 0;
for (int i = 0; i < emails.getLength(); i++) {
    String email = emails.item(i).getTextContent();
    if (email.endsWith("@gmail.com")) {
        count++;
    }
}
System.out.println("gmail.com 用户数:" + count);

SAX 方案:

class GmailCounterHandler extends DefaultHandler {
    private String currentElement = "";
    int count = 0;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        currentElement = qName;
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        if ("email".equals(currentElement)) {
            String email = new String(ch, start, length).trim();
            if (email.endsWith("@gmail.com")) {
                count++;
            }
        }
    }
    @Override
    public void endDocument() {
        System.out.println("gmail.com 用户数:" + count);
    }
}

5. 使用 DOM 和 SAX 的特点与细节

  • DOM 在 XML 文件非常大时可能“吃光”内存。如果你看到 OutOfMemoryError,很可能该切换到 SAX 了.
  • SAX 需要足够细心:要自己跟踪当前处于哪个元素,并谨慎聚合所需数据。在深度嵌套的结构中,有时需要使用栈或额外变量避免混乱。
  • 在 SAX 中,characters 对同一段文本可能被调用多次(尤其当文本很长或包含特殊字符时)。最好用 StringBuilder 累积文本。
  • DOM 非常适合搜索、导航与结构修改,但不适合流式处理。
  • 如果不确定该选哪种方式——为简单起见先用 DOM;一旦内存“吃不消”,再改为 SAX。

6. 使用 DOM 和 SAX 的常见错误

错误 1:在 SAX 中错误处理空格和换行。
characters 方法可能会返回文本碎片,包括元素之间的空格和换行。如果不筛掉 .trim().isEmpty(),就会产生大量“空”调用或拼接出错误文本。

错误 2:试图用 SAX 修改 XML。
SAX 仅用于读取!如果需要修改结构——请使用 DOM。

错误 3:破坏 SAX 的事件顺序。
如果不在 endElement 中重置变量,数据可能会在元素之间“串味”。

错误 4:对超大文件使用 DOM。
结果要么是 OutOfMemoryError,要么是非常慢的性能。

错误 5:在 DOM 中错误的类型转换。
在 DOM 中一切都是 Node,但操作属性和子元素需要转成 Element。转换错误可能导致 ClassCastException。

1
任务
JAVA 25 SELF, 第 47 级, 课程 1
已锁定
识别图书馆主集合 🏛️
识别图书馆主集合 🏛️
1
任务
JAVA 25 SELF, 第 47 级, 课程 1
已锁定
系统中的角色或用户列表 👤
系统中的角色或用户列表 👤
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION