字符识别

对于字符处理有两个印象比较深刻的事儿。第一个就是在学校里面写个小应用,用Java读文件结果乱码了。第二个就是用ant做打包,结果在编译class的过程中老是报语法错误。结果原来是用记事本编辑了下,加了BOM所致。

一直认为从纸带时代到达字符时代是个伟大的跨越。可是如今纷繁的字符编码却让人觉得眼花缭乱,不知所从。最简单的做法便是一切UTF-8,标准。

前段时间公司内部正好搞字符培训,留了几个题目。刚好手头活儿不多,便利用闲暇时间做了下。边做边查wiki,总算对字符编码有了比较深刻的认识了。

首先了解了编码名字的意思。GB表示国家标准,CJK表示China+Japan+Korea(中日韩),UTF-8Unicode Transform)BIG5表示5个参与编码的IT巨头。

Unicode是标准的字符编码,Unicode收录了世界上几乎所有的字符,然后将每个字符与二进制值相对应。(http://zh.wikibooks.org/wiki/Unicode/6000-6FFF)


上图便展示了Unicode字符编码。问题是,有Unicode不是已经足够了?为什么还要GBK,UTF-8BIG5呢?

我觉得有几个因素。首先,在计算机由卡带向字符过渡的时候。收录世界上所有的字符,并将其编码比较困难。当时互联网也没有兴起,人们并没有意识到需要跨文化交流。所以那时基本上是每个国家把自己的所有字符整理出来,先自行编码。

当互联网兴起,人们有跨文化交流欲望的时候。Unicode联盟便站出来,将那些分散全世界的字符编码加以收录整合,才做出来标准的Unicode

Unicode编码工作完成只是解决了收录全世界字符并且给每个字符一个唯一id的任务。对Unicode进一步编码,便形成了UTF-8UTF-16UTF-32之类。

为什么要进一步编码?Unicode字符从0~e0fff。最大的字符\uE0FFF达到了2.5个字节,\u0则只需要半个字节。同样是一个字符,占用的字节数有多有少。如果碰到0x2d 0x3e 0xf7 0x44这一串byte,你该怎么解释。我可以解释成为0x2d,0x3e 0xf7,0x44,也可以解释成0x2d 0x3e,0xfe 0x44。两种解释,所取得的字符就不一样了。

也许你会说,既然目前Unicode最多只到2.5个字节。那么我学ipv6,冗余一下,预留几个扩展,每个字符编码4个字节不就得了。且慢,这样做并不好。首先,字符是大量出现的,冗余会造成很大的空间浪费。原先用UTF-8编码只需要2MB的文件,照这样一编码恐怕就需要4MB了。其次,ipv4-ipv6可以通过网络拓扑升级实现无痛升级,可如果你原来是4个字节一个字符,忽然来了外星文化,字符多了一倍,你发现不够使,想改到8个字节一个字符。你咋升级?以前的字符怎么兼容啊。最后,这样做还不利于定位。如果我想从第30个字节开始截取30个字符的文本。如果截取的不是头位置,那么后面所有的文本都会乱掉。

下面来看几个比较有意思的例子。


 

 

GBK编码的第7个字节是中的低字节位。如果从这里截断的话,会发生什么呢?


  

 

发现乱码了。其原因便是上面所述的断句错误产生的。

如果是UTF-8编码,从非法位置截断的结果又是这样子的。(因为只有收费版的winhex才可以自由选择编码格式,这里只能写个程序验证下了)

 

String string="2013年中国经济的增长目标是目前市场舆论的焦点";
byte[] utf8C=string.getBytes("utf8");
dumpCode(utf8C);
System.out.println(new String(Arrays.copyOfRange(utf8C, 6, utf8C.length),"utf8"));

 

 

6这个位置是年的中间位(汉字是3个字节,ascii1个字节)。看看截断后的结果吧

 

��国经济的增长目标是目前市场舆论的焦点

 

咦,虽然截断了。可是没有像GBK那样全部乱掉,而只是非法位置的那个字符才乱掉了。由此看来似乎UTF-8GBK还先进一点。

我们来参照一下GBK的编码规则(http://zh.wikipedia.org/wiki/GBK)。GBK对ASCII码做了兼容,1个字节的都是ASCII码,2个字节的是通用汉字字符。(所谓兼容,就是用GBK编码规则展示的ASCII文本,与ASCII编码展示的文本一模一样)。

注意2个字节的编码规则。第一位81-FE,第二位40-7E。结合上述的例子,D0开始的截断后,原本D6 D0,B9 FA(中国)变成了D0 B9,FA BE。事实上如果中间插入了明显的标志,GBK还是可以做一些修正的。例如观察到汉字字符无论高低位都不得小于40,那么如果这样子。

 

String string="2013年中国3经济的增长目标是目前市场舆论的焦点";
byte[] gbkC=string.getBytes("gbk");
dumpCode(gbkC);
System.out.println(new String(Arrays.copyOfRange(gbkC, 7, gbkC.length),"gbk"));

 

 

结果是:

 

 

泄�经济的增长目标是目前市场舆论的焦点

  

7的位置还是中的低字节位,但是注意“中国”后面加了个3。当识别0x33的时候,它不可能呆在汉字字符。此时GBK认识到了有问题,便做了修复。也就是说,我们错怪GBK了。它相比UTF-8来说并不是没有对错误位置的修复能力,只是它的能力比较弱而已!

看一下UTF-8的编码规则(http://zh.wikipedia.org/wiki/UTF-8#.E7.B7.A8.E5.AF.AB.E4.B8.8D.E8.89.AF.E7.9A.84.E8.A7.A3.E6.9E.90.E5.99.A8)UTF-8的字符范围从1个字节到4个字节。但是它的编码方式非常的规律整齐,高字节用开头1的数量标志后面低字节数目,低字节都是10开头。因此上面的截断程序中UTF-8非常快速的发现并修正了问题。

培训最后一道题目是用正则表达式识别文本属于什么编码格式。根据编码范围可以比较容易的写出来了。下面用强大的RegexBuddy来尝试一下

 

咦?怎么一个汉字也没有匹配到呢?\x匹配的是一个ASCII字符,本身只占用一个字节。而如果用GBK编码解析的话,"2013年中国"。本身10个字节被分割成了7个字符。怎么办?RegexBuddy还有强大的Hex模式,ctrl+H便可以进入。


 

hex模式下,果然完全匹配成功了。写个小例子来探测字符编码。

 

 

String gbkString=new String("2013年中国经济的增长目标是目前市场舆论的焦点");
String encodedGBK = new String(gbkString.getBytes("gbk"),"iso-8859-1");
dumpCode(encodedGBK.getBytes("iso-8859-1"));
		
String utf8String=new String("2013年中国经济的增长目标是目前市场舆论的焦点");
String encodedUTF8=new String(utf8String.getBytes("utf-8"),"iso-8859-1");
dumpCode(encodedUTF8.getBytes("iso-8859-1"));
		
System.out.println(PATTERN_GBK.matcher(encodedGBK).matches());
System.out.println(PATTERN_GBK.matcher(encodedUTF8).matches());
		
System.out.println(PATTERN_UTF8.matcher(encodedGBK).matches());
System.out.println(PATTERN_UTF8.matcher(encodedUTF8).matches());
 

UTF8GBK的正则如下。

 

 

final static Pattern PATTERN_UTF8=Pattern.compile("(" +
			"([\\x00-\\x7f])+" +
			"|([\\xc0-\\xdf][\\x80-\\xbf]{1})+" +
			"|([\\xe0-\\xef][\\x80-\\xbf]{2})+" +
			"|([\\xf0-\\xf7][\\x80-\\xbf]{3})+" +
			"|([\\xf8-\\xfb][\\x80-\\xbf]{4})+" +
			"|([\\xfc-\\xfd][\\x80-\\xbf]{5})+" +
			")+");
final static Pattern PATTERN_GBK=Pattern.compile("(" +
			"([\\x00-\\x7f])+" +
			"|([\\xa1-\\xa9][\\xa1-\\xfe])+" +
			"|([\\xb0-\\xf7][\\xa1-\\xfe])+" +
			"|([\\x81-\\xa0][\\x40-\\x7e\\x80\\xfe])+" +
			"|([\\xaa-\\xfe][\\x40-\\x7e\\x80\\xfe])+" +
			"|([\\xa8-\\xa9][\\x40-\\x7e\\x80\\xfe])+" +
				")+");

  

为什么要ISO-8859-1呢?其实还是\x的老问题,\x映射的是ascii字符。只有将String encode成一个ascii字符串才可以使用上述的二进制正则匹配。

题目只要求匹配UTF-8GBK的编码。如果是其它呢,比如UTF-16http://zh.wikipedia.org/wiki/UTF-16?根据编码范围紧接着我便写出了正则表达式,也匹配成功了。

 


 

 

 

可是当尝试用表达式匹配UTF-8GBK时,发现匹配也成功了。UTF-16的取值范围过大,编码特征不易识别。那么还有办法识别吗?我上Unicode的官方网站查找了一下。

(http://www.unicode.org/faq/utf_bom.html#bom1)官方网站给出了解释,UTF-16/UTF-32均支持BOM,通过特征的BOM序列可以比较容易的判断。

其实字节流,如果样本较少时。多种编码规则都可以匹配,此时判断字符编码也比较困难。BOM是个一劳永逸的终极解决方法。在Windows上面使用Notepad编辑后,可以看到它存储文件时总会在头部分加上一串BOM标识。可要注意的是,标准的UTF-8是不需要BOM的,而Windows下为了便于识别也会给UTF-8加上BOM。这也是当初我用Notepad编辑Java后编译失败的原因。

猜你喜欢

转载自sd6733531.iteye.com/blog/1742375