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>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 的主要類別
- 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。
GO TO FULL VERSION