gb2312java编码⼏个字节_彻底搞懂乱码——字符,字节和编
级别:中级
摘要:本⽂介绍了字符与编码的发展过程,相关概念的正确理解。举例说明了⼀些实际应⽤中,编码的实现⽅法。然后,本⽂讲述了通常对字符与编码的⼏种误解,由于这些误解⽽导致乱码产⽣的原因,以及消除乱码的办法。本⽂的内容涵盖了“中⽂问题”,“乱码问题”。
掌握编码问题的关键是正确地理解相关概念,编码所涉及的技术其实是很简单的。因此,阅读本⽂时需要慢读多想,多思考。
引⾔
“字符与编码”是⼀个被经常讨论的话题。即使这样,时常出现的乱码仍然困扰着⼤家。虽然我们有很多的办法可以⽤来消除乱码,但我们并不⼀定理解这些办法的内在原理。⽽有的乱码产⽣的原因,实际上由于底层代码本⾝有问题所导致的。因此,不仅是初学者会对字符编码感到模糊,有的底层开发⼈员同样对字符编码缺乏准确的理解。
1. 编码问题的由来,相关概念的理解
1.1 字符与编码的发展
从计算机对多国语⾔的⽀持⾓度看,⼤致可以分为三个阶段:
字符串在内存中的存放⽅法:
在 ASCII 阶段,单字节字符串使⽤⼀个字节存放⼀个字符(SBCS)。⽐如,"Bob123" 在内存中为:
42 6F 62 31 32 33 00
B o b 1 2 3 \0
在使⽤ ANSI 编码⽀持多种语⾔阶段,每个字符使⽤⼀个字节或多个字节来表⽰(MBCS),因此,这种⽅式存放的字符也被称作多字节字符。⽐如,"中⽂123" 在中⽂ Windows 95 内存中为7个字节,每个汉字占2个字节,每个英⽂和数字字符占1个字节:
D6 D0 CE C4 31 32 33 00
中 ⽂ 1 2 3 \0
在 UNICODE 被采⽤之后,计算机存放字符串时,改为存放每个字符在 UNICODE 字符集中的序号。⽬前计算机⼀般使⽤ 2 个字节(16位)来存放⼀个序号(DBCS),因此,这种⽅式存放的字符也被称作宽字节字符。⽐如,字符串 "中⽂123" 在 Windows 2000 下,内存中实际存放的是 5 个序号:
中 ⽂ 1 2 3 \0
⼀共占 10 个字节。
1.2 字符,字节,字符串
理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做⼀下区分:
由于不同 ANSI 编码所规定的标准是不相同的,因此,对于⼀个给定的多字节字符串,我们必须知道它采⽤的是哪⼀种编码规则,才能够知道它包含了哪些“字符”。⽽对于 UNICODE 字符串来说,不管在什么环境下,它所代表的“字符”内容总是不变的。
1.3 字符集与编码
java语言使用的字符码集是各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各⾃语⾔所需的“字符”。⽐如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:
使⽤哪些字符。也就是说哪些汉字,字母和符号会被收⼊标准中。所包含“字符”的集合就叫做“字符集”。
规定每个“字符”分别⽤⼀个字节还是多个字节存储,⽤哪些字节来存储,这个规定就叫做“编码”。
各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”⼀般都是同时制定的。因此,平常我们所说的“字符集”,⽐如:
GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。
“UNICODE 字符集”包含了各种语⾔中使⽤到的所有“字符”。⽤来给 UNICODE 字符集编码的标准有很多种,⽐如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。
1.4 常⽤的编码简介
简单介绍⼀下常⽤的编码规则,为后边的章节做⼀个准备。在这⾥,我们根据编码规则的特点,把所有的编码分成三类:
我们实际上没有必要去深究每⼀种编码具体把某⼀个字符编码成了哪⼏个字节,我们只需要知道“编码”的概念就是把“字符”转化成“字节”就可以了。对于“UNICODE 编码”,由于它们是可以通过计算得到的,因此,在特殊的场合,我们可以去了解某⼀种“UNICODE 编码”是怎样的规则。
2. 字符与编码在程序中的实现
2.1 程序中的字符与字节
在 C++ 和 Java 中,⽤来代表“字符”和“字节”的数据类型,以及进⾏编码的⽅法:
以上需要注意⼏点:
Java 中的 char 代表⼀个“UNICODE 字符(宽字节字符)”,⽽ C++ 中的 char 代表⼀个字节。
MultiByteToWideChar()和 WideCharToMultiByte() 是 Windows API 函数。
2.2 C++ 中相关实现⽅法
声明⼀段字符串常量:
// ANSI 字符串,内容长度 7 字节
char sz[20] = "中⽂123";
// UNICODE 字符串,内容长度 5 个 wchar_t(10 字节)
wchar_t wsz[20] = L"\x4E2D\x6587\x0031\x0032\x0033";
UNICODE 字符串的 I/O 操作,字符与字节的转换操作:
// 运⾏时设定当前 ANSI 编码,VC 格式
setlocale(LC_ALL, ".936");
// GCC 中格式
setlocale(LC_ALL, "zh_CN.GBK");
// Visual C++ 中使⽤⼩写 %s,按照 setlocale 指定编码输出到⽂件
// GCC 中使⽤⼤写 %S
fwprintf(fp, L"%s\n", wsz);
// 把 UNICODE 字符串按照 setlocale 指定的编码转换成字节
wcstombs(sz, wsz, 20);
// 把字节串按照 setlocale 指定的编码转换成 UNICODE 字符串
mbstowcs(wsz, sz, 20);
在 Visual C++ 中,UNICODE 字符串常量有更简单的表⽰⽅法。如果源程序的编码与当前默认 ANSI 编码不符,则需要使⽤ #pragma setlocale,告诉编译器源程序使⽤的编码:
// 如果源程序的编码与当前默认 ANSI 编码不⼀致,
// 则需要此⾏,编译时⽤来指明当前源程序使⽤的编码
#pragma setlocale(".936")
// UNICODE 字符串常量,内容长度 10 字节
wchar_t wsz[20] = L"中⽂123";
以上需要注意 #pragma setlocale 与 setlocale(LC_ALL, "")的作⽤是不同的,#pragma setlocale 在编译时起作⽤,setlocale() 在运⾏时起作⽤。
2.3 Java 中相关实现⽅法
字符串类 String 中的内容是 UNICODE 字符串:
// Java 代码,直接写中⽂
String string = "中⽂123";
// 得到长度为 5,因为是 5 个字符
System.out.println(string.length());
字符串 I/O 操作,字符与字节转换操作。在 Java 包 java.io.* 中,以“Stream”结尾的类⼀般是⽤来操作“字节串”的类,
以“Reader”,“Writer”结尾的类⼀般是⽤来操作“字符串”的类。
// 字符串与字节串间相互转化
// 按照 GB2312 得到字节(得到多字节字符串)
byte [] bytes = Bytes("GB2312");
// 从字节按照 GB2312 得到 UNICODE 字符串
string = new String(bytes, "GB2312");
// 要将 String 按照某种编码写⼊⽂本⽂件,有两种⽅法:
// 第⼀种办法:⽤ Stream 类写⼊已经按照指定编码转化好的字节串
OutputStream os = new FileOutputStream("1.txt");
os.write(bytes);
os.close();
// 第⼆种办法:构造指定编码的 Writer 来写⼊字符串
Writer ow = new OutputStreamWriter(new FileOutputStream("2.txt"), "GB2312");
ow.write(string);
ow.close();
/* 最后得到的 1.txt 和 2.txt 都是 7 个字节 */
如果 java 的源程序编码与当前默认 ANSI 编码不符,则在编译的时候,需要指明⼀下源程序的编码。⽐如:
E:\>javac -encoding BIG5 Hello.java
以上需要注意区分源程序的编码与 I/O 操作的编码,前者是在编译时起作⽤,后者是在运⾏时起作⽤。
3. ⼏种误解,以及乱码产⽣的原因和解决办法
3.1 容易产⽣的误解
对编码的误解
第⼀种误解,往往是导致乱码产⽣的原因。第⼆种误解,往往导致本来容易纠正的乱码问题变得更复杂。
在这⾥,我们可以看到,其中所讲的“误解⼀”,即采⽤每“⼀个字节”就是“⼀个字符”的转化⽅法,实际上也就等同于采⽤ iso-8859-1 进⾏转化。因此,我们常常使⽤ bytes = Bytes("iso-8859-1") 来进⾏逆向操作,得到原始的“字节串”。然后再使⽤正确的ANSI 编码,⽐如 string = new String(bytes, "GB2312"),来得到正确的“UNICODE 字符串”。
3.2 ⾮ UNICODE 程序在不同语⾔环境间移植时的乱码
⾮ UNICODE 程序中的字符串,都是以某种 ANSI 编码形式存在的。如果程序运⾏时的语⾔环境与开发时的语⾔环境不同,将会导致 ANSI 字符串的显⽰失败。
⽐如,在⽇⽂环境下开发的⾮ UNICODE 的⽇⽂程序界⾯,拿到中⽂环境下运⾏时,界⾯上将显⽰乱
码。如果这个⽇⽂程序界⾯改为采⽤UNICODE 来记录字符串,那么当在中⽂环境下运⾏时,界⾯上将可以显⽰正常的⽇⽂。
由于客观原因,有时候我们必须在中⽂操作系统下运⾏⾮ UNICODE 的⽇⽂软件,这时我们可以采⽤⼀些⼯具,⽐如,南极
星,AppLocale 等,暂时的模拟不同的语⾔环境。
3.3 ⽹页提交字符串
当页⾯中的表单提交字符串时,⾸先把字符串按照当前页⾯的编码,转化成字节串。然后再将每个字节转化成 "%XX" 的格式提交到 Web 服务器。⽐如,⼀个编码为 GB2312 的页⾯,提交 "中" 这个字符串时,提交给服务器的内容为 "%D6%D0"。
在服务器端,Web 服务器把收到的 "%D6%D0" 转化成 [0xD6, 0xD0] 两个字节,然后再根据 GB2312 编码规则得到 "中" 字。
在 Tomcat 服务器中,Parameter() 得到乱码时,常常是因为前⾯提到的“误解⼀”造成的。默认情况下,当提交
"%D6%D0" 给 Tomcat 服务器时,Parameter() 将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,⽽不是返回⼀个 "中"字符。因此,我们需要使⽤ bytes = Bytes("iso-8859-1") 得到原始的字节串,再⽤ string = new String(bytes, "GB2312")重新得到正确的字符串 "中"。
3.4 从数据库读取字符串
通过数据库客户端(⽐如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使⽤的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。
如果从数据库读取字符串时得到乱码,⽽数据库中存放的数据⼜是正确的,那么往往还是因为前⾯提到的“误解⼀”造成的。解决的办法还是通过 string = new String( Bytes("iso-8859-1"), "GB2312") 的⽅法,重新得到原始的字节串,再重新使⽤正确的编码转化成字符串。
3.5 电⼦邮件中的字符串
当⼀段 Text 或者 HTML 通过电⼦邮件传送时,发送的内容⾸先通过⼀种指定的字符编码转化成“字节串”,然后再把“字节串”通过⼀种指定的传输编码(Content-Transfer-Encoding)进⾏转化得到另⼀串“字节串”。⽐如,打开⼀封电⼦邮件源代码,可以看到类似的内容:
Content-Type: text/plain;
charset="gb2312"
Content-Transfer-Encoding: base64
sbG+qcrQuqO17cf4yee74bGjz9W7+b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg==
最常⽤的 Content-Transfer-Encoding 有 Base64 和 Quoted-Printable 两种。在对⼆进制⽂件或者中⽂⽂本进⾏转化时,Base64 得到的“字节串”⽐ Quoted-Printable 更短。在对英⽂⽂本进⾏转化时,Quoted-Printable 得到的“字节串”⽐ Base64 更短。
邮件的标题,⽤了⼀种更简短的格式来标注“字符编码”和“传输编码”。⽐如,标题内容为 "中",则在邮件源代码中表⽰为:
// 正确的标题格式
Subject: =?GB2312?B?1tA=?=
其中,
第⼀个“=?”与“?”中间的部分指定了字符编码,在这个例⼦中指定的是 GB2312。
“?”与“?”中间的“B”代表 Base64。如果是“Q”则代表 Quoted-Printable。
最后“?”与“?=”之间的部分,就是经过 GB2312 转化成字节串,再经过 Base64 转化后的标题内容。
如果“传输编码”改为 Quoted-Printable,同样,如果标题内容为 "中":
// 正确的标题格式
Subject: =?GB2312?Q?=D6=D0?=
如果阅读邮件时出现乱码,⼀般是因为“字符编码”或“传输编码”指定有误,或者是没有指定。⽐如,有的发邮件组件在发送邮件时,标题 "中":
// 错误的标题格式
Subject: =?ISO-8859-1?Q?=D6=D0?=
这样的表⽰,实际上是明确指明了标题为 [0x00D6, 0x00D0],即 "ÖÐ",⽽不是 "中"。
4. ⼏种错误理解的纠正
误解:“ISO-8859-1 是国际编码?”
⾮也。iso-8859-1 只是单字节字符集中最简单的⼀种,也就是“字节编号”与“UNICODE 字符编号”⼀致的那种编码规则。当我们要把⼀个“字节串”转化成“字符串”,⽽⼜不知道它是哪⼀种 ANSI 编码时,先暂时地把“每⼀个字节”作为“⼀个字符”进⾏转化,不会造成信息丢失。然后再使⽤ bytes = Bytes("iso-8859-1") 的⽅法可恢复到原始的字节串。
误解:“Java 中,怎样知道某个字符串的内码?”
Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。