Character Encoding

以前收藏的一篇

标题 谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词 选择自 fmddlmyy 的 Blog
关键字 谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词

这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级。整理这篇文章的动机是两个问题:

问题一:
使用Windows记事本的“另存为”,可以在GBK、Unicode、Unicode big endian和UTF-8这几种编码方式间相互转换。同样是txt文件,Windows是怎样识别编码方式的呢?

我很早前就发现Unicode、Unicode big endian和UTF-8编码的txt文件的开头会多出几个字节,分别是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但这些标记是基于什么标准呢?

问题二:
最近在网上看到一个ConvertUTF.c,实现了UTF-32、UTF-16和UTF-8这三种编码方式的相互转换。对于Unicode(UCS2)、GBK、UTF-8这些编码方式,我原来就了解。但这个程序让我有些糊涂,想不起来UTF-16和UCS2有什么关系。
查了查相关资料,总算将这些问题弄清楚了,顺带也了解了一些Unicode的细节。写成一篇文章,送给有过类似疑问的朋友。本文在写作时尽量做到通俗易懂,但要求读者知道什么是字节,什么是十六进制。

0、big endian和little endian
big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。

“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

1、字符编码、内码,顺带介绍汉字编码
字符必须编码后才能被计算机处理。计算机使用的缺省编码方式就是计算机的内码。早期的计算机使用7位的ASCII编码,为了处理汉字,程序员设计了用于简体中文的GB2312和用于繁体中文的big5。

GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。

从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS)。

有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常我们还是用GBK指代中文Windows内码。

这里还有一些细节:

GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0。

在DBCS中,GB内码的存储格式始终是big endian,即高位在前。

GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBK和GB18030的低字节最高位都可能不是1。不过这不影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。

2、Unicode、UCS和UTF
前面提到从ASCII、GB2312、GBK到GB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(更准确地说,是与ISO-8859-1兼容),与GB码不兼容。例如“汉”字的Unicode编码是6C49,而GB码是BABA。

Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。

根据维基百科全书( http://zh.wikipedia.org/wiki/)的记载:历史上存在两个试图独立设计Unicode的组织,即国际标准化组织(ISO)和一个软件制造商的协会(unicode.org)。ISO开发了ISO 10646项目,Unicode协会开发了Unicode项目。

在1991年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode2.0开始,Unicode项目采用了与ISO 10646-1相同的字库和字码。

目前两个项目仍都存在,并独立地公布各自的标准。Unicode协会现在的最新版本是2005年的Unicode 4.1.0。ISO的最新标准是10646-3:2003。

UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-7、UTF-16。

IETF的RFC2781和RFC3629以RFC的一贯风格,清晰、明快又不失严谨地描述了UTF-16和UTF-8的编码方法。我总是记不得IETF是Internet Engineering Task Force的缩写。但IETF负责维护的RFC是Internet上一切规范的基础。

3、UCS-2、UCS-4、BMP

UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。下面让我们做一些简单的数学游戏:

UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。

UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行 (rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。

group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。

将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。

4、UTF编码

UTF-8就是以8位为单元对UCS进行编码。从UCS-2到UTF-8的编码方式如下:

UCS-2编码(16进制) UTF-8 字节流(二进制)
0000 - 007F 0xxxxxxx
0080 - 07FF 110xxxxx 10xxxxxx
0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx

例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

读者可以用记事本测试一下我们的编码是否正确。

UTF-16以16位为单元对UCS进行编码。对于小于0x10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0x10000的UCS码,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0x10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同。但UCS-2只是一个编码方案,UTF-16却要用于实际的传输,所以就不得不考虑字节序的问题。

5、UTF的字节序和BOM
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法:

在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。

Windows就是使用BOM来标记文本文件的编码方式的。

6、进一步的参考资料
本文主要参考的资料是 "Short overview of ISO-IEC 10646 and Unicode" ( http://www.nada.kth.se/i18n/ucs/unicode-iso10646-oview.html)。

我还找了两篇看上去不错的资料,不过因为我开始的疑问都找到了答案,所以就没有看:

"Understanding Unicode A general introduction to the Unicode Standard" ( http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a)
"Character set encoding basics Understanding character set encodings and legacy encodings" ( http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter03)
我写过UTF-8、UCS-2、GBK相互转换的软件包,包括使用Windows API和不使用Windows API的版本。以后有时间的话,我会整理一下放到我的个人主页上( http://fmddlmyy.home4u.china.com/)。

我是想清楚所有问题后才开始写这篇文章的,原以为一会儿就能写好。没想到考虑措辞和查证细节花费了很长时间,竟然从下午1:30写到9:00。希望有读者能从中受益。
//===========2===============
Unicode的最初目标,是用1个16位的编码来为超过65000字符提供映射。但这还不够,它不能覆盖全部历史上的文字,也不能解决传输的问题(implantation head-ache's),尤其在那些基于网络的应用中。已有的软件必须做大量的工作来程序16位的数据。
因此,Unicode用一些基本的保留字符制定了三套编码方式。它们分别是UTF-8,UTF-16和UTF-32。正如名字所示,在UTF-8中,字符是以8位序列来编码的,用一个或几个字节来表示一个字符。这种方式的最大好处,是UTF-8保留了ASCII字符的编码做为它的一部分,例如,在UTF-8和ASCII中,“A”的编码都是0x41.
UTF-16和UTF-32分别是Unicode的16位和32位编码方式。考虑到最初的目的,通常说的Unicode就是指UTF-16。在讨论Unicode时,搞清楚哪种编码方式非常重要。Unicdoe相关的技术介绍参见 http://www.unicode.org/unicode/standard/principles.html.
 
//===========3===============
 几年前,我在初次接触Unicode时学习过一段时间的编码,当时解决了问题就没有继续下去,我记得当时遗留下来的一个问题就是UTF-8到底是怎样一种编码,和Unicode有什么区别?为什么有了Unicode还要有UTF-8?
    最近又遇到了UTF-8的问题,因此我决定就此机会好好学习一下Unicode相关的知识。
    从前在编程的时候我都不用Unicode字符集的,好像VC6对工程的默认设置是使用多字节字符集。记得那个时候如果要在VC6里使用Unicode字符集,好像C++程序的入口点还要在工程属性里进行特别的设置。
    最近我彻底抛弃了多字节字符集,不仅因为VS2005工程的默认字符集是Unicode,而且如果使用Unicode编译出来的GUI应用程序,当XP的主题变化时,控件的外观也会跟随改变;而如果使用多字节字符集,为了让程序的外观适应主题变化,则必须另附一个manifest文件。不用写一行代码就可以达到这个效果,我只用脚趾头想了一下就决定从此在所有工程里采用Unicode了:)
   
    ASCII字符集
    7位的编码方案,总共表示128个字符,其中包括了大小写英文字母、数字、标点符号等常用字符。英语世界已经足够应付。
   
    ISO-8859-1字符集

    也称ISO-Latin字符集,它扩展了ASCII字符集,用到了8bit字节里的最高一位,这样它就有256个字符,前128个字符和ASCII字符集相同。有了ISO-Latin字符集,西方世界的一些其它语言,如西班牙语、法语、德语、意大利语都够用了。

    GB系列字符集(GB2312,GBK,GB18030)
    由于一个字节是无论如何也表达不了哪怕是最长用的汉字字符集的,所以为了用计算机存储汉字,必须使用多个字节。
    多字节字符集就是使用可变长的编码长度来编码字符,有的字符用一个字节编码,比如ASCII字符,有的字符用两个字节编码,比如汉字。在VC里,多字节字符集等同于双字节字符集,VC不支持多于2个字节编码长度的字符。GB系列的字符集和ISO-Latin字符集一样,前128个字符和ASCII字符集相同。GB系列字符集是兼容的,相同的中文字符在这3个字符集里有相同的编码。GB2312和GBK一个字符最多2个字节表示,GB18030可多达4个字节。在这种编码里表示汉字时,需要一个leading byte,它总是大于127,这个字节的含义是说明它和后面的字节(们)一起表示一个字符。
    这些字符集(ISO-Latin字符集,GB系列字符集)都是以ASCII为基础扩展而来,统称为ANSI字符集。
    记事本在默认情况下(选择ANSI编码)就是使用多字节字符集保存文件的,至于使用的是GB2312,GBK,还是GB18030我不清楚。
   
    Unicode字符集:
    每个地区的人都试图扩展ASCII编码来支持本地的语言,最终的结果是导致互不兼容。因为除了最低的128个字符相同以外,其它的字符都使用自己特殊的编码方案。
    当使用与文件保存时的编码方案不同的编码来读取文件时,就会产生错误——比如Windows记事本那个著名的“联通BUG”。
    统一所有字符的编码是Unicode被设计出来的初衷。
    长久以来,Unicode在我心中的概念就是:使用2个字节来编码字符,使用Unicode可以表示世界上所有的字符。但这种理解并不准确!
    其实Unicode可以看成是一种理想:这种理想就是世界上的所有字符都只有一个唯一的标识!至于怎样去实现这种理想,有很多的实现方式:UTF-8,UTF-16,UTF-32,甚至在Unicode标准里还介绍了一种压缩的实现方式。Unicode把这个唯一的标识称之为代码点(code point),字符的代码点以U+XXXX的方式表示,这个可以打开Windows自带的字符映射表看得到。
    Unicode最初被设计出来的时候希望使用2个字节就可以表示世界上的所有字符。因此,实现Unicode最直接的想法就是用两个字节来存储一个字符,如果大家都这么想就好了,这样一个字符就可以用2个字节长的短整形来存储。但是偏偏还有一个叫做大端小端东西存在,这样2个字节的短整型在内存中的表示顺序就有2种可能,这就是为什么当用记事本保存文本文件时可以选择Unicode或者Unicode big endian的原因。
    1个字符=2个字节在现实中却遇到了麻烦。一方面,用2个字节表示一个字符,浪费了大量的空间(如果仅仅用来存储ISO-Latin字符集里的字符的话),而且还会有大端小端的问题,解决的方案是UTF-8编码;另一方面,人们在实践中发现即使用2个字节编码也无法表示所有字符,因此出现了UTF-16。UTF-16除了使用2个字节编码外,还使用一对2个字节来表示Unicode里很少用到的字符;另外还有UTF-32,它使用单独的4个字节来编码所有的Unicode字符。
   
    UTF-8编码

    我想最早提出UTF-8的一定是美国人,“用2个字节来表示一个英语字母这太浪费了!”,他们肯定会这么说的。顾名思义,那个8说明UTF-8编码中最小的单位是8bit的字节。采用UTF-8编码,Unicode代码点中U+007F以下(包含U+007F)的字符用一个字节编码,其它的字符用多个字节编码,最多一个字符用4个字节编码。这样UTF-8兼容ASCII,但是不兼容ISO-Latin字符集。

    Unicode字符集采用UTF-8编码方案时的对照表:
   
Unicode代码点区间UTF-8编码后的结果
U-00000000 - U-0000007F0xxxxxxx
U-00000080 - U-000007FF110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

    网上的很多文章都有这个表,可以看到一个Unicode代码点采用UTF-8编码时最多可达6个字节。但是从Unicode官方网站上看到的是UTF-8编码的最大字节长度是4个字节。也就是说最下面的两行没有了。并且第四行的范围是从U-00010000到U-00110000。Unicode协会说它们对UTF-8编码的改进工作进行地很快,看来网上的那些文章早就过时了。
    UTF-8编码的实现方式比较好理解:例如“汉”字的Unicode编码是6C49,6C49在0800-FFFF之间,所以最终编码应该是3个字节。6C49的二进制位串是:110110001001001,把这个位串从右向左填充到那3个字节的x部分,高位不够的用0补。最终得到的3个字节是:11100110 10110001 10001001,即E6 B1 89。注意由于UTF-8的最小编码单元是字节,所以不存在大端小端的问题。在各种 Unicode编码方案之间转换的标准算法(诸如从UTF-16到UTF-8或者反过来)已经有了,在Unicode的官方网站上可以找到。
       
    这样Unicode至少就有5种编码方案了(UTF-8,UTF-16两种,UTF-32两种),怎么区分它们呢?
    区分各种不同Unicode编码方案的技巧被称为Byte Order Mark(BOM)。将BOM插入到文件的开头,应用程序就能知道接下来应该使用哪种编码来解析文本了,以ANSI编码保存的文件没有BOM。
Byte order mark Description
EF BB BFUTF-8
FF FEUTF-16, little endian(VC的Unicode用的就是这种格式)
FE FFUTF-16, big endian
FF FE 00 00UTF-32, little endian
00 00 FE FF00 00 FE FF

    关于Unicode编程:
    当然最常见的问题是多字节字符集,与Unicode的各种编码之间怎么转换。Unicode与UTF8之间有一一对应的关系,它们之间可以直接相互转换,多字节字符集和UTF8之间的转换得经过Unicode中转。
    MBCS--Unicode--UTF8
    UTF8--Unicode--MBCS
    在Windows平台上,进行转换的函数是WideCharToMultiByte和MultiByteToWideChar。这样如果要将MBCS转换成UTF8,先调用MultiByteToWideChar,使用CP_ACP代码页(默认的代码页),然后调用WideCharToMultiByte,这次用CP_UTF8作代码页。

    解释一下记事本的那个"联通"的BUG
    当以ANSI编码保存联通两个字时,文件里的内容如下:
    c1 aa cd a8
    1100 0001 1010 1010 1100 1101 1010 1000
    记事本在打开文件的时候会去猜测文本文件的编码方式。由于第一二个字节、第三四个字节的起始部分的都是"110"和"10",正好与UTF8规则里的两字节模板是一致的,记事本猜测文件是以UTF-8的编码方式保存的,所以不能正确的显示。
    而如果使用记事本的打开菜单,强制以ANSI编码的方式打开文件则能够正确得显示联通两个字。而如果你在"联通"之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。
    奇怪的是:记事本为什么没有使用BOM呢?使用ANSI编码存储的文本文件是没有BOM的啊!既然没有BOM,它应该以ANSI编码读取文件才合理啊!只能解释说记事本在很久以前就在使用“猜”技术来揣度文件的编码了。

    关于编码的一些资源:
    CSDN的blog上有两篇相当不错的文章, 汉字编码及相关问题(unicode,ansi,gb2312)(补充阅读)关于编码: ascii(ansi), gb-2312, unicode, utf8 。
    joelonsoftware上有篇写得浅显易懂的有关字符集的文章: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
    如果你想知道所有关于Unicode的细节, 官方网站是最权威的地方。关于 Unicode的一些FAQ可以很快了解一些常识。
 


作者Blog: http://blog.csdn.net/fmddlmyy/
参考资料: http://blog.csdn.net/fmddlmyy/

转载于:https://www.cnblogs.com/devilmsg/archive/2008/09/02/1282095.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值