Gecko是一套网络排版引擎,由来已久,为当年大名鼎鼎的netscape网络浏览器流传而来,后面也成为了firefox浏览器,thunderbird等等软件的基础。详细的发展历程在这里就不展开作具体介绍了,读者能够自行查阅百度百科,维基百科等资料。php
在这一章咱们重点介绍一下gecko中是如何对全球各类不一样的网页文档的编码方式来作出识别和转换的。html
咱们知道,netscape或者firefox是面向全球用户的,而且,在互联网的世界,并无什么界限妨碍一个美国的用户访问中文或者日文的网页。因此,在这种场景下,浏览器是否能正确识别每一个地区的网页的编码格式,并正确地显示出来,就尤其重要了。算法
有一部分网页,可能会在html的标签中写上charset , 可是还有很是大的一部分网页,是缺乏这个信息的。因此,浏览器须要经过页面的数据内容,去猜想这个页面内容最有可能的编码是什么。数组
固然,有可能会猜错,因此用户若是看到乱码了,并且又知道页面是哪种编码,能够手动强制改变。浏览器
1:编码空间(coding scheme)网络
咱们知道,在多字节编码中,总会有一些码点(code point)是用不到的。若是咱们遇到若干个字节是不属于当前的编码,那么咱们能够立刻否认当前的编码方式。另外,某些编码拥有属于本身特定的编码特征,经过这种特性咱们也能够立刻肯定具体的编码。app
在gecko中,使用了一种状态机(Parallel State Machine)的算法来作这种检测。ide
在这个状态机中,存在三种状态:大数据
这个算法的思路主要是结合上一个字符的检测状态来判断当前字符的状态,也就是说他的状态改变是受上一个字符影响的。这对东亚文字的处理颇有用,由于东亚文字通常都是多字节的。编码
在任何一种语言的编码中,一些字符的出现方式每每会比其余的编码要更多一点,这种状况每每适合使用了特别多的码点来走编码的东亚文字,例如:中文,日文和韩文等。
有人分别就简体中文,繁体中文,日文,韩文作过专门的调查。最常用的字符每每分布在比较小的字符范围内,大部分字符使用频率较低。以下表所示:
以上数据代表使用频率高的字符每每分布在较小的字符范围内,并且高频字符足以说明该语言的语言特性。而且每种字符的码点分布是很稀疏的,从而大大减小了不一样编码之间重复交叉的范围。这就为区分不一样编码提供了一个比较有效的解决方案。
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