Java高效能解析器實現思路及方法

InfoQ - 邵思華發表於2015-01-20

在某些情況下,你可能需要在Java中實現你自己的資料或語言解析器,也許是這種資料格式或語言缺乏標準的Java或開源解析器可以使用。或者雖然有現成的解析器實現,但它們要麼太慢,要麼太佔記憶體,要麼就是沒有符合你所需要的特性。又或者是某個開源的解析器存在缺陷,要麼是某個開源解析器的專案中止了,原因不一而足。不過無論原因是什麼,總之事實就是你必須要自己去實現這個解析器。

當你必須自己實現一個解析器時,你對它的期望會有很多,包括效能良好、靈活、特性豐富、方便使用,以及便於維護等等。說到底,這也是你自己的程式碼。在本文中,我將為你介紹在Java中實現高效能解析器的一種方式,這種方法並且獨一無二,但難度適中,不僅實現了高效能,而且它的模組化設計方式也比較合理。這種設計是受到了VTD-XML的設計方式的啟發,後者是我所見過的最快的Java XML解析器,比起StAX和SAX這兩種標準的Java XML解析器都要快上許多。

兩種基本的解析器型別

為解析器進行分類的方式有好幾種,在這裡我將解析器分為兩種基礎型別:

  • 順序訪問解析器
  • 隨機訪問解析器

順序訪問是指解析器對進行資料進行解析,在資料解析完成後將其轉交給資料處理器(processor)的過程。資料處理器只能訪問當前正在進行解析的資料,它既不能訪問已解析過的資料,也不能訪問等待解析的資料。這種解析器也被稱為基於事件的解析器,例如SAX和StAX解析器。

而隨機訪問解析器是指解析器允許資料處理程式碼可以隨意訪問正在進行解析的資料之前和之後的任意資料(隨機訪問)。這種解析器的例子有XML DOM解析器。

下圖展示了順序訪問解析器與隨機訪問解析器的不同之處:

順序訪問解析器只能讓你訪問當前正在解析的“視窗”或“事件”,而隨機訪問解析器允許你任意地瀏覽所有已解析資料。

設計概況

我在這裡所介紹的解析器設計屬於隨機訪問解析器。

隨機訪問解析器的實現通常會慢於順序訪問解析器,因為它們一般都會為已解析資料建立某種物件樹,資料處理程式碼將通過這棵樹對資料進行訪問。建立這種物件樹不僅要花費較長的CPU時間,消耗的記憶體也很大。

相對於從已解析資料中建立一棵物件樹的方式,另一種效能更佳的方式是為原來的資料緩衝區建立一個對應的索引緩衝區,這些索引會指向在已解析資料中找到的元素的起點與終點。資料處理程式碼此時不再通過物件樹訪問資料,而是直接在包括了原始資料的緩衝區中訪問已解析資料。以下是對這兩種處理方式的圖示:

由於我找不到一個更好的名字,因此我將這種方式簡單地命名為“索引覆蓋解析器”(Index Overlay Parser)。該解析器為原始資料建立了一個覆蓋於其上的索引。這種方式讓人聯想起資料庫索引將資料儲存在磁碟的方式,它為原始的、未處理的資料建立了一個索引,以實現更快地瀏覽和搜尋資料的目的。

如同我之前所說的,這種設計方式是受到了VTD-XML(VTD是指虛擬令牌描述符)的啟發,因此你也可以把這種解析器稱為虛擬令牌描述符解析器。但我還是傾向於索引覆蓋這個名字,因為它表現了虛擬令牌描述符的本質,即對原始資料建立的索引。

解析器設計概要

一種常規的解析器設計方式將解析過程分為兩步。第一步是將資料分解為內聚的令牌,一個令牌是已解析資料中的一個或多個位元組或字元。第二步是對令牌進行解釋,並根據這些令牌構建更大的元素。以下是這兩個步驟的圖示:

這裡的元素並不一定是指XML元素(雖然XML元素也是解析器元素),而是指構成解析資料的更大的“資料元素”。比如說,在一個XML文件中元素代表了XML元素,而在一個JSON文件中元素則代表了JSON物件,等等。

舉例來說,<myelement>這個字串可以被分解為以下幾個令牌:

  • <
  • myelement
  • >

一旦資料被分解為令牌,解析器就能夠相對容易地瞭解它的意義,並且決定這些令牌構成的更大的元素。解析器就能夠理解一個XML元素是由一個’<’令牌開始,隨後是一個字串(即元素名稱),隨後有可能是一些屬性,最後以一個’>’令牌結尾。

索引覆蓋解析器設計

在這種解析器的設計方式中也包含了兩個步驟:輸入資料首先被一個令牌生成器(tokenizer)元件分解為令牌,解析器隨後將對令牌進行解析,以決定輸入資料的一個更大的元素邊界。

你也可以為解析過程加入一個可選的“元素瀏覽步驟”。如果解析器從解析資料中構建出一棵物件樹,它通常會包含在整棵樹中進行瀏覽的連結。如果我們不選擇物件樹,而是構建出一個元素索引緩衝區,我們也許需要另一個元件以幫助資料處理程式碼在元素索引緩衝區中進行瀏覽。

以下是我們的解析器設計的概要:

我們首先將所有資料讀入一個資料緩衝區中,為了能夠通過在解析過程中建立的索引對原始資料進行隨機訪問,所有的原始資料必須已經存在於記憶體中。

第二步,令牌生成器會將資料分解為令牌。令牌生成器內部的某個令牌緩衝區會將該令牌的起點索引、終點索引和令牌型別都保留下來。使用令牌緩衝區使你能夠查詢之前或之後的令牌,在這種設計中解析器會利用到這一項特性。

第三步,解析器獲取了令牌生成器所產生的令牌,根據上下文對其進行驗證,並決定它所表示的元素。隨後解析器會根據從令牌生成器處獲取的令牌構建一個元素索引(即索引覆蓋)。解析器會從令牌生成器中一個接一個地獲取令牌。因此令牌生成器不必立即將所有資料都分解為令牌,它只需要每次找到一個令牌就行了。

資料處理程式碼將瀏覽整個元素緩衝區,利用它訪問原始資料。你也可以選擇用一個元素瀏覽元件將元素緩衝區包裝起來,使瀏覽元素緩衝區的工作更加簡單。

這種設計不會從解析資料中生成一棵物件樹,但它確實生成了一個可瀏覽的結構,即元素緩衝區,索引(即整數陣列)將指向包含了原始資料的資料緩衝區。你可以使用這些索引瀏覽原始資料緩衝區中的所有資料。

本文的以下部分將分析這種設計的各方面細節。

資料緩衝區

資料緩衝區是一個包括了原始資料的位元組字元緩衝區,而令牌緩衝區和元素緩衝區則包含了指向資料緩衝區的索引。

為了實現對解析資料的隨機訪問,必須以某種形式將它保留在記憶體中。我們在這裡沒有選擇物件樹,而是選擇了包含未處理資料本身的資料緩衝區。

將所有資料全部保留在記憶體中可能會導致對記憶體的大量消耗。如果你的資料包含了互相獨立的元素,例如日誌記錄,那麼將整個日誌檔案匯入記憶體很可能會造成崩潰。你應該採取的方式是隻匯入日誌檔案的一部分,其中至少包含一條完整的日誌記錄。由於每一條日誌記錄都可以不依賴於其它日誌記錄進行解析和處理,你就不需要將整個日誌檔案在同一時刻載入到記憶體裡了。我在我的文章《使用緩衝區對流進行迭代處理》中描述瞭如何對一塊資料流進行迭代的方式。

令牌生成器與令牌緩衝區

令牌生成器將資料緩衝區分解為令牌,令牌的資訊會儲存在令牌緩衝區中,包括以下資訊:

  • 令牌的位置(起始位置的索引)
  • 令牌長度
  • 令牌型別(可選資訊)

以上資訊都儲存在陣列中,這裡是一段示例程式碼:

   public class IndexBuffer {
       public int[]  position = null;
       public int[]  length   = null;
       public byte[] type     = null; 
     /* assuming a max of 256 types (1 byte / type) */
   }

當令牌生成器在資料緩衝區中找到令牌之後,它會將該位置(起始位置的索引)插入position陣列、將令牌長度插入length陣列,並將令牌型別插入type陣列。

如果你不使用這個可選的令牌型別陣列,你也可以在需要的時候通過令牌中的資料得出令牌的型別。這是一種效能與記憶體佔用之間的權衡。

解析器

解析器本質上與令牌生成器非常類似,不同的是它將令牌作為輸入,而將元素索引作為輸出。和令牌類似,每個元素由它的位置(起始位置的索引)、長度和可選的元素型別幾部分組成。用以儲存這些數字的結構與儲存令牌的結構是完全一樣的。

在這裡type陣列仍然是可選的。如果你能夠從元素的首個位元組或字元中很容易地判斷元素的型別,那就無需特意儲存元素的型別資訊。

在元素緩衝區中所包含的元素的精確粒度取決於被解析的資料,以及之後將對資料進行處理的程式碼段。舉例來說,如果你要實現一個XML解析器,你可能會選擇將每個開始標籤、屬性和結束標籤作為獨立的“解析元素”。

元素緩衝區(索引)

解析器所生成的元素緩衝區包含了引向原始資料的索引。這些索引會記錄解析器在資料中所找到的元素的位置(起始位置的索引)、長度和型別資訊。你可以利用這些索引實現在原始資料的任意瀏覽。

從之前的IndexBuffer程式碼段中,你可以看到元素緩衝區為每個元素保留了9個位元組的緩衝區,4個位元組用於儲存位置、另4個位元組用於儲存令牌長度,最後1個位元組用於儲存令牌型別。

你或許能夠通過某些手段來減少IndexBuffer的記憶體佔用。舉例來說,如果你確認其中的元素不超過65535個位元組,你就可以選擇使用short短整數,而不是常規的int整數來儲存令牌長度資訊,這樣每個元素都可以節省兩個位元組,將整個記憶體佔用減少至每個元素七個位元組。

此外,如果你確認被解析檔案的大小不會超過16,777,216個位元組,那你只需要三個位元組來儲存位置資訊(起始位置的索引)。那麼在position陣列中的每個整數的第四個位元組就可以用來儲存元素型別,這樣就可以完全不用使用單獨的type陣列了。如果你的令牌型別不超過128種,你就可以使用七個位元組、而不是八個位元組來儲存令牌型別,這樣一來你就可以使用25個位元來儲存位置,使得最大的位置可以達到33,554,432。如果你的令牌型別少於64種,你還可以空出一個位元以儲存位置資訊。

VTD-XML實際上將所有這些資訊都儲存在一個長整數型別中,以達到節省空間的目的。為了將幾個分離的欄位載入成為一個單獨的整數或者長整數,需要進行一些位元操作,也因此會降低一些速度,但好處是節省了部分記憶體,這就是一種資源的權衡。

元素Navigator

元素navigator可以幫助處理資料的程式碼在元素緩衝區中對資料任意瀏覽。請記住一個語義化的物件或元素(例如一個XML元素)或許會包含多個解析器元素。為了簡化瀏覽的實現,你可以建立一個元素navigator物件,讓它負責在語義化物件級別對解析器元素進行瀏覽的操作。舉例來說,XML元素navigator可以通過在開始標籤之間跳轉的方式實現對元素緩衝區的瀏覽。

是否使用元素navigator元件由你自行選擇,如果你只需要為某個單一的專案的某一個功能實現解析器,你也可以選擇不使用這種方式。但如果你希望實現的解析器能夠在多個專案中重用,或者是將它釋出為開原始碼,你或許需要新增一個元素navigator元件,這取決於對解析資料的瀏覽的複雜度有多高。

案例學習:一個JSON解析器

為了讓索引覆蓋解析器的設計更為直觀,我自己實現了一個基於Java的小型JSON解析器,它遵循了索引覆蓋解析器設計的方式,你可以在GitHub上找到它的完整程式碼。

JSON是JavaScript物件表示法的簡稱,它是在web服務端和客戶端瀏覽器之間通過AJAX進行資料交換的一種常見資料格式,這是因為web瀏覽器內建了將JSON轉換為JavaScript物件的原生支援。以下篇章中我會假設你已經熟悉JSON格式了。

這裡有一個簡單的JSON示例:

  {"key1":"value1","key2":"value2",["valueA":"valueB":"valueC"]}

JSON令牌生成器將JSON字串分別為以下令牌:

這裡的下劃線強調了每個令牌的長度。

令牌生成器還將決定每個令牌的基本型別,以下的JSON示例與之前的相同,只是加入了令牌的型別資訊:

請注意這裡的令牌型別並非語義化,它只是說明了令牌的基本型別是什麼,而並沒有體現出這些令牌包含了什麼內容。

解析器會分析出基本的令牌型別,並將它們替換為語義化的型別。這裡是一個相同的JSON示例,但使用了語義化的型別(即解析器元素):

當解析器完成了對該JSON物件的解析之後,你將獲得一個索引(即元素緩衝區),它由圖中所標註的元素的位置、長度和元素型別資訊所組成。接下來你就可以對該索引進行瀏覽,以找出該JSON物件中你所需的資料。

JsonTokenizer.parseToken()

為了讓你瞭解令牌生成器和解析工作是如何實現的,我會為你展示JsonTokenizer和JsonParser中的核心程式碼。請記得去Github下載完整的程式碼。

以下是JsonTokenizer.parseToken()方法的實現,它將負責解析資料緩衝區中的下一個令牌:

public void parseToken() {
     skipWhiteSpace();
     this.tokenLength = 0;

     this.tokenBuffer.position[this.tokenIndex] = this.dataPosition;
     char nextChar = this.dataBuffer.data[this.dataPosition];

     switch(nextChar) {
	 case '{'   :
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_CURLY_BRACKET_LEFT;
           break;
	 case '}'   :  
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_CURLY_BRACKET_RIGHT;
           break;
	 case '['   :
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_SQUARE_BRACKET_LEFT ;
           break;
	 case ']'   : 
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_SQUARE_BRACKET_RIGHT;
           break;
	 case ','   :
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_COMMA;
           break;
	 case ':'   :  
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_COLON;
           break;
	 case '"'   :
           parseStringToken();
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_STRING_TOKEN;
           break;
	 default    : 
           parseStringToken();
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_STRING_TOKEN;
     }
     this.tokenBuffer.length[this.tokenIndex] = this.tokenLength;
  }

如你所見,這部分程式碼非常簡單。我們第一步首先呼叫skipWhiteSpace()方法,它將忽略當前位置的資料中的空格字元。第二步是將令牌長度設為0。第三步,將當前令牌的位置(資料緩衝區中的相對位置)儲存在TokenBuffer中。第四步,對下一個字元進行分析,根據字元種類(即令牌種類)的不同,將執行switch—case結構中的某條語句。最後,將當前令牌的長度儲存起來。

以上就是為資料緩衝區生成令牌的全部工作了,請注意,當找到了某個字串令牌的開頭部分之後,令牌生成器就會呼叫parseStringToken()方法,它會對資料進行完整的掃描,直到找到了該字串令牌的結束為止。這種方式比起在parseToken()方法中進行各種條件判斷並處理各種不同情況執行得會更快,而且實現也更加容易。

JsonTokenizer中其餘的方法都是parseToken()的輔助方法,或者是將資料的位置移至下一個令牌(即當前令牌之後的第一個位置),等等。

JsonParser.parseObject()

JsonParser類的主要方法是parseObject(),它會檢查JsonTokenizer中令牌的型別,並嘗試在輸入資料中查詢該型別的JSON物件。

以下是parseObject()方法的實現:

private void parseObject(JsonTokenizer tokenizer) {
     assertHasMoreTokens(tokenizer);
     tokenizer.parseToken();
     assertThisTokenType(tokenizer, TokenTypes.JSON_CURLY_BRACKET_LEFT);
     setElementData     (tokenizer, ElementTypes.JSON_OBJECT_START);
     tokenizer.nextToken();
     tokenizer.parseToken();
     while( tokenizer.tokenType() != TokenTypes.JSON_CURLY_BRACKET_RIGHT) {                 
         assertThisTokenType(tokenizer, TokenTypes.JSON_STRING_TOKEN);
	 setElementData(tokenizer, ElementTypes.JSON_PROPERTY_NAME);          
         tokenizer.nextToken();
	 tokenizer.parseToken();
	 assertThisTokenType(tokenizer, TokenTypes.JSON_COLON);
	 tokenizer.nextToken();
	 tokenizer.parseToken();
	 if(tokenizer.tokenType() == TokenTypes.JSON_STRING_TOKEN) {             
             setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE);
	 } else if(tokenizer.tokenType() == TokenTypes.JSON_SQUARE_BRACKET_LEFT) {
	     parseArray(tokenizer);
	 }
         tokenizer.nextToken();
	 tokenizer.parseToken();
	 if(tokenizer.tokenType() == TokenTypes.JSON_COMMA) {
	     tokenizer.nextToken();  //skip , tokens if found here.             
             tokenizer.parseToken();
	 }
      }
      setElementData(tokenizer, ElementTypes.JSON_OBJECT_END);
  }

  private void setElementData(JsonTokenizer tokenizer, byte elementType) {
     this.elementBuffer.position[this.elementIndex] = tokenizer.tokenPosition();     
     this.elementBuffer.length  [this.elementIndex] = tokenizer.tokenLength();         
     this.elementBuffer.type    [this.elementIndex] = elementType;      
     this.elementIndex++;
  }

parseObject()方法能夠接受的資訊包括:一個左大括({)後接著一個字串令牌;或是一個逗號後跟著一個字串令牌;或是某個陣列的開始符號([);或是另一個JSON物件。當JsonParser從JsonTokenizer中獲得了這些令牌之後,就將它們的開始位置、長度和語義資訊儲存在它自己的elementBuffer欄位中。資料處理程式碼就可以隨後瀏覽elementBuffer中的資訊,從輸入資料中獲取所需的資料了。

看過了JsonTokenizer和JsonParser的核心程式碼部分之後,你應該對令牌生成器和解析器的工作方式有所瞭解了。如果要完整地瞭解程式碼的工作方式,你可能需要檢視JsonTokenizer和JsonParser的完整實現。它們的程式碼都不超過115行,理解它們應該不是難事。

效能基準測試

VTD-XML已經為它的XML解析器與StAX、SAX和DOM解析器進行過大量的效能基準比較測試了,從效能上來看VTD-XML無疑是最大的贏家。

為了讓使用者對索引覆蓋解析器的效能建立起信心,我也對我的JSON解析器實現與Google的JSON解析器——GSON,進行了效能對比。GSON的方式是從某個JSON輸入(字串或流)中建立一棵物件樹。

請記住,GSON是一個非常成熟的產品,品質優秀,經過了大量的測試,並且接受使用者的錯誤報告。而我的JSON解析器還只是處於概念產品的級別。這次測試僅僅是對效能的表現,這個結果也不代表最終的結論。也請注意閱讀該測試的相關討論。

這裡有一些關於構建該測試的具體細節:

  • 為了使JIT預熱以減少啟動時的負載,對該JSON的輸入解析一共執行了1千萬次。
  • 該測試一共對三個不同的檔案重複執行了相同的次數,以測試解析器解析小檔案、中等檔案和大檔案的效果。檔案的大小分別為64位元組、406位元組和1012位元組。因此測試的過程就是首先對小檔案進行1千萬次解析,並分析其結果,然後解析中等檔案並分析結果,最後是解析大檔案並分析結果。
  • 在解析和分析工作開始前,檔案已經全部載入到記憶體中,因此避免了將檔案載入的時間算到整個解析時間裡。
  • 對1千萬次解析的分析過程會在自己的程式中進行,這意味著每個檔案都在獨立的程式中進行解析,在每個時間點只有一個檔案在進行解析。
  • 每個檔案會進行3次分析,因此對檔案的1千萬次解析工作一共會進行3次,每1次的分析工作是順序進行的,而沒有采用並行方式。

測試結果表格包括以下三列:

  • 原始資料緩衝區的迭代數目
  • JSON解析器
  • GSON

第一列中的內容是原始資料緩衝區中的所有資料的迭代數目,這個數字僅僅是用以表示極限的最小時間,即理論上處理所有這些資料的最小時間。當然不可能有任何解析器能夠達到這一速度,不過這個數字能夠起到參照作用,以顯示出解析器和原始迭代速度的差距。第二列中顯示了我的JSON解析器的執行時間,第三列則是Google的GSON解析器的執行時間。

以下資料是對三個檔案(64位元組、406位元組、1012位元組)各執行1千萬次解析所需的毫秒數:

File Run Iteration JSON Parser GSON
Small 1 2341 69708 91190
Small 2 2342 70705 91308
Small 3 2331 68278 92752
Medium 1 13954 122769 314266
Medium 2 13963 131708 316395
Medium 3 13954 132277 323585
Big 1 33494 239614 606194
Big 2 33541 231866 612193
Big 3 32462 232951 618212

 

如你所見,索引覆蓋的實現比起GSON(一種物件JSON解析器)要快得多。雖然結果在預計之中,不過你現在能夠了解到它們的效能差距到底有多大了。

值得注意的一點是,在測試程式的執行過程中,記憶體佔用的指標一直非常穩定。儘管GSON建立了大量的物件樹,但它的記憶體佔用並沒有瘋狂地增長。而索引覆蓋方式的記憶體佔用也非常穩定,比起GSON還要小了1兆左右,這有可能是因為載入到JVM中的GSON程式碼庫較大的緣故。

關於測試結果

如果我們只是簡單地說對一個為資料建立物件樹的解析器(GSON)和一個標記出資料中所找到的元素位置的解析器進行比較,這種說法有欠公平。我們還需要分析一下具體比較了哪些內容。

在一個執行中的應用程式對檔案進行解析通常包含以下步驟:

首先從磁碟或者網路上載入資料,然後對資料進行解析,最後進行資料處理。

為了準確測量資料解析部分的速度,我將被解析的檔案預先載入入記憶體中,並且測試程式碼對資料完全不做任何處理。這種方式雖然測量了純粹的解析速度,但這一效能差別並不能代表在實際執行中的應用程式一定會獲得更好的效能,原因如下:

一個流解析器通常能夠在所有資料載入到記憶體之前就開始解析正在載入中的資料,而我的JSON解析器目前還沒有實現這一功能,這意味著雖然它在單純的解析速度上要快上一籌,但運用在實際執行中的應用程式上時,由於它必須等待所有資料載入完成,因此真實的完成速度不一定會更快。下圖就表現了這一過程:

為了加快整體的解析速度,你也可以對我的解析器進行一些修改,讓它能夠邊載入資料邊進行解析,不過這樣做也許會稍稍降低單純的解析效能。當然,最終的執行速度或者還是得到一些提升。

與上面的情況類似的是,我的JSON解析器對已解析的資料也沒有進行任何處理。如果你需要從大量的已解析資料中抽取字串,那麼GSON已經為你的需求做好了準備工作,因為它已經為已解析資料建立了一棵物件樹。下圖就表現了這一過程:

如果你打算使用GSON,那麼它或許已經為你實現了在資料處理中所需的資料抽取過程,如果整個資料處理過程可以省略資料抽取(例如抽取為字串)這一步驟,那麼它的整體速度還要再快一點。

因此,為了準確地測量解析器對你的應用程式的影響,你必須將不同的解析器在你的應用程式中的表現進行測量。我仍然確信使用索引覆蓋解析器的速度要更快,但具體有多少差距還不好說。

對索引覆蓋解析器的總體討論

我經常聽到一種關於索引覆蓋解析器的爭論,這種說法認為由於索引覆蓋解析器為了實現對原始資料的索引,而不是將原始資料抽取為物件樹,它在解析時必須將所有資料讀入記憶體中,這種方式在解析大檔案時會對記憶體產生很大的負擔。

這種說法其實就是表明了流解析器(例如SAX或StAX)能夠解析巨大的檔案,而不需要將整個檔案讀入記憶體中。但這種說法成立的前提是,該檔案中的資料可以分為多個小塊進行解析與處理,而且每個小塊可以獨立地被解析與處理。舉例來說,一個大XML檔案包含了一系列的元素,每個元素都可以進行獨立的解析和處理(類似於一個日誌記錄集合)。但如果你的資料可以以獨立的小塊進行分別解析的話,那麼你也完全可以實現一個能夠做到這一點的索引覆蓋解析器。

而如果該檔案不能夠分解為多個獨立的小塊進行解析的話,那無論如何你必須將資訊載入到某種結構中,以便程式碼在處理之後的小塊時訪問這一部分資訊。而如果你能夠在流解析器中做到這一點的話,那麼也同樣可以在一個索引覆蓋解析器做到這一點。

那些為輸入資料建立物件樹的解析器往往會佔用更大的記憶體,因為物件樹的記憶體佔用會超過原始資料的尺寸。其原因在於不僅每個物件例項會佔用內在,而且物件之間的引用也佔用了一部分記憶體資料。

此外,由於所有資料必須一次性全部載入到記憶體中,因此你需要預先為資料緩衝區預留足以儲存全部資料的空間。但如果在開始解析某個檔案的資料時,你還不知道整個檔案的大小,又該怎麼做呢?

假設你有一個允許使用者上傳檔案的web應用程式(或者是web service,或其它型別的服務端應用程式),你很難判斷這些檔案會有多大,那又如何能夠在開始解析之前為它們分配足夠大小的緩衝區呢?當然,出於安全性的考慮,你應該設定一個允許上傳檔案的最大尺寸,否則使用者可以通過上傳超大檔案使你的系統完全崩潰,或者編寫一段程式以模擬瀏覽器上傳檔案的操作,讓這段程式不停地向你的伺服器傳送資料。你可以考慮為緩衝區分配與允許上傳檔案的最大尺寸相同的值,這樣可以保證你的緩衝區對於有效的上傳不會佔用所有的記憶體。如果緩衝的尺寸真的過大,那一定是因為你的使用者上傳了超大的檔案。

 

相關文章