正如其名稱所暗示的,基於事件的語法分析器將事件發送給應用程序。這些事件類似於用戶界面事件,例如,瀏覽器中的 ONCLICK
事件或者 Java 中的 AWT/Swing 事件。
事件通知應用程序發生了某件事並需要應用程序作出反應。在瀏覽器中,通常爲響應用戶操作而生成事件:當用戶單擊按鈕時,按鈕產生一個 ONCLICK
事件。
在 XML 語法分析器中,事件與用戶操作無關,而與正在讀取的 XML 文檔中的元素有關。有對於以下方面的事件:
- 元素開始和結束標記
- 元素內容
- 實體
- 語法分析錯誤
圖 3 顯示語法分析器在讀取文檔時如何生成事件。
圖 3. 語法分析器生成事件
清單 1 顯示了 XML 格式的清單。它詳細列出了不同公司對 XML 培訓的收費。圖 4 顯示了價目表文檔的結構。
清單 1. pricelist.xml
<?xml version="1.0"?> |
圖 4. 價目表的結構
XML 語法分析器讀取並解釋該文檔。每當它識別出文檔中的某些內容,就會生成一個事件。
讀取 清單 1 時,語法分析器首先讀取 XML 聲明並生成文檔開始事件。當它遇到第一個開始標記 <xbe:price-list>
時,語法分析器生成它的第二個事件來通知應用程序已經遇到了 price-list
元素。
接下來,語法分析器看到 product
元素的開始標記(爲簡單起見,在本文其餘部分,我將忽略名稱空格和縮進空格)並生成它的第三個事件。
在開始標記後,語法分析器看到 product
元素的內容: XML Training
,它產生另一個事件。
下一個事件指出 product
元素的結束標記。語法分析器已經完成了對 product
元素的語法分析。到目前爲止,它已經激發了 5 個事件: product
元素的 3 個事件,一個文檔開始事件和一個 price-list
開始標記事件。
語法分析器現在移動到第一個 price-quote
元素。它爲每個 price-quote
元素生成兩個事件:一個開始標記事件和一個結束標記事件。
是的,即使將結束標記簡化爲開始標記中的 /
字符,語法分析器仍然生成一個結束事件。
有 4 個 price-quote
元素,所以語法分析器在分析它們時生成 8 個事件。最後,語法分析器遇到 price-list
的結束標記並生成它的最後兩個事件:結束 price-list
和文檔結束。
如圖 5 所示,這些事件共同嚮應用程序描述了文檔樹。開始標記事件意味着“轉到樹的下一層”,而結束標記元素意味着“轉到樹的上一層”。
圖 5. 語法分析器如何隱含地構建樹
請注意,語法分析器傳遞了足夠信息以構建 XML 文檔的文檔樹,但是與 DOM 語法分析器不同,它並不顯式地構建該樹。
現在,我敢肯定你已經糊塗了。應該使用哪一種類型的 API,應該何時使用它 - SAX 還是 DOM?不幸的是,這個問題沒有明確的答案。這兩種 API 中沒有一種在本質上更好;他們適用於不同的需求。
經驗法則是在需要更多控制時使用 SAX;要增加方便性時,則使用 DOM。例如,DOM 在腳本語言中很流行。
採用 SAX 的主要原因是效率。SAX 比 DOM 做的事要少,但提供了對語法分析器的更多控制。當然,如果語法分析器的工作減少,則意味着您(開發者)有更多的工作要做。
而且,正如我們已討論的,SAX 比 DOM 消耗的資源要少,這只是因爲它不需要構建文檔樹。
在 XML 早期,DOM 得益於 W3C 批准的官方 API 這一身份。逐漸地,開發者選擇了功能性而放棄了方便性,並轉向了 SAX。
SAX 的主要限制是它無法向後瀏覽文檔。實際上,激發一個事件後,語法分析器就將其忘記。如您將看到的,應用程序必須顯式地緩衝其感興趣的事件。
當然,無論它實現 SAX 還是 DOM API,語法分析器都做許多工作:它讀取文檔,強制實施 XML 語法並解析實體 - 先只列舉這幾個。驗證語法分析器還強制實施文檔模式。
使用語法分析器有很多原因,並且您應該掌握 API、SAX 和 DOM。它使您能靈活地根據手上的任務來選擇最好的 API。幸好,現代語法分析器同時支持兩種 API。
SAX 是由 XML-DEV 郵件列表的成員開發的一種用於基於事件的語法分析器的標準和簡單的 API。SAX 是“Simple API for XML”的縮寫。
SAX 最初是爲 Java 而定義,但是它也可以用於 Python、Perl、C++ 和 COM(Windows 對象)。以後一定還有更多的語言綁定。而且,通過 COM,SAX 語法分析器還可以用於所有 Windows 編程語言,包括 Visual Basic 和 Delphi。
與 DOM 不同,SAX 沒有經過官方標準機構的認可,但是它被廣泛使用並被視爲事實上的標準。(現在,SAX 由 David Megginson 編輯,但是他已經宣佈將要退休。)
如您所見,在瀏覽器中,DOM 是首選的 API。因此,本章中的示例是用 Java 編寫的。(如果您覺得需要一個 Java 速成課程,請轉至拙作的附錄 A 或者 developerWorks Java 區的“教學”部分。)
一些支持 SAX 的語法分析器包括 Xerces,Apache parser(以前的 IBM 語法分析器)、MSXML(Microsoft 語法分析器)和 XDK(Oracle 語法分析器)。這些語法分析器是最靈活的,因爲它們還支持 DOM。
有幾個語法分析器僅提供 SAX,例如 James Clark 的 XP和 Vivid Creations 的 ActiveSAX(請參閱 參考資料)。
清單 2是查找清單 1 中最便宜價格的 Java 應用程序。該應用程序打印出最優的價格和供應商名稱。
要編這個應用程序,需要適用於您平臺的“Java 開發工具箱(JDK)”(請參閱 參考資料)。對於該示例,Java Runtime(Java 運行時環境)是不夠的。
從 作者網站的 XBE2 頁面下載本摘錄的清單。下載內容包括 Xerces。如果清單有問題,請訪問作者網站以獲取更新。
在名爲 Cheapest.java
的文件中保存 清單 2 。轉至 DOS 提示符,更改到保存 Cheapest.java
的目錄,然後在 DOS 提示符處發出下列命令來編譯:
mkdir classes |
編譯將在 classes 目錄中安裝 Java 程序。這些命令假設您已經在 lib
目錄中安裝了 Xerces,並且在 src
目錄中安裝了清單 2。如果在另一個目錄下安裝語法分析器,則可能必須修改 classpath
(第二條命令)。
要對價目表運行應用程序,請發出下面的命令:
java com.psol.xbe2.Cheapest data\pricelist.xml |
結果應該是:
The cheapest offer is from XMLi ($699.00) |
這條命令假設 清單 1在一個名爲 data\pricelist.xml 的文件中。同樣,您可能需要修改系統路徑。
將 SAX 中的事件定義爲連接到特定 Java 接口的方法。本節將逐步複查清單 2。下面一節爲您提供關於主要 SAX 接口的更多信息。
聲明事件處理器的最簡單方案是繼承 SAX 提供的 DefaultHandler
:
public class Cheapest |
該應用程序僅實現一個事件處理器 startElement()
,語法分析器在遇到開始標記時調用它。語法分析器將對文檔 <xbe:price-list>
、 <xbe:product>
和 <xbe:price-quote>
中的每個開始標記調用 startElement()
。
在清單 2 中,事件處理器僅對 price-quote
感興趣,所以僅對它測試。該處理器對其它元素的事件不作任何處理。
if(uri.equals(NAMESPACE_URI) && name.equals("price-quote")) |
當事件處理器發現 price-quote
元素時,它從屬性列表中抽取供應商名稱和價格。有了這些信息,查找最便宜的產品就是一個簡單的比較處理了。
String attribute = |
請注意,事件處理器接收元素名稱、名稱空間和屬性列表作爲來自語法分析器的參數。
現在,讓我們將注意力轉向 main()
方法。它創建一個事件處理器對象和一個語法分析器對象:
Cheapest cheapest = new Cheapest(); |
XMLReader
和 XMLReaderFactory
由 SAX 定義。 XMLReader
是一種 SAX 語法分析器。factory 是用於創建 XMLReaders
的幫助器類。
main()
設置一個語法分析器功能以請求名稱空間處理,並且使用語法分析器註冊事件處理器。最後,main() 使用至 XML 文件的 URI 調用 parse() 方法:
parser.setFeature("http://xml.org/sax/features/namespaces",true); |
看似無關的 parse()
方法觸發對 XML 文檔的語法分析,這導致了調用事件處理器。我們的 startElement()
方法正是在執行這個方法期間被調用的。在調用 parse()
背後發生了很多事情。
最後但很重要的一點, main()
打印出結果:
Object[] objects = new Object[] |
等一下! Cheapest.vendor
和 Cheapest.min
何時獲取它們的值?我們不在 main()
中顯式地設置它們!確實如此;這是事件處理器的工作。最後由 parse()
調用事件處理器。這就是事件處理的美妙之處。
注意 請記住,除非已經安裝了“Java 開發工具箱”,否則不能編譯這些示例。最後,可能有一個錯誤類似於:
或
這極有可能出自以下原因:
|
到目前爲止,我們僅討論了一個事件( startElement()
)。在繼續之前,讓我們研究一下 SAX 定義的主接口。
SAX 將其事件分爲幾個接口:
ContentHandler
定義與文檔本身關聯的事件(例如,開始和結束標記)。大多數應用程序都註冊這些事件。DTDHandler
定義與 DTD 關聯的事件。然而,它不定義足夠的事件來完整地報告 DTD。如果需要對 DTD 進行語法分析,請使用可選的 DeclHandler。DeclHandler 是 SAX 的擴展,並且不是所有的語法分析器都支持它。EntityResolver
定義與裝入實體關聯的事件。只有少數幾個應用程序註冊這些事件。ErrorHandler
定義錯誤事件。許多應用程序註冊這些事件以便用它們自己的方式報錯。
爲簡化工作,SAX 在 DefaultHandler
類中提供了這些接口的缺省實現。在大多數情況下,爲應用程序擴展 DefaultHandler
並覆蓋相關的方法要比直接實現一個接口更容易。
爲註冊事件處理器並啓動語法分析器,應用程序使用 XMLReader
接口。如我們所見, parse()
,這種 XMLReader
方法,啓動語法分析:
parser.parse(args[0]); |
XMLReader 的主要方法是:
parse()
對 XML 文檔進行語法分析。parse()
有兩個版本;一個接受文件名或 URL,另一個接受InputSource
對象(請參閱“InputSource”一節)。setContentHandler()
、setDTDHandler()
、setEntityResolver()
和setErrorHandler()
讓應用程序註冊事件處理器。setFeature()
和setProperty()
控制語法分析器如何工作。它們採用一個特性或功能標識(一個類似於名稱空間的 URI 和值)。功能採用 Boolean 值,而特性採用“對象”。
最常用的 XMLReaderFactory 功能是:
http:// xml.org/sax/features/namespaces
,所有 SAX 語法分析器都能識別它。如果將它設置爲 true(缺省值),則在調用ContentHandler
的方法時,語法分析器將識別出名稱空間並解析前綴。http://xml.org/sax/features/validation
,它是可選的。如果將它設置爲 true,則驗證語法分析器將驗證該文檔。非驗證語法分析器忽略該功能。
XMLReaderFactory
創建語法分析器對象。它定義 createXMLReader()
的兩個版本:一個採用語法分析器的類名作爲參數,另一個從 org.xml.sax.driver
系統特性中獲得類名稱。
對於 Xerces,類是 org.apache.xerces.parsers.SAXParser
。應該使用 XMLReaderFactory
,因爲它易於切換至另一種 SAX 語法分析器。實際上,只需要更改一行然後重新編譯。
XMLReader parser = XMLReaderFactory.createXMLReader( |
爲獲得更大的靈活性,應用程序可以從命令行讀取類名或使用不帶參數的 createXMLReader()
。因此,甚至可以不重新編譯就更改語法分析器。
InputSource
控制語法分析器如何讀取文件,包括 XML 文檔和實體。
在大多數情況下,文檔是從 URL 裝入的。但是,有特殊需求的應用程序可以覆蓋 InputSource
。例如,這可以用來從數據庫中裝入文檔。
ContentHandler
是最常用的 SAX 接口,因爲它定義 XML 文檔的事件。
如您所見, 清單 2 實現在 ContentHandler
中定義的事件 startElement()
。它用語法分析器註冊 ContentHandler
:
Cheapest cheapest = new Cheapest(); |
ContentHandler
聲明下列事件:
startDocument()
/endDocument()
通知應用程序文檔的開始或結束。startElement()
/endElement()
通知應用程序標記的開始或結束。屬性作爲Attributes
參數傳遞(請參閱下面一節“屬性”)。即使只有一個標記,“空”元素(例如,<img href="logo.gif"/>
)也生成startElement()
和endElement()
。startPrefixMapping()
/endPrefixMapping()
通知應用程序名稱空間作用域。您幾乎不需要該信息,因爲當http://xml.org/sax/features/namespaces
爲 true 時,語法分析器已經解析了名稱空間。- 當語法分析器在元素中發現文本(已經過語法分析的字符數據)時,
characters()
/ignorableWhitespace()
會通知應用程序。要知道,語法分析器負責將文本分配到幾個事件(更好地管理其緩衝區)。ignorableWhitespace
事件用於由 XML 標準定義的可忽略空格。 processingInstruction()
將處理指令通知應用程序。skippedEntity()
通知應用程序已經跳過了一個實體(即,當語法分析器未在 DTD/schema 中發現實體聲明時)。setDocumentLocator()
將Locator
對象傳遞到應用程序;請參閱後面的 Locator 一節。請注意,不需要 SAX 語法分析器提供Locator
,但是如果它提供了,則必須在任何其它事件之前激活該事件。
屬性
在 startElement()
事件中,應用程序在 Attributes
參數中接收屬性列表。
String attribute = attributes.getValue("","price"); |
Attributes
定義下列方法:
getValue(i)
/getValue(qName)
/getValue(uri,localName) 返回第 i 個屬性值或給定名稱的屬性值。getLength()
返回屬性數目。getQName(i)
/getLocalName(i)
/getURI(i) 返回限定名(帶前綴)、本地名(不帶前綴)和第 i 個屬性的名稱空間 URI。- getType(i)/getType(qName)/getType(uri,localName) 返回第 i 個屬性的類型或者給定名稱的屬性類型。類型爲字符串,即在 DTD 所使用的:
“CDATA”
、“ID”
、“IDREF”
、“IDREFS”
、“NMTOKEN”
、“NMTOKENS”
、“ENTITY”
、“ENTITIES”
或“NOTATION”
Locator
爲應用程序提供行和列的位置。不需要語法分析器來提供 Locator
對象。
Locator
定義下列方法:
getColumnNumber()
返回當前事件結束時所在的那一列。在endElement()
事件中,它將返回結束標記所在的最後一列。getLineNumber()
返回當前事件結束時所在的行。在endElement()
事件中,它將返回結束標記所在的行。getPublicId()
返回當前文檔事件的公共標識。getSystemId()
返回當前文檔事件的系統標識。
DTDHandler
聲明兩個與 DTD 語法分析器相關的事件。
notationDecl()
通知應用程序已經聲明瞭一個標記。nparsedEntityDecl()
通知應用程序已經發現了一個未經過語法分析的實體聲明。
EntityResolver
接口僅定義一個事件 resolveEntity()
,它返回 InputSource
(在另一章討論)。
因爲 SAX 語法分析器已經可以解析大多數 URL,所以很少應用程序實現 EntityResolver
。例外情況是目錄文件(在另一章中討論),它將公共標識解析成系統標識。如果在應用程序中需要目錄文件,請下載 Norman Walsh 的目錄軟件包(請參閱 參考資料)。
ErrorHandler
接口定義錯誤事件。處理這些事件的應用程序可以提供定製錯誤處理。
安裝了定製錯誤處理器後,語法分析器不再拋出異常。拋出異常是事件處理器的責任。
接口定義了與錯誤的三個級別或嚴重性對應的三個方法:
warning()
警示那些不是由 XML 規範定義的錯誤。例如,當沒有 XML 聲明時,某些語法分析器發出警告。它不是錯誤(因爲聲明是可選的),但是它可能值得注意。error()
警示那些由 XML 規範定義的錯誤。fatalError()
警示那些由 XML 規範定義的致命錯誤。
SAX 定義的大多數方法都可以拋出 SAXException
。當對 XML 文檔進行語法分析時, SAXException
通知一個錯誤。
錯誤可以是語法分析錯誤也可以是事件處理器中的錯誤。要報告來自事件處理器的其它異常,可以將異常封裝在 SAXException
中。
示例: 假設在處理 startElement
事件時,事件處理器捕獲了一個 IndexOutOfBoundsException
。事件處理器可以將IndexOutOfBoundsException
封裝在 SAXException
中:
public void startElement(String uri, |
SAXException
一直向上傳遞到 parse()
方法,它在那裏被捕獲並進行解釋。
try |
清單 1 對於 SAX 語法分析器是很方便的,因爲它將信息存儲爲價格元素的屬性。應用程序只需要註冊 startElement()
。
示例清單 3 更復雜,因爲信息分散到了幾個元素中。特別是,根據不同的交付延遲,供應商有不同的價格。如果用戶願意等待,他(或她)可能得到更好的價格。圖 6 演示了文檔結構。
清單 3. xtpricelist.xml
<?xml version="1.0"?> |
圖 6. 價目表結構
要找到最好的生意,應用程序必須從幾個元素蒐集信息。但是,語法分析器可以最多爲每個元素生成三個事件 - startElement()
、 characters()
和 endElement()
。應用程序必須以某種方法將事件和元素相關聯。
清單 4是一個新建的 Java 應用程序,它查找價目表中的最優價格。當查找最優價格時,它考慮到了客戶對交付日期的需求。實際上,清單 3 中最便宜的供應商(XMLi)也是最慢的。另一方面,Emailaholic 很貴,但是它可以在兩天內交付。
您可以如前面介紹的 Cheapest 應用程序那樣編譯並運行該應用程序。結果取決於對交付日期的需求。您將注意到這個程序採用兩個參數:文件名和客戶願意等待的最長延遲。
java com.psol.xbe2.BestDeal data/xtpricelist.xml 60 |
返回:
The best deal is proposed by XMLi. A(n) XML Training delivered[ccc] |
而:
java com.psol.xbe2.BestDeal data/xtpricelist.xml 3 |
返回:
The best deal is proposed by Emailaholic. A(n) XML Training[ccc] |
清單 4 是目前爲止您所見到的最複雜的應用程序。這沒什麼不尋常的:SAX 語法分析器的級別很低,所以應用程序必須接管本來由 DOM 才能完成的大量工作。
應用程序是圍繞兩個類組織的: SAX2BestDeal
和 BestDeal
。 SAX2BestDeal
管理 SAX 語法分析器之間的接口。它用一致的方法來管理狀態並將事件分組。
BestDeal
具有執行價格比較的邏輯。它還以結構形式保持爲應用程序而不是爲 XML 優化的信息。圖 7 演示了該應用程序的體系結構。圖 8 顯示了 UML 類圖。
圖 7. 應用程序的體系結構
圖 8. 應用程序的類圖
SAX2BestDeal
處理幾個事件: startElement()
、 endElement()
和 characters()
。 SAX2BestDeal
一直跟蹤其在文檔樹中的位置。
例如,在 characters()
事件中, SAX2BestDeal
需要知道文本是名稱、價格還是可以忽略的空格。而且,有兩個 name
元素: price-list
的 name
和 vendor
的 name
。
與 DOM 語法分析器不同,SAX 語法分析器不提供狀態信息。應用程序負責跟蹤它自己的狀態。這有幾個可選實體。清單 4 標識有意義的狀態以及它們之間的轉換。從圖 6 中的文檔結構中獲得該信息並不困難。
很明顯,應用程序將首先遇到 price-list 標記。因此,第一個狀態應該是 位於 price-list
內。 從那裏開始,應用程序到達一個 name
。因此,第二個狀態是 位於 price-list
的 name
內。
下一個元素必須是 vendor
,因此第三個狀態是 位於 price-list
的 vendor
內。 第四個狀態是 位於 price-list
的 vendor
的 name
內, 因爲
name 跟在 vendor 後。
name
後面是一個 price-quote
元素,相應的狀態是 位於 price-list
的 vendor
的 price
內。 隨後,語法分析器遇到已經有狀態存在的 price-quote
或 vendor
。
在帶有狀態和轉換的圖上(例如圖 9 所示)會更容易使這個概念可視化。請注意根據您在處理 price-list/name
還是price-list/vendor/name
,有兩個不同的狀態與兩個不同的名稱元素相關聯。
圖 9. 狀態轉換圖
在清單 4 中狀態變量存儲當前狀態:
final protected int START = 0, |
轉換 1狀態變量的值根據事件而相應更改。在本示例中,elementStart() 更新狀態:
ifswitch(state) |
SAX2BestDeal
有幾個實例變量來存儲當前 name
和 price-quote
的內容。實際上,它維護樹的一個小子集。請注意,與 DOM 不同,它從不擁有整個樹,因爲當應用程序使用過 name
和 price-quote
之後,它會廢棄它們。
這是很有效的內存策略。事實上,您可以處理幾十億字節的文件,因爲在任何時候,內存中只有一個小子集。
轉換 2 語法分析器對文檔中的每個字符(包括縮進)調用 characters()
。只有記入 name
和 price-quote
中的文本纔有意義,因此事件處理器使用狀態。
switch(state) |
轉換 3endElement() 的事件處理器更新狀態,並調用 BestDeal 來處理當前元素:switch(state)
{ |
清單 4 是典型的 SAX 應用程序。有一個 SAX 事件處理器( SAX2BestDeal
),它用最適合應用程序的格式將事件打包。
應用程序邏輯(在 BestDeal
中)與事件處理器保持分離。事實上,在很多情況下,都獨立於 XML 來編寫應用程序邏輯。
分層方法在應用程序邏輯和語法分析之間建立一個明顯的分界。
示例也清晰地說明了 SAX 比 DOM 更高效,但是它需要程序員完成更多工作。特別是,程序員必須顯式地管理狀態和狀態之間的轉換。(在 DOM 中,狀態在樹的遞歸遍歷過程中是隱含的。)
XML 是非常靈活的標準。但實際上,XML 應用程序的靈活性取決於您,程序員,如何創建它們。本節提供一些技巧,以確保您的應用程序利用 XML 的靈活性。
BestDeal 應用程序對 XML 文檔結構的約束很少。如果在 XML 文檔中添加元素,它們就會被忽略。例如,BestDeal 將接受下列 vendor
元素:
<xbe:vendor> |
但是將忽略聯繫信息。通常,簡單地忽略未知元素是個好主意 - HTML 瀏覽器就總這樣做。
但是,從事件處理器驗證它們的結構並不困難。下列代碼片斷(摘自 startElement()
)檢查結構,並且如果 vendor 元素包含除名稱或價格以外的任何元素,則拋出 SAXException
。
case VENDOR: |