摘要:本文从Unicode入手,介绍由于通信问题而产生的字符集,以及Unicode的发展情况。介绍各种字符集的及其使用。并适时的介绍一些历史情况,主要讨论字符集在java及C语言环境中的使用,及阐述UTF,ISO 8859-1,ASCII他们之间的关系。会介绍一些乱码知识,总而言之,乱码产生的根本原因就是:编码与解码不一致造成的。
一、概念:
1、BCD码
最初的计算机性能和存储容量都比较差,所以普遍采用4位BCD(BinaryCoded Decimal)编码(这个编码出现比计算机还早,最早是用在打孔卡上的)。BCD编码简单点说就是将十进制数用二进制表示,如下图所示。
十进制数 | 8421BCD编码 |
---|---|
0 | 0000 |
1 | 0001 |
2 | 0010 |
3 | 0011 |
4 | 0100 |
5 | 0101 |
6 | 0110 |
7 | 0111 |
8 | 1000 |
9 | 1001 |
BCD编码表示数字还可以,但表示字母或符号就很不好用,需要用多个编码来表示。后来经过演变发展成了ASCII码。ASCII含33(ASCII码范围为:0~31和127)个控制字符, 和95(ASCII码范围32~126)个可显示字符。
2、由ASCII码发展到Unicode
ASCII编码存储方式:
其中最高位0,其余七位为0或1,可表示的范围为:0 ~ 2^7= 0 ~ 128
C语言实现打印字符A
# include <stdio.h>
int main()
{
char ch = '65';
printf("%c", ch);
return 0;
}
下图为ASCII码表
后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称"扩展字符集"。从此之后,贪婪的人类再没有新的状态可以用了。
等中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
中国人民看到这样很不错,于是就把这种汉字方案叫做"GB2312"。GB2312
是对ASCII
的中文扩展。
但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家领导人。于是我们不得不继续把 GB2312
没有用到的代码点找出来老实不客气地用上。
后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK
标准,GBK
包括了GB2312
的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK
扩成了 GB18030
。从此之后,中华民族的文化就可以在计算机时代中传承了。
由于世界各地都产生了自己的编码方案,这是给人的沟通带来了巨大麻烦。于是有一个叫做ISO的国际组织开始着手解决这个问题,想用一种规范来表示出所有的语言。于是Unicode就这样产生了。注意:Unicode是内存编码表示方案(是规范),而UTF是如何保存和传输Unicode的方案(是实现)这也是UTF与Unicode的区别。
字符是各种文字和符号的总称,包括各个国家文字、标点符号、图形符号、数字等。字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集有:ASCII字符集、ISO 8859字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。
各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。
3、ISO 8859-1
ISO/IEC8859-1
,又称Latin-1
或“西欧语言”,是国际标准化组织内ISO/IEC 885
9的第一个8位字符集。它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入96个字母及符号,藉以供使用变音符号的拉丁字母语言使用。iOS 8859-1
表示的字符就是Unicode的0x0000-0x00ff
之间的字符。
在下文代码页中有关于ISO 8859-1与Windows-1252的区别。
4、Unicode编码详解
Unicode字符集可以简写为UCS(Unicode Character Set),0x0000~0X00ff与ISO 8859-1保持一致
Unicode可以逻辑分为17平面(Plane),每个平面拥有65536( = 2^16)个代码点,虽然目前只有少数平面被使用。
平面0 (0000–FFFF): 基本多文种平面(Basic Multilingual Plane, BMP).
平面1 (10000–1FFFF): 多文种补充平面(SupplementaryMultilingual Plane, SMP).
平面2 (20000–2FFFF): 表意文字补充平面(SupplementaryIdeographic Plane, SIP).
平面3 (30000–3FFFF): 表意文字第三平面(TertiaryIdeographic Plane, TIP).
平面4 to 13 (40000–DFFFF)尚未使用
平面14 (E0000–EFFFF): 特别用途补充平面(SupplementarySpecial-purpose Plane, SSP)
平面15 (F0000–FFFFF)保留作为私人使用区(PrivateUse Area, PUA)平面16 (100000–10FFFF),保留作为私人使用区(PrivateUse Area, PUA)
中、日、韩的三种文字占用了Unicode中0x3000(12288)到0x9FFF(40959)的部分,共计28671个字符;
而中文在BMP中的范围是:U+4E00到U+9FA5之间是汉字的Unicode编码。
5、 UTF格式详解
UTF
是 Unicode Transformation Format
的缩写。是Unicode的一种实现方案。任何文字在Unicode中都对应一个值,这个值称为代码点也叫码位(CodePoint)。代码点的值通常写为:U+ABCD,在Java中可以直接将一个字符赋值为
public class Test1 {
public static void main(String[] args) throws Exception {
char ch = '\u6211';
System.out.println(ch);
}
}
输出结果:我
UTF-8四种具体实现方式:
1.第一种是一个字节的编码:即128个ascii字符(只需要一个字节)
格式:0xxxxxxx
2^7 - 1 = 127 = 7F = (0111-1111)
编码方式Unicoe范围由(U+0000 至 U+007F)
**2.第二种是两个字节的编码:即带有符号的拉丁文,希腊文,西里尔字母,亚美尼亚语,希伯来文,阿拉伯文等,则需要两个字节编码(Unicode 范围由U+0080至U+07FF) **
格式:110xxxxx 10xxxxxx
(0080)16 = (128)10
(07FF) 16 = (2047)10 = 2^11-1;
3.第三种是三字节的编码,即其他多文种平面(BMP)中的字符(这包括了大部分的汉字)(范围为: U+0800 至 U+FFFF)
格式:1110xxxx 10xxxxxx 10xxxxxx
U+0800 = 2048;
U+FFFF = 65535 = 2^16 -1;
1110xxxx 10xxxxxx 10xxxxxx
4.第四种是4-6字节编码。
U+1 0000至U+1 FFFFF:使用四字节
U+20 0000 至U+3FF FFFF:使用五字节
U+400 0000至U+7FFF FFFF
例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
目前计算机一般使用 2 个字节(16 位)来存放一个序号(DBCS,DoubleByte Character System),因此,这种方式存放的字符也被称作宽字节字符。比如,字符串"中文123" 在 Windows2000 下,内存中实际存放的是 5 个字符,一共10个字节;若在gb2312编码中,共计五个字符,7个字节。
UTF-16和UCS-2区别与联系
UTF-16和UCS-2都是Unicode的编码方式。
Unicode使用一个确定的名字和一个叫做码位(code point)的整数来定义一个字符。例如©字符被命名为“copyright sign”并且有一个值为U+00A9(0xA9,十进制169)的码位。
Unicode的码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符. Unicode的码空间可以划分为17个平面(plane),每个平面包含216
(65,536)个码位。每个平面的码位可表示为从U+xx0000到U+xxFFFF, 其中xx表示十六进制值从0016
到1016
,共计17个平面。
第一个Unicode平面(码位从U+0000至U+FFFF)包含了最常用的字符,该平面被称为基本多语言平面(Basic Multilingual Plane),缩写为BMP。其他平面称为辅助平面(Supplementary Planes)。
UCS-2 (2-byte Universal Character Set)是一种定长的编码方式,UCS-2仅仅简单的使用一个16位码元来表示码位,也就是说在0到0xFFFF的码位范围内,它和UTF-16基本一致。
UTF-16 (16-bit Unicode Transformation Format)是UCS-2的拓展,它可以表示BMP以为的字符。UTF-16使用一个或者两个16位的码元来表示码位,这样就可以对0到0x10FFFF的码位进行编码。
例如,在UCS-2和UTF-16中,BMP中的字符U+00A9 copyright sign(©)都被编码为0x00A9。
但是在BMP之外的字符,例如𝌆,只能用UTF-16进行编码,使用两个16为码元来表示:0xD834 0xDF06。这被称作代理对,值得注意的是一个代理对仅仅表示一个字符,而不是两个。UCS-2并没有代理对的概念,所以会将0xD834 0xDF06解释为两个字符。
简单的说,UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的是同一的意思。(严格的说这并不正确,因为在UTF-16中从U+D800到U+DFFF的码位不对应于任何字符,而在使用UCS-2的时代,U+D800到U+DFFF内的值被占用。)但当引入辅助平面字符后,就称为UTF-16了。
6、代码页及字符集对照表
Windows将字符集称作代码页。代码页是字符集编码的别名,也有人称"内码表",
代码页 | 名称 | 显示名称 |
---|---|---|
37 | IBM037 | IBM EBCDIC(美国 - 加拿大) |
936 | gb2312 | 简体中文 (GB2312) |
950 | big5 | 繁体中文 (Big5) |
1200 | utf-16 | Unicode(Little-Endian) |
1201 | UnicodeFFFE | Unicode (Big-Endian) |
28591 | Windows-28591 | ISO-8859-1 |
65001 | UTF-8 | UTF-8 |
7、ISO-8859-1和Windows-1252的区别
ISO-8859-1,正式编号为ISO/IEC 8859-1:1998,又称Latin-1或“西欧语言”,是国际标准化组织内ISO/IEC 8859的第一个8位字符集。它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入96个字母及符号,藉以供使用附加符号的拉丁字母语言使用。Unicode的前0-255个字符与ISO-8859-1相一致。
Windows-1252经常被错误地贴上ISO-8859-1的标签,因为它们十分相似。除了128到159(十六进制80到9F)范围内的很少使用的C1控制字符被替换为额外的字符外,Windows-1252代码页的字符和ISO-8859-1完全一致。Windows-28591代码页才是真正的ISO-8859-1,然而,英文版的Windows 7系统上似乎没有Windows-28591代码页,至于其他系统有没有我就不知道了。Windows-1252是ISO的超集。
UTF-16与UCS-2的联系与区别:
UTF-16和UCS-2都是Unicode的编码方式。Unicode使用一个确定的名字和一个叫做代码点(code point)的整数来定义一个字符。例如©字符被命名为“copyright sign”并且有一个值为U+00A9(0xA9,十进制169)的代码点。
Unicode的码空间为U+0000到U+10FFFF,共有1,112,064个代码点(code point)可用来映射字符. Unicode的码空间可以划分为17个平面(plane),每个平面包含216(65,536)个代码点。每个平面的代码点可表示为从U+xx0000到U+xxFFFF, 其中xx表示十六进制值从0016 到1016,共计17个平面。
第一个Unicode平面(代码点从U+0000至U+FFFF)包含了最常用的字符,该平面被称为基本多语言平面(Basic Multilingual Plane),缩写为BMP。其他平面称为辅助平面(Supplementary Planes)。
UCS-2 (2-byte UniversalCharacter Set)是一种定长的编码方式,UCS-2仅仅简的使用一个16位码元来表示代码点,也就是说在0到0xFFFF的代码点范围内,它和UTF-16基本一致。
UTF-16 (16-bit UnicodeTransformation Format)是UCS-2的拓展,它可以表示BMP以为的字符。UTF-16使用一个或者两个16位的码元来表示代码点,这样就可以对0到0x10FFFF的代码点进行编码。
例如,在UCS-2和UTF-16中,BMP中的字符U+00A9copyright sign(©)都被编码为0x00A9。
但是在BMP之外的字符,例如,只能用UTF-16进行编码,使用两个16位码元来表示:0xD834 0xDF06。这被称作代理对,值得注意的是一个代理对仅仅表示一个字符,而不是两个。UCS-2并没有代理对的概念,所以会将0xD834 0xDF06解释为两个字符。
简单的说,UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的是同一的意思。(严格的说这并不正确,因为在UTF-16中从U+D800到U+DFFF的代码点不对应于任何字符,而在使用UCS-2的时代,U+D800到U+DFFF内的值被占用。)但当引入辅助平面字符后,就称为UTF-16了。
但UCS-2只是一个编码方案,UTF-16却要用于实际的传输,所以就不得不考虑字节序的问题。
8、UTF的字节序和BOM
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E
,“乙”的Unicode编码是4E59
。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
Unicode规范中推荐的标记字节顺序的方法是BOM。BOM是Byte Order Mark。BOM是一个有点小聪明的想法:
在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTHNO-BREAK SPACE"。
这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符" zero widthno-break space"又被称作BOM。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
9、Windows记事本有四种保存格式
用记事本-文件-另存为,如上如图即可看到Windows记事本保存的四种格式,如上如所示,分别为:
ANSI:在简体中文系统的windows中ANSI即gb2312.
Unicode:对应UTF-16LE,
Unicode Big Endian:对应UTF-16BE
UTF-8:使用了变长的编码
Big Endian 和 Little Endian名词的由来
这两个术语来自于 Jonathan Swift 的《《格利佛游记》其中交战的两个派别无法就应该从哪一端--小端还是大端--打开一个半熟的鸡蛋达成一致。:)
“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。
我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。
在那个时代,Swift是在讽刺英国和法国之间的持续冲突,Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序,后来这个术语被广泛接纳了。
主要表现在存储格式上,比如一个字符的编码为ABCD
Big Endian的(FE FF)存储格式为:AB CD;
Little Endian的(FF FE)存储格式为:CD AB ;
Windows记事本就是使用BOM来标记文本文件的编码方式的。当打开一个txt文本,会自动添加BOM。
二 应用
- 1 Java对字符的处理
1 )、String类的public byte[] getBytes(Charset charset)
这是java字符串处理的一个标准函数,其作用是将字符串所表示的字符按照charset编码,并以字节方式表示。注意字符串在java内存中总是按unicode编码存储的。
public class Test1 {
public static void main(String[] args) throws Exception {
String string = "你好!";
String str1 = new String(string.getBytes("gbk"));
System.out.println(str1);
}
}
将一个String 类型Unicode字符串转为对应字节,一般String默认光标gbk编码;各个编译器可能不同,可以到windows-preference-general-workspace界面的左下角有显示,也可以自行调节。
2)、 new String(charset)
这是java字符串处理的另一个标准函数,和上一个函数的作用相反,将字节数组按照charset编码进行组合识别,最后转换为unicode存储。参考上述getBytes的例子
3)、setCharacterEncoding()
该函数用来设置http请求或者相应的编码。
1.2. String 与byte的相互转换
java字符编码常见问题主要在两个方面
- 字节到String
- String转字节。
1.2.1 字节到String。
只有字节才有编码含义,String永远是Unicode。在java中,字符默认存储的编码为utf-8码,所以String str1 = “你好,Ice Blue”;Str的编码为utf-8可以用一下代码来实验:
System.out.println(Charset.defaultCharset());
以下java代码实现了将一个字符的编码转换为汉字。
public class Test02 {
public static void main(String[] args) throws Exception {
System.out.println("字节按编码转成字符:");
String strUtf8Hex = "E4B8ADE69687"; // “中文”的utf8的16进制编码
byte byteUtf8[] = hex2byte(strUtf8Hex);// 转成字节流
String str = new String(byteUtf8,"UTF-8");
System.out.println(str);
}
public static byte[] hex2byte(String str) {
byte[] b = new byte[str.length() / 2];
for (inti = 0; i < str.length(); i += 2) {
String str2 = str.substring(i, i + 2);
b[i / 2] = (byte) Integer.parseInt(str2, 16);
}
return b;
}
}
2. String转字节。String.getBytes方法是按编码集转换编码,不能理解为取出String的字节来。是平时常见转码工作应该采用的方法。
以下代码实现了将一个汉字转换为其对应编码
public class Test1 {
public static void main(String[]args) throws Exception {
System.out.println("字节按编码转成字符:");
String strUtf8Hex ="中文賦";// “中文”的utf8的16进制编码
byte[] Utf8byte = strUtf8Hex.getBytes("UTF-16BE");
System.out.println(byte2hex(Utf8byte));
}
public static String byte2hex(byte[]b) {
String sum = "";
String stmp = "";
for (inti = 0; i < b.length; i++) {
stmp = Integer.toHexString(b[i] & 0XFF);//保留前8位
if (stmp.length() == 1)
sum = sum + "0" + stmp;
else
sum = sum + stmp;
}
return sum.toUpperCase();
}
}
拓展:
计算机数制的概念
基本概念:
数码:数制中表示基本数值大小的不同数字符号。
例如,
二进制有两个数码:0,1;
十进制有10个数码:0、1、2、3、4、5、6、7、8、9。
十六进制有16个数码:0、1、2、3、4、5、6、7、8、9,A、B、C、D、E、F
基数:数制所使用数码的个数。例如,二进制的基数为2;十进制的基数为10。****
位权: 数制中某一位上的1所表示数值的大小(所处位置的价值)。例如,十进制的123,1的位权是100,2的位权是10,3的位权是1。二进制中的 1011 ,第一个1的位权是8,0的位权是4,第二个1的位权是2,第三个1的位权是1;
数制:按进位的原则进行计数,称为进位计数制,简称数制。不论是哪一种数制,其计数和运算都有共同的规律和特点。
⑴ ** 逢N进一
N是指数制中所需要的数字字符的总个数,称为基数。如:0、1、2、3、4、5、6、7、8、9等10个不同的符号来表示数值,这个10就是数字字符的总个数,也是十进制的基数,表示逢十进一。
⑵ ** 位权表示法
位权是指一个数字在某个固定位置上所代表的值,处在不同位置上的数字所代表的值不同,每个数字的位置决定了它的值或者位权。位权与基数的关系是:各进位制中位权的值是基数的若干次幂。
数制符号
二进制B(binary)
八进制O(octal)
十进制D(decimal)
十六进制H(hexadecimal)
至于进制转换网上有很多参考文档,这里不再赘述。
参考资料:
[1] 趣谈Unicode,ansi,utf-8,Unicode big endian这些编码有什么区别(http://blog.csdn.net/fanwenbo/article/details/2298800)
[2] Unicode字符查询(http://unicode-table.com/cn/#control-character)
[3] 国标码查询 (http://www.qqxiuzi.cn/bianma/guobiaoma.php)
[4] Code Page Identifiers ( https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx)
[5] UTF-16与UCS-2的区别 http://demon.tw/programming/utf-16-ucs-2.html