文本分析的三種典型設計模式
許式偉
2004-10-27
事件驅動:Parse-Handler模型(如:xml之SAX模型)
該模型主要有Parser和Handler兩個組件。其原型大體如下:
{
public:
// any event sended from Parser
...
};
class xxxParser
{
public:
xxxxParser(InputSource* source);
HRESULT parse(xxxxHandler* handler)
{
// analyze source and send event to handler
...
}
};
該模型不規定Handler類型的詳細規格,由Parser的實現者根據具體情況而定。
這種模型的核心思想就是由Parser類來具體分析文本的格式,而讓信息真正的處理者Handler類從具體的格式中脫離出來,不再需要關心文本物理組織細節。
Tokenizer模型(如:編譯器的詞法分析器)
這種模型僅涉及一個Tokenizer組件。該組件負責將文本分解爲一個個token。其原型大體如下:
{
public:
xxxTokenizer(InputSource* source);
//
// 成功返回S_OK,如果遇到eof返回S_FALSE。
//
HRESULT next(TOKEN* token);
};
其中分析的結果以一個結構體TOKEN表示。這個結構體如何設計,同樣視具體情況而定。通常它看起來是這樣的:
{
UINT type;
union
{
DATATYPE1 data1; // 當type = type1時
DATATYPE2 data2; // 當type = type2時
...
};
};
有了Tokenizer,我們就可以輕易的遍歷整個文檔:
{
TOKEN token;
xxxTokenizer tokenizer(source);
while (tokenizer.next(&token) == S_OK)
{
print(token);
}
}
token應當如何劃分,其粒度如何,完全取決於設計者的考量。以以下一段xml文本爲例:
<elem attr="value">content</elem>
你可以劃分爲:
attr="value" // attr-value pair
content // content
</elem> // element end
也可以將attr-value pair細分爲三個token:attr, assign-symbol, value。
你甚至也可以將整個element作爲一個token。
從廣義上來說,我們文件系統提供的字節流本身已經是一個Tokenizer了,只不過它劃分的token是一個個並無多少邏輯含義的character。
而我們後面提到的DOM模型,也可以算是一個Tokenizer。只不過它劃分的token只有一個,就是DOM樹,與文件系統的字節流走的是另一個極端。
Tokenizer方式與Parse-Handler方式設計思路,最大的不同在於具體處理信息的人主被動地位相異。在Tokenizer模式下,信息處理者調用Tokenizer得到分析數據,如果相鄰的token存在上下文關係,你可以根據需要去取得下一個token,故處於主動地位。
而Parse-Handler模式相關死板一些,一方面Handler類實現者纔是真正試圖處理信息的人,但是實際上對信息的劃分(token)卻是由Parse規定的,未必完全符合Handler類的需求。另一方面在token存在上下文關係,當前接受的數據信息不足時,Handler類無法隨心所欲的取得下一個token(因爲從流程上它是被動的數據接受方),而只能暫時緩存數據,等待下一條信息的到來。
文檔對象:DOM模型
DOM模型是最高級的一種模型。它的思路是將文檔完整地讀入內存,並提供數據訪問接口。
DOM模型消耗的內存最多,可提供的服務(我們可以聯想一下xml的諸多應用,如xslt等)也最爲完整。
這裏提到DOM模型消耗的內存最多,這種說法並不全面。例如,在將它與Parse-Handler模型相比時,我們只是計算了Parser的開銷,而Handler類是客戶實現的,內存開銷多少,無從計算。
另一方面,由於DOM模型可以按自己的方式組織數據,它在內存開銷上的可優化餘地很大,並且客戶在使用它時通常不再需要大量的內存分配操作;而Parse-Handler模型中,Handler類的實現者出現蹩腳的設計可能性非常高,計入Handler類的內存開銷的話,有時甚至可能遠遠超過採用DOM模型。
因此我個人認爲相對於DOM的能力而言,內存問題在DOM模型中並不算一個了不起的缺陷。實現者可以有很多技巧來進行內存優化。
但是DOM模型有一個問題,就是它一開始就將文檔完整的讀入了內存,使得它無法勝任那些對響應時間要求較高、希望能夠漸進處理的應用。而這一點是採用Parse-Handler模型和Tokenizer模型的好處。
後記
這篇文章寫得比較早,因爲最近寫WINX可視化開發工具相關的設計稿時用到,所以整理了下。我個人在文本文件和各種文檔格式的文件打交道較多,多年來也算是形成了一定的經驗。我個人現在越來越傾向於採用DOM模型來處理文件。原因在於採用DOM模型有很多優點:
- DOM模型是提供了最高級的服務,模塊的客戶負擔少。
- 模塊劃分極其清晰,方便維護。通常DOM模型的內部仍然建立於SAX模型(或Tokenizer模型)上,但是這種依賴侷限在DOM模型的內部。因此,程序通常會劃分爲3層:
SAX(或Tokenizer) ==> DOM模型 ==> DOMClient(實際的應用)
- 內存管理方面的可優化餘地大。在多數情況下,我們建立的DOM模型是隻讀的(或允許進行少量修改),這種情形下,內存管理方案可以以最簡潔的方式實現。下文我們詳細討論這一點。在此之前,我推薦你回顧一下《C++內存管理變革:最袖珍的垃圾回收器》。
- 易獲得更好的性能。雖然理論上來講程序建立在SAX模型性能上可以獲得更好的性能,但是經驗表明,在團代開發的情形下,採用DOM模型的性能通常可優於建立在SAX模型之上的同樣功能的複雜程序(不是簡單打印或提取有限數據的情形)。