中文编码
Document Information:
Owner: | Li.zhang |
Date: | 2008.02.05 |
Version: | 0.01 |
Version History:
Version | Date | Author | Summary of Changes | Details of Changes |
0.01 | 2008-02-05 | Li.zhang | 草稿 | |
1 The History of Character Coding
- ASCII(American Standard Code for Information Interchange)
早期计算机单字节编码。
0x0-0x20 (包含0x7F):控制字符,如0x0 NUL,0xA LF(line feed), 0x7F del。
0x21-0x7e : 通用英文字符,数字,运算符,标点符号等。
0x80-0xff : 扩展字符。
- ANSI(American National Standards Institute)
由于计算机的普及,不同的国家地区由于语言的需要,指定了不同的适应本国和地区的编码标准如GB2312,JIS等。ANSI是扩展ASCII编码,由两个字节来表示ASCII没有涵盖的本地文字编码。由于ASCII编码是0x00-0x7F,ANSI编码利用0x80-0xFF做为扩展编码的标示,详细解释参见后文。
GB2312又称为GB2312-80字符集,全称为《信息交换用汉字编码字符集·基本集》,由原中国国家标准总局发布,1981年5月1日实施,是中国 国家标准的简体中文字符集。
GB2312收录简化汉字及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符。其中包括6763个汉字,其中一级汉字3755个,二级汉字3008个;包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符.
GB2312是中国大陆普及采用的ANSI编码,现在已被GBK代替,GBK兼容GB2312,并提供GB2312中没有囊括的汉字编码。GB18030又 囊括了GBK.
GB2312 (GB2312-1980) 的编码:
* 单字节:第一字节 0-----0x7F (0-127)
* 双字节:第一字节 0xB0--0xF7 (176--247) 第二节 0xA1--0xFE(161--254)
GBK (GB13000/GB12345) 的编码:
* 单字节:第一字节 0-----0x7F (0-127)
* 双字节:第一字节 0x81--0xFE (129--254) ,第二节 0x40--0xFE(64--254)
2.3 GB18030 的编码:
* 单字节:第一字节 0--0x7F (0-127)
* 双字节:第一字节 0x81--0xFE (129--254) ,第二节 0x40--0xFE(64--254)
* 四字节:第一字节 0x81--0xFE (129--254) ,第二节 0x30--0x39(48--57)
第三字节 0x81--0xFE (129--254) ,第四节 0x30--0x39(48--57)
例2: Hello,world: 其内存标示:
int main() { char* s = "Hello,world"; int I = 0; while( s[i] != 0) { printf("%-4c ", s[i++]); } printf("/n"); i=0; while( s[i] != 0) {时 printf("%#x ", s[i++]); } } 结果: 图 2 Test中程序从源码中读取Hello,world,并将其按照ASCII编码存储于内存。 |
例3: ”Hello,中文”,其内存标示: //Test.c int main() { char* s = "Hello,中文"; char temp[3]; unsigned short *p; int i = 0; int j = 0; int k = 0; for(i = 0; i < 6; i++) { printf("%-4c ",s[i]); } printf("/n"); for(i = 0; i < 6; i++) { printf("%#x ",s[i]); } printf("/n"); for(i = 6; i < 10; i += 2) { temp[0] = s[i]; temp[1] = s[i+1]; temp[2] = 0; j = s[i]; j = j&0x00ff; k = s[i+1]; k = k&0x00ff; printf("%s(%#x %#x) ",temp, j, k); } printf("/n"); for(i = 6; i < 10; i += 2) { p = &s[i]; printf("%-#13x ", *p); } printf("/n"); }
首先分析一下结果:“Hello,”是按照ASCII编码,如H,ASCII编码为0x48.但中文却是一个字符占两个字节,“中”在GBK和GB2312中对应的编码为0xd6d0,其在程序结果对应的内存值为0xd6 0xd0. GB2312用双字节标示一个汉字,在windows内存存储和文件存储中,第一个字节存放高字节,第二个字节存放低字节(big endian),如“中”,他在内存中存放两字节,第一字节存放0xd6,第二个字节存放0xd0。在GB2312中原始兼容的ASCII编码仍然为1字节,但对于汉字为两个字节。 如”Hello,中文”,其编码为: H e l l o , 中 文 0x 48 65 6c 6c 6f 2c d6d0 cec4 文字编辑器是如何存储这些文字的呢?
图 5 可以看到编码选项为ANSI,而在中文WINDOWS XP操作系统中ANSI编码为GBK或者GB2312(现在基本为GBK,windows95/98以GBK为基本汉字编码)。当默认保存test.c时候,采取的就是ANSI默认编码GBK。所以其存储格式如图4所示。 如何识别GBK(GB2312)编码的呢? 仅以GBK为例,(GBK编码表可以参照http://users.ir-lab.org/~taozi/GBK1.txt) GBK用双字节编码,两字节中前面字节为第一字节或高字节,后面为第二字节或低字节,高位字节值在0X81-0XFE之间,低位字节在0X40-0XFE之间(除去7F,7F在GBK编码中有特殊用处)。GB2312,GBK都是双字节字符集(DBCS)。DBCS中内码格式始终为big-endian(高位在前).
例如: 英文+汉字 --- ”Hello,中文”, 图 6,文本编辑器显示 图7 ,二进制显示
“文”同理。 |
- Unicode
Unicode不与ANSI编码如GBK,GB2312兼容,只与ASCII兼容.
(Unicode编码表,请参考http://zh.wikipedia.org/w/index.php?title=Unicode&variant=zh-tw)
Unicode编码方式与UCS ( Universal Character Set)概念相对应,目前实际应用的Unicode版本对应于UCS-2,使用16bit的编码空间,及两个字节存储一个Character,可以标示65536个Character。
如果一个仅包含ASCII Char的Unicode文件,如果每个字节都用双字节存储,则char的第一字节始终为0,造成空间浪费。鉴于这种情况,可以使用UTF-8编码,UTF-8(UTF: Unicode Translation Format)是变长编码,UTF-8使用可变长度字节来储存 Unicode字符,例如ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节.非ASCII Char以1-3个字节编码,利用每个字节的高位0/1来辨别。而UTF-16俱采用2字节标示一个char,java默认采用UTF-16编码。
标准Unicode为UTF-16.当Unicode字符在网络中传输时,由于UTF-8采用字节编码,所以没有字节序的问题,UTF-16以两个字节为一个编码单元,所以字节序很重要,如0X4E4F“乏”,0X4F4E “低”,如果程序探测到一个字节流0x4e4f,究竟这个字是“低”还是“乏”呢?所以Unicode提供了一种标定字节顺序的方法 BOM(Byte Order Mark),由于UCS编码规范中规定一个"ZERO WIDTH NO-BREAK SPACE"的字符(0xFEFF),由于0Xfffe在UCS中是不存在的字符,所以在传输字节流的时候可以先传输” ZERO WIDTH NO-BREAK SPACE”,如果收到0xfeff则这个字节流为big-endian,如果为0xfffe则为little-endian. UTF -8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF。所以如果收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
实验: 打开开始菜单 –>所有程序->附件->系统工具->字符映射表. 选择一个字体 “宋体”,并选择高级查看,选择Unicode。选择汉字“中”,如图,可见”中”的Unicode值为0X4E2D,查询Unicode编码表如图可见为0x4e2d. 在字符映射表中选择”windows: 中文(简体)“,选择汉字“中”:由图可见汉字“中”在windows:中文(简体)(GBK)中为0XD6D0,而Unicode中为0x4e2d. 图 “U+4E2D” Unicode编码,D6D0 GBK 编码 |
当在文本编辑器中如notepad: 写入“中文”,然后存储为Unicode编码,是如何实现转换的呢? 在非 Unicode环境中,由于不同国家和地区采用的字符集不一致,很肯呢个无法正常显示文字,微软公司采用了code page 转换表的技术暂时解决这个问题,及通过转换表将非Unicode的char编码转换为同以字对应的windows系统内部使用的Unicode编码,由于字window2000以来,windows内码使用unicode.可以在控制面板“语言与区域设置”中高级选项选择代码也转换表。选择不同的语言从而选择不同的代码转换表集合。如选择中文,其中在代码也转换表中囊括GBK(code page 936),GB2312(code page 20936)等。 Windows就是用代码页来适应各种语言的。指定了哪种代码页,windows和程序就用哪中编码来解释遇见的的字节流。在c/c++中可以通过setlocal这个函数来实现指定代码页。如setlocale(LC_ALL, ".936");就是用code page 936来解释程序中的字节流,code page 936对应GBK. |
2 Unicode 与 Java
JVM中字符都是Unicode(UTF-16),所以String中数据都是Unicode编码的,由于Unicode统一编码,所以String无差别,但由String转化成的byte[]数组确实带编码的。一个GBK编码的byte[]转换成String,其实就是GBK编码转化Unicode编码。一个String转换成一个GBK编码的byte[],就是从Unicode编码转化GBK编码。
那么java程序开发中,java是如何编码解码的呢?
可见“中文“采用UTF-8格式编码 “中” -> 0xe4b8ad “文”->e69687,已从源码中的d6d0cec4 –转换成UTF-8格式。如果在非GBK默认file.encoding中编译GB K源码呢。仅以Cp1252为例(Cp1252 ISO 8859-1,拉丁字母 1 号,编码0x00-0xff, 最简单的编码规则,每一个字节直接作为一个 UNICODE 字符。比如,[0xD6, 0xD0] 这两个字节,通过 iso-8859-1 转化为字符串时,将直接得到 [0x00D6, 0x00D0] 两个 UNICODE 字符,即 "ÖÐ"。)
如果源码是GBK的,在Cp1252下编译运行: 生成的class文件: 为什么此时的“中文“编码变为0x c3 96 c3 90 c3 8e c3 84 呢? 由于此时系统默认的file.encoding为Cp1252,从Cp1252编码转化成Unicode编码时,仅是添加一个0字节,”中文”:0xd6d0 ce c4- >会变成Unicode编码 0x00d6 0x00d0 0x00ce 0x 00c4.在将这个Unicode(UTF-16)编码转化成UTF-8编码。 附: UTF-16 转化 UTF-8 规则: 所以 0x 00d6 二进制-> 1101-0110 UTF-8: 1100-0011 1001-0110 UTF-8 0X-> c3 96 同理 0x00d0 0x00ce 0xc4 二进制-> 1101-0000 1100-1110 1100-0100 UTF-8: 1100-0011 1001-0000 1100-0011 1000-1110 1100-0011 1000-0100 UTF-8 0X-> c3 90 c3 8e c3 84 所以class结果完全正确,他按照cp1252读取源码并转化成Unicode-,并最后生成class文件中的UTF-8.
假设某操作系统默认编码为GBK,源码以GBK编码,并含有“中”的字符,程序运行并打印”中” 源码 javac class Java console 0xd6d0 0x4e2d 0xe4b8ad 0x4e2d 0xd6d0 因为系统默认编码为GBK,所以javac编译源码时能正确的将0xd6d0转化成相应的Unicode 0x4e2d ,并正确的转化成UTF-8 0XE4B8AD. 运行java程序,java将UTF-8正确的解析为0x4e2d,打印时java有正确的将unicode解析为GBK,并正确打印。 假设某操作系统默认编码为Cp1252,源码以GBK编码,并含有“中”的字符,程序运行并打印”中” 源码 javac class Java console d6d0 00d6 00d0 c396 c390 00d6 00d0 d6d0 因为系统默认编码为GBK,所以javac编译源码时将0xd6d0转化成相应的Unicode 0x00d6 0x00d0 ,并转化成class: UTF -8 c396 c390,最后运行java,java load class解析成Unicode 0x00d6 0x00d0,打印时Unicode又按照Cp1252转化成d6d0. |
实验: //Test.java public class Test { public static void main(String[] args) { System.out.println(System.getProperty("file.encoding")); String str = "中文"; for(int i = 0; i < str.length(); i++) { System.out.printf("%d: %x /n",i,str.codePointAt(i)); } System.out.println("*************"); byte[] b = str.getBytes(); for(int i = 0; i < b.length; i++) { System.out.printf("%d: %x /n",i,b[i]); } } } 结果: 可见在内存中是按照Unicode(UTF-16)存储的,中->0x4e2d.但调用str.getBytes(),java会将Unicode解析成系统默认的file.encoding GBK编码,所以见到编码 0xd6d0.当然可以调用str.getBytes(charset)得到任意编码形式的“中文”byte[],如str.getBytes(“Cp1252”); public class Test { public static void main(String[] args) { try { System.out.println(System.getProperty("file.encoding")); String str = "中文"; for(int i = 0; i < str.length(); i++) { System.out.printf("%d: %x /n",i,str.codePointAt(i)); } System.out.println("*************"); byte[] b = str.getBytes("Cp1252"); for(int i = 0; i < b.length; i++) { System.out.printf("%d: %x /n",i,b[i]); } }catch(Exception e) { e.printStackTrace(); } } } 但其结果: 得到的bytes为3f,3f,3f对应字符“?”,这是由于4e2d,是汉字编码,java认为从Unicode转化成CP1252,CP1252中没有正确的对应字符,所以反馈为”?”. 也可以设置getBytes,Charset ->BIG5,(台湾等地编码) 解析为big5中字符,在big5中a4a4也对应“中”而a4a5->对应“丰” |
Java中编码注意的地方主要在数据库,文件,网络,JVM之间交流数据时产生问题。但解决这个问题最关键一点: 如果java得到是一个字节流,他会用默认的file.encoding来解析这个字节流,并将这个字节流转化成Unicode。当JAVA解析一个Unicode成bytes,他也用默认的file.encoding解析。记住这一点就没有问题了。 |