Gecko是一套网络排版引擎,由来已久,为当年大名鼎鼎的netscape网络浏览器流传而来,后面也成为了firefox浏览器,thunderbird等等软件的基础。详细的发展历程在这里就不展开做具体介绍了,读者可以自行查阅百度百科,维基百科等资料。
在这一章我们重点介绍一下gecko中是如何对全球各种不同的网页文档的编码方式来做出识别和转换的。
我们知道,netscape或者firefox是面向全球用户的,并且,在互联网的世界,并没有什么界限妨碍一个美国的用户访问中文或者日文的网页。所以,在这种场景下,浏览器是否能正确识别每个地区的网页的编码格式,并正确地显示出来,就尤为重要了。
有一部分网页,可能会在html的标签中写上charset , 但是还有非常大的一部分网页,是缺少这个信息的。所以,浏览器需要通过页面的数据内容,去猜测这个页面内容最有可能的编码是什么。
当然,有可能会猜错,所以用户如果看到乱码了,而且又知道页面是哪一种编码,可以手动强制改变。
1:编码空间(coding scheme)
我们知道,在多字节编码中,总会有一些码点(code point)是用不到的。如果我们遇到若干个字节是不属于当前的编码,那么我们可以马上否定当前的编码方式。另外,某些编码拥有属于自己特定的编码特征,通过这种特性我们也可以马上确定具体的编码。
在gecko中,使用了一种状态机(Parallel State Machine)的算法来做这种检测。
在这个状态机中,存在三种状态:
这个算法的思路主要是结合上一个字符的检测状态来判断当前字符的状态,也就是说他的状态改变是受上一个字符影响的。这对东亚文字的处理很有用,因为东亚文字一般都是多字节的。
在任何一种语言的编码中,一些字符的出现方式往往会比其他的编码要更多一点,这种情况往往适合使用了特别多的码点来走编码的东亚文字,例如:中文,日文和韩文等。
有人分别就简体中文,繁体中文,日文,韩文做过专门的调查。最经常使用的字符往往分布在比较小的字符范围内,大部分字符使用频率较低。如下表所示:
以上数据表明使用频率高的字符往往分布在较小的字符范围内,而且高频字符足以说明该语言的语言特性。并且每种字符的码点分布是很稀疏的,从而大大减少了不同编码之间重复交叉的范围。这就为区分不同编码提供了一个比较有效的解决方案。
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最高的检测器,并将其对应的编码类型作为最终结果。
这个方法专门用以检测单字节编码。
相对于多字节编码检测,单字节编码检测就变得容易的多了,它不需要状态机和分发分析器。但由于众多单字节编码共享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的因素等。
图四.编码检测结构图
编码检测方案中,编码检测方式总体上可以分为四类:多字节检测,单字节检测,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。
我们知道,当前主流的编码从字节长度上主要分为如下几种:
而同一个字符在不同编码方案中的编码可能是不一样的;在同一编码方案中编码连续的两个字符在另一种编码方案中未必是连续的。基于此,想通过一种直接的数学转换来解决所有编码之间的转换是相当困难的。
所以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*映射出具体的编码。一次典型的编码转换过程如下图所示:
通过med值确定具体的format.这一过程由uGetFormat,uGetMapCell,uHit三个方法或宏协调完成的,每一个format都对应了一组处理方法,包括对med值区间的判定方式及后面的Mapping方式等。
调用uMap方法去映射具体的编码值。该调用依然会使用format值确定Mapping方法。不同format值映射方式是不一样的。
这是gecko中一次典型的编码转换过程,当然具体到每个转换器又有所不同,比如GB18030转换成Unicode的时候,它会直接根据当前字节序直接查找一个映射表,只有遇到四字节编码或者当前表中查询不到的字符时交付新的转换器处理。
by panyunhong