Gecko架構淺析之編碼檢測和轉換

一:前言簡介

Gecko是一套網絡排版引擎,由來已久,爲當年大名鼎鼎的netscape網絡瀏覽器流傳而來,後面也成爲了firefox瀏覽器,thunderbird等等軟件的基礎。詳細的發展歷程在這裏就不展開做具體介紹了,讀者可以自行查閱百度百科,維基百科等資料。

在這一章我們重點介紹一下gecko中是如何對全球各種不同的網頁文檔的編碼方式來做出識別和轉換的。

我們知道,netscape或者firefox是面向全球用戶的,並且,在互聯網的世界,並沒有什麼界限妨礙一個美國的用戶訪問中文或者日文的網頁。所以,在這種場景下,瀏覽器是否能正確識別每個地區的網頁的編碼格式,並正確地顯示出來,就尤爲重要了。

 

二:編碼檢測算法介紹

有一部分網頁,可能會在html的標籤中寫上charset , 但是還有非常大的一部分網頁,是缺少這個信息的。所以,瀏覽器需要通過頁面的數據內容,去猜測這個頁面內容最有可能的編碼是什麼。

當然,有可能會猜錯,所以用戶如果看到亂碼了,而且又知道頁面是哪一種編碼,可以手動強制改變。

編碼檢測方法三叉戟

1:編碼空間(coding scheme)

我們知道,在多字節編碼中,總會有一些碼點(code point)是用不到的。如果我們遇到若干個字節是不屬於當前的編碼,那麼我們可以馬上否定當前的編碼方式。另外,某些編碼擁有屬於自己特定的編碼特徵,通過這種特性我們也可以馬上確定具體的編碼。

在gecko中,使用了一種狀態機(Parallel State Machine)的算法來做這種檢測。

在這個狀態機中,存在三種狀態:

  1. eStart: 它表示符合當前編碼規則的第一個合法編碼已經檢測出來,後續的編碼也可能是符合要求的。
  2. eItsMe: 它表示當前字符不但符合當前編碼規則,而且是當前編碼類型特有的,那麼檢測結束。
  3. eError: 它表示檢測到了一個不符合當前編碼規則的字符。

這個算法的思路主要是結合上一個字符的檢測狀態來判斷當前字符的狀態,也就是說他的狀態改變是受上一個字符影響的。這對東亞文字的處理很有用,因爲東亞文字一般都是多字節的。

2:字符分佈情況分析(Character Distribution Method)

在任何一種語言的編碼中,一些字符的出現方式往往會比其他的編碼要更多一點,這種情況往往適合使用了特別多的碼點來走編碼的東亞文字,例如:中文,日文和韓文等。

有人分別就簡體中文,繁體中文,日文,韓文做過專門的調查。最經常使用的字符往往分佈在比較小的字符範圍內,大部分字符使用頻率較低。如下表所示:

以上數據表明使用頻率高的字符往往分佈在較小的字符範圍內,而且高頻字符足以說明該語言的語言特性。並且每種字符的碼點分佈是很稀疏的,從而大大減少了不同編碼之間重複交叉的範圍。這就爲區分不同編碼提供了一個比較有效的解決方案。

gecko基於這種分析使用了Confidence based的檢測方式,每一趟數據分發分析過程中,都會做兩次重要的檢查,一次是檢查符合當前編碼器編碼範圍的字符個數(mTotalChars)。另一個是檢查當前數據文件中落在使用頻率較高的字符集中的個數(mFreqChars)。爲減少不同編碼類型交叉帶來的干擾,使用頻率高的字符集也不能選的太大,上表顯示使用頻率最高的前512個字符幾乎涵蓋了每種編碼的大部分字符。而基於上述思想,每種字符集都被分成兩部分,頻繁使用的(frequence used)和非頻繁使用的(not frequence uesed).若一個字符分佈在前512個字符範圍內,它就是頻繁使用的。於是gecko中又使用了另一個概念Distribution Ratio,它指的是當前字符集中前512個字符的使用頻率與剩餘字符的使用頻率的比值。

例如如在一篇標準的用GB2312編碼的中文文檔中,前512個字符的使用頻率爲0.79135,後面的使用頻率爲(1- 0,79135),所以Distribution Ratio爲0.79135/(1- 0,79135)=3.79。這個值只是一個理想狀態,還不能用來求Confidence Level。欲求Confidence Level我們需要乘以一個常數,這個值我們叫做Typical Distribution Ratio,這個值因各種語言不同而各異,是一個經過分析各種語言的多份文檔後得出的一個經驗值。

基於以上分析及代碼中的展示,Confidence Level的定義如下:

float confidence = mFreqChars/ ((mTotalChars – mFreqChars) * mTypicalDistributionRatio

這樣,每一次編碼檢查,編碼檢測器都會將每個字符交付狀態機和分析器逐一掃描,直到遇到一個特有的字符,或者是將所有字符全部掃描完畢。最後,系統會從這些衆多掃描器中選擇一個Confidence Level最高的檢測器,並將其對應的編碼類型作爲最終結果。

3:2字節序列字符分佈法

這個方法專門用以檢測單字節編碼。

相對於多字節編碼檢測,單字節編碼檢測就變得容易的多了,它不需要狀態機和分發分析器。但由於衆多單字節編碼共享256個編碼空間,而且還要去除ASCII碼127個編碼空間,單純的字節範圍比對,很難精確的區分西文字符。

關於這個問題gecko的開發人員進行了大量的分析,提出了2-Char Sequence的概念,即它不是用單個字節作爲考量字符編碼的單位,而是以兩個字節爲單位考量編碼類型。研究人員發現,在西文字符集中有很多字符經常以成對的形式出現,而且這種比例也比較高。而不同語言之間這種成對的字符交叉率顯然很低。這顯然爲這些編碼的檢測提供了一種解決方案。

有人曾經下載20M的俄語純文本文件,然後寫代碼研究這段文件,總共發現了21,199,528個 2-Char sequence,除去space-space組合的垃圾數據外,剩下的20,134, 122 個2-Char sequence佔據了所有序列的95%,這些可以用來構建語言模型的序列可以被分成4096個序列,在21,199,528個 2-Char sequence中有1961個序列出現的概率明顯偏低。我們把這1961個序列叫做我們語言中的Negative Sequence。

gecko的單字節編碼檢測方案就是基於這個實驗結論,併爲每一個編碼檢查定義了一個語言模型(SequenceModel)用來描述這種2-Char Sequence。 每一個SequenceModel中都定義了一個256*256的二位矩陣,用來映射每兩個字符組合對應的等級,共有4個等級,0代表Nagative Sequence,3代表Positive Sequence,其餘爲中間階層;每一個SequenceModeld都有一個mTypicalPositiveRatio用來描述Positive Sequece在所有Sequece中的比例;每一個編碼檢測器又爲這種等級劃分定義了一個數組PRUint32 mSeqCounters[NUMBER_OF_SEQ_CAT]用來記錄每一個等級的2-Char sequence在被檢查文本中出現的次數。

有了上述理論的鋪墊,單字節編碼檢查就變得容易的多了,每掃描一個字符總會結合上一次掃描過的字符做2-Char Sequence序列檢查,並把其出現次數記錄在對應的mSeqCounters中。

同多字節編碼檢測一樣,單字節編碼檢測也是confidence based。不同於多字節檢測的是,計算方式有所改變。若定義了NEGATIVE_APPROACH,它的計算式爲:

((float)(mTotalSeqs – mSeqCounters[NEGATIVE_CAT]*10))/mTotalSeqs * mFreqChar / mTotalChar;

若未定義,它的計算式爲:

r = ((float)1.0) * mSeqCounters[POSITIVE_CAT] / mTotalSeqs / mModel->mTypicalPositiveRatio;

r    =   r*mFreqChar/mTotalChar;

最終依然是選取confidence level最高的單字節編碼檢測器對應的編碼作爲最終的結果。

對於一段文本輸入,我們不知道它的編碼類型,那麼我們就要把文件數據交付所有的多字節檢測器和單字節檢測器,最終找到confidence level最高的作爲結果。當然這只是gecko編碼檢測的中心思想,詳細過程還要更復雜一些,比如對Unicode系列的檢測,它會考慮BOM的因素等。

4.編碼檢測方案結構

圖四.編碼檢測結構圖

編碼檢測方案中,編碼檢測方式總體上可以分爲四類:多字節檢測,單字節檢測,EscCharSet檢測及Latin1檢測。

多字節檢測室基於狀態機和分發器的。每獲取一個字符輸入都會交付狀態機進行判斷,若返回狀態是eItsMe,檢測就結束,否則交付分發器統計高頻字符和符合當前編碼集的字符數。

單字節檢測主要是基於語言模型的。每獲取一個字符輸入,它都會結合上一次的輸入到語言模型中查詢確定其是否是高頻的2-Char Sequence,並統計高頻的個數。

EscCharProber主要是針對HZ,ISO-2022系列的,他們有一個明顯的特徵,就是數據中存在ESC字符或者”~{”字符(待進一步研究)

由於Latin1字符的匹配率較高,這裏單獨處理它。最終確定confidence的時候,它會主動降低50%,以給其他編碼提供機會。

三. 一次編碼檢測過程

1. nsUniversalDetector首先根據判斷是否有當前數據是否有BOM,若有,直接根據BOM判定當前編碼的類型。若無,就遍歷一遍當前數據判斷當前數據應該使用的編碼檢測組。

2. 編碼檢測組將輸入的數據交付給每一個編碼檢測器。若檢測過程中遇到eItsMe狀態,結束所有檢測,返回結果。

3. 調用nsUniversalDetector的DataEnd方法獲取編碼類型。若獲取失敗,nsUniversalDetector會統計每一個檢測器的confidence值,選取confidence最大的檢測器對應的編碼類型返回。

四.總結

在gecko的編碼檢測中,對於以上三種方法是結合起來使用的,在實際的使用過程中,還是收到了很好的效果。

我們可以看到,無論單字節編碼還是多字節編碼的檢查,都加入了語言特性,通過語言特性來彌補編碼檢測能力的不足。兩者相互配合,還是比較完美,對所有大數據文件基本上都能給出正確的結論。

在實際的應用中,

1:Positive Sequence或大概率字符集出現的頻率越高檢測越準確。

2:字符越多且重複率不高時檢測越準確

編碼轉換

在gecko中,所有的編碼都是基於Unicode的,所有的其他編碼最終都會轉換成Unicode。所以在gecko中,通常把轉換成Unicode的過程叫做Decode,逆向轉換爲自身的過程叫做Encode。

  1. 1. 基於表查詢的編碼方案

我們知道,當前主流的編碼從字節長度上主要分爲如下幾種:

  1. 單字節編碼(Latin1,KIO8-R等西文字符)
  2. 雙字節編碼(東亞文字(中文GB除外))
  3. 變長編碼(Unicode,UTF-8,GB18030等)

而同一個字符在不同編碼方案中的編碼可能是不一樣的;在同一編碼方案中編碼連續的兩個字符在另一種編碼方案中未必是連續的。基於此,想通過一種直接的數學轉換來解決所有編碼之間的轉換是相當困難的。

所以gecko使用了基於表查詢的編碼轉換解決方案,每次編碼轉化都會基於一張或多張表完成。它的基本規則如下:

每種編碼的最大查詢表的個數由第一個字節的區段決定的,如EUC-KR每個字符的首字節的區段範圍是,{ 0×00, 0×7E },{ 0xA4, 0xA4 },{ 0xA1, 0xFE },{ 0xA1, 0xC6 },{ 0×80, 0xA0 },那麼EUC-KR向Unicode轉換就會基於5個表來完成。

圖4,查詢表結構示意圖

每一個查詢表的結構如上圖所示,其中每個字段的意義是:

itemOfList: 查詢區間的個數,這裏我還理解爲處理策略的個數。

offsetToFormatArray: FormatArray數組相對於查詢表的偏移位置,在表查詢方案中,每個查詢區間都對應了一組處理策略,通過這個formatArray可以確定一個特定字符的處理策略。

offsetToMapCellArray: uMapCell數組相對於查詢表的偏移位置,uMapCell可以認爲是查詢區間的規則說明,它定義了通過各種format查詢表時的查詢結構(詳細見uMapCell的定義)。

offsetToMappingTable: 他定義了被查詢數據相對查詢表的偏移量。被查詢數據可能是一個區間,也可能映射到一個具體的編碼。具體的使用方式是由format所對應的映射方法確定的。

gecko編碼轉換的主體過程,基本上是結合這個表來完成的。每獲取一個新的字符輸入,它都會查找該字符對應區段的uScanClassID (uScanClassID的定義見intl/uconv/util/uconvutil.h),並結合該id找到相應的處理方法,該處理方法會以當前字節開頭的字符按字節順序組裝成一個16位的數值med,這個值主要用來映射原始字符的查詢範圍,我們可以通過med值方便的確定它的偏移量,format等,然後找到對應的uMapCell及format,交付uMapFormate*映射出具體的編碼。一次典型的編碼轉換過程如下圖所示:

  1. 每獲取一個字節輸入字轉換器都會查詢RanageArray,確定以該字節開頭的字符使用哪個表去查詢。
  2. 將該字符交付uScan方法,構建十六位的數值med。通常Unicode逆向轉換的時候會省去這一步驟,因爲通常Unicode一個字符已經是十六位的了,med值跟原始字符時一樣的。
  3. 使用該med值調用uMapCode方法去映射具體的轉換後的編碼。

通過med值確定具體的format.這一過程由uGetFormat,uGetMapCell,uHit三個方法或宏協調完成的,每一個format都對應了一組處理方法,包括對med值區間的判定方式及後面的Mapping方式等。

調用uMap方法去映射具體的編碼值。該調用依然會使用format值確定Mapping方法。不同format值映射方式是不一樣的。

這是gecko中一次典型的編碼轉換過程,當然具體到每個轉換器又有所不同,比如GB18030轉換成Unicode的時候,它會直接根據當前字節序直接查找一個映射表,只有遇到四字節編碼或者當前表中查詢不到的字符時交付新的轉換器處理。

by panyunhong

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章