CodeGym /課程 /JAVA 25 SELF /使用 DOM 與 SAX:解析 XML

使用 DOM 與 SAX:解析 XML

JAVA 25 SELF
等級 47 , 課堂 1
開放

1. DOM (Document Object Model)

有時候 XML 檔案太大,無法整個載入記憶體。也可能文件結構事先未知或過於複雜。在其他情況下,你只需要處理部分資料,而非整個檔案,或甚至要在程式執行時直接修改文件:刪除節點、加入新元素,或重建部分結構。

在這些情況下,經典而可靠的工具 — DOMSAX — 就派上用場。它們讓你不必綁定預先定義的 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>Mariya</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 ("Mariya")
    └── 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 的主要類別

  • SAXParserFactorySAXParser — 工廠與解析器。
  • DefaultHandler — 處理事件的基底類別。
  • 事件處理方法:startElementcharactersendElementstartDocumentendDocument

範例:使用 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

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION