[原创文章Q{载请保留或注明出处:http://www.regexlab.com/zh/encoding.htm]
U别Q初U?/p>
摘要Q本文介l了字符与编码的发展q程Q相x늚正确理解。D例说明了一些实际应用中Q编码的实现Ҏ。然后,本文讲述了通常对字W与~码的几U误解,׃q些误解而导致ؕ码生的原因Q以及消除ؕ码的办法。本文的内容늛了“中文问题”,“ؕ码问题”?/p>
“字W与~码”是一个被l常讨论的话题。即使这P时常出现的ؕ码仍然困扰着大家。虽然我们有很多的办法可以用来消除ؕ码,但我们ƈ不一定理解这些办法的内在原理。而有的ؕ码生的原因Q实际上׃底层代码本n有问题所D的。因此,不仅是初学者会对字W编码感到模p,有的底层开发h员同样对字符~码~Z准确的理解?/p>
![]() | |||
|
从计机对多国语a的支持角度看Q大致可以分Z个阶D:
pȝ内码 | 说明 | pȝ | |
阶段一 | ASCII | 计算机刚开始只支持pQ其它语a不能够在计算Z存储和显C?/td> | 英文 DOS |
阶段?/td> | ANSI~码 Q本地化Q?/td> | Z计算机支持更多语aQ通常使用 0x80~0xFF 范围?2 个字节来表示 1 个字W。比如:汉字 '? 在中文操作系l中Q?[0xD6,0xD0] q两个字节存储?br /> 不同的国家和地区制定了不同的标准Q由此生了 GB2312, BIG5, JIS {各自的~码标准。这些?2 个字节来代表一个字W的各种汉字延~码方式Q称?b> ANSI ~码。在体中文系l下QANSI ~码代表 GB2312 ~码Q在日文操作pȝ下,ANSI ~码代表 JIS ~码?br /> 不同 ANSI ~码之间互不兼容Q当信息在国际间交流Ӟ无法属于两U语a的文字,存储在同一D?b> ANSI ~码的文本中?/td> | 中文 DOSQ中?Windows 95/98Q日?Windows 95/98 |
阶段?/td> | UNICODE Q国际化Q?/td> | Z使国际间信息交流更加方便Q国际组l制定了 UNICODE 字符?/b>Qؓ各种语言中的每一个字W设定了l一q且唯一的数字编P以满语言、跨q_q行文本转换、处理的要求?/td> | Windows NT/2000/XPQLinuxQJava |
字符串在内存中的存放ҎQ?/p>
?ASCII 阶段Q?b>单字节字W串使用一个字节存放一个字W(SBCSQ。比如,"Bob123" 在内存中为:
42 | 6F | 62 | 31 | 32 | 33 | 00 |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
B | o | b | 1 | 2 | 3 | \0 |
在?ANSI ~码支持多种语言阶段Q每个字W用一个字节或多个字节来表C(MBCSQ,因此Q这U方式存攄字符也被UC多字节字W?/b>。比如,"中文123" 在中?Windows 95 内存中ؓ7个字节,每个汉字?个字节,每个英文和数字字W占1个字节:
D6 | D0 | CE | C4 | 31 | 32 | 33 | 00 |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ||
?/td> | ?/td> | 1 | 2 | 3 | \0 |
?UNICODE 被采用之后,计算机存攑֭W串Ӟ改ؓ存放每个字符?UNICODE 字符集中的序受目前计机一般?2 个字节(16 位)来存放一个序PDBCSQ,因此Q这U方式存攄字符也被UC宽字节字W?/b>。比如,字符?"中文123" ?Windows 2000 下,内存中实际存攄?5 个序P
2D | 4E | 87 | 65 | 31 | 00 | 32 | 00 | 33 | 00 | 00 | 00 | ??x86 CPU 中,低字节在?/font> |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ||||||
?/td> | ?/td> | 1 | 2 | 3 | \0 |
一共占 10 个字节?/p>
![]() | |||
|
理解~码的关键,是要把字W的概念和字节的概念理解准确。这两个概念ҎhQ我们在此做一下区分:
概念描述 | 举例 | |
字符 | Z使用的记P抽象意义上的一个符受?/td> | '1', '?, 'a', '$', 'K?, …?/td> |
字节 | 计算Z存储数据的单元,一?位的二进制数Q是一个很具体的存储空间?/td> | 0x01, 0x45, 0xFA, …?/td> |
ANSI 字符?/td> | 在内存中Q如果“字W”是?ANSI ~码形式存在的,一个字W可能用一个字节或多个字节来表C,那么我们U这U字W串?ANSI 字符?/b>或?b>多字节字W串?/td> | "中文123" Q占7字节Q?/font> |
UNICODE 字符?/td> | 在内存中Q如果“字W”是以在 UNICODE 中的序号存在的,那么我们U这U字W串?UNICODE 字符?/b>或?b>宽字节字W串?/td> | L"中文123" Q占10字节Q?/font> |
׃不同 ANSI ~码所规定的标准是不相同的Q因此,对于一个给定的多字节字W串Q我们必ȝ道它采用的是哪一U编码规则,才能够知道它包含了哪些“字W”。而对?UNICODE 字符?/b>来说Q不在什么环境下Q它所代表的“字W”内ҎL不变的?/p>1.3 字符集与~码
各个国家和地区所制定的不?ANSI ~码标准中,都只规定了各自语a所需的“字W”。比如:汉字标准QGB2312Q中没有规定韩国语字W怎样存储。这?ANSI ~码标准所规定的内容包含两层含义:
各个国家和地区在制定~码标准的时候,“字W的集合”和“编码”一般都是同时制定的。因此,q_我们所说的“字W集”,比如QGB2312, GBK, JIS {,除了有“字W的集合”这层含义外Q同时也包含了“编码”的含义?/p>
?b>UNICODE 字符?/b>”包含了各种语言中用到的所有“字W”。用来给 UNICODE 字符集编码的标准有很多种Q比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig {?/p>
![]() | |||
|
?C++ ?Java 中,用来代表“字W”和“字节”的数据cdQ以及进行编码的ҎQ?/p>
cd或操?/b> | C++ | Java |
字符 | wchar_t | char * |
字节 | char | byte |
ANSI 字符?/td> | char[] | byte[] |
UNICODE 字符?/td> | wchar_t[] | String |
字节东y字符?/td> | mbstowcs(), MultiByteToWideChar() * | string = new String(bytes, "encoding") |
字符东y字节?/td> | wcstombs(), WideCharToMultiByte() | bytes = string.getBytes("encoding") |
以上需要注意几点:
![]() | |||
|
声明一D字W串帔RQ?/p>
// ANSI 字符Ԍ内容长度 7 字节 char sz[20] = "中文123"; // UNICODE 字符Ԍ内容长度 5 ?wchar_tQ?0 字节Q?/span> wchar_t wsz[20] = L"\x4E2D\x6587\x0031\x0032\x0033"; |
UNICODE 字符串的 I/O 操作Q字W与字节的{换操作:
// q行时设定当?ANSI ~码QVC 格式 setlocale(LC_ALL, ".936"); // GCC 中格?/span> setlocale(LC_ALL, "zh_CN.GBK"); // Visual C++ 中用小?%sQ按?setlocale 指定~码输出到文?br />// GCC 中用大?%S fwprintf(fp, L"%s\n", wsz); // ?UNICODE 字符串按?setlocale 指定的编码{换成字节 wcstombs(sz, wsz, 20); // 把字节串按照 setlocale 指定的编码{换成 UNICODE 字符?br />mbstowcs(wsz, sz, 20); |
?Visual C++ 中,UNICODE 字符串常量有更简单的表示Ҏ。如果源E序的编码与当前默认 ANSI ~码不符Q则需要?#pragma setlocaleQ告诉编译器源程序用的~码Q?/p>
// 如果源程序的~码与当前默?ANSI ~码不一_ // 则需要此行,~译时用来指明当前源E序使用的编?/font> #pragma setlocale(".936") // UNICODE 字符串常量,内容长度 10 字节 wchar_t wsz[20] = L"中文123"; |
以上需要注?#pragma setlocale ?setlocale(LC_ALL, "") 的作用是不同的,#pragma setlocale 在编译时起作用,setlocale() 在运行时起作用?/p>
![]() | |||
|
字符串类 String 中的内容?UNICODE 字符Ԍ
// Java 代码Q直接写中文 String string = "中文123"; // 得到长度?5Q因为是 5 个字W?/span> System.out.println(string.length()); |
字符?I/O 操作Q字W与字节转换操作。在 Java ?java.io.* 中,以“Stream”结cM般是用来操作“字节串”的c,以“Reader”,“Writer”结cM般是用来操作“字W串”的cR?/p>
// 字符串与字节串间怺转化 // 按照 GB2312 得到字节Q得到多字节字符Ԍ byte [] bytes = string.getBytes("GB2312"); // 从字节按?GB2312 得到 UNICODE 字符?/span> string = newString(bytes, "GB2312"); // 要将 String 按照某种~码写入文本文gQ有两种ҎQ?br /> // W一U办法:?Stream cd入已l按照指定编码{化好的字节串 OutputStream os = new FileOutputStream("1.txt"); os.write(bytes); os.close(); // W二U办法:构造指定编码的 Writer 来写入字W串 Writer ow = new OutputStreamWriter(new FileOutputStream("2.txt"), "GB2312"); ow.write(string); ow.close(); /* 最后得到的 1.txt ?2.txt 都是 7 个字?*/ |
如果 java 的源E序~码与当前默?ANSI ~码不符Q则在编译的时候,需要指明一下源E序的编码。比如:
E:\>javac -encoding BIG5 Hello.java |
以上需要注意区分源E序的编码与 I/O 操作的编码,前者是在编译时起作用,后者是在运行时起作用?/p>
![]() | |||
|
对编码的误解 | |
误解一 | 在将“字节串”{化成“UNICODE 字符东y时Q比如在d文本文gӞ或者通过|络传输文本ӞҎ“字节串”简单地作ؓ单字节字W串Q采用每“一个字节”就是“一个字W”的Ҏq行转化?br /> 而实际上Q在非英文的环境中,应该“字节串”作?ANSI 字符Ԍ采用适当的编码来得到 UNICODE 字符Ԍ有可能“多个字节”才能得到“一个字W”?br /> 通常Q一直在英文环境下做开发的E序员们Q容易有q种误解?/td> |
误解?/td> | ?DOSQWindows 98 {非 UNICODE 环境下,字符串都是以 ANSI ~码的字节Ş式存在的。这U以字节形式存在的字W串Q必ȝ道是哪种~码才能被正地使用。这使我们Ş成了一个惯性思维Q“字W串的编码”?br /> ?UNICODE 被支持后QJava 中的 String 是以字符的“序号”来存储的,不是以“某U编码的字节”来存储的,因此已经不存在“字W串的编码”这个概念了。只有在“字W串”与“字节串”{化时Q或者,一个“字节串”当成一?ANSI 字符串时Q才有编码的概念?br /> 不少的h都有q个误解?/td> |
W一U误解,往往是导致ؕ码生的原因。第二种误解Q往往D本来ҎU正的ؕ码问题变得更复杂?/p>
![]() | |||
|
单介l一下常用的~码规则Qؓ后边的章节做一个准备。在q里Q我们根据编码规则的特点Q把所有的~码分成三类Q?/p>
分类 | ~码标准 | 说明 |
单字节字W编?/td> | ISO-8859-1 | 最单的~码规则Q每一个字节直接作Z?UNICODE 字符。比如,[0xD6, 0xD0] q两个字节,通过 iso-8859-1 转化为字W串Ӟ直接得?[0x00D6, 0x00D0] 两个 UNICODE 字符Q即 "ÖÐ"?br /> 反之Q将 UNICODE 字符串通过 iso-8859-1 转化为字节串Ӟ只能正常转化 0~255 范围的字W?/td> |
ANSI ~码 | GB2312, BIG5, Shift_JIS, ISO-8859-2 …?/td> | ?UNICODE 字符串通过 ANSI ~码转化为“字节串”时Q根据各自编码的规定Q一?UNICODE 字符可能转化成一个字节或多个字节?br /> 反之Q将字节串{化成字符串时Q也可能多个字节转化成一个字W。比如,[0xD6, 0xD0] q两个字节,通过 GB2312 转化为字W串Ӟ得?[0x4E2D] 一个字W,?'? 字?br /> “ANSI ~码”的特点Q?br />1. q些“ANSI ~码标准”都只能处理各自语言范围之内?UNICODE 字符?br />2. “UNICODE 字符”与“{换出来的字节”之间的关系是h定的?/td> |
UNICODE ~码 | UTF-8, UTF-16, UnicodeBig …?/td> | 与“ANSI ~码”类似的Q把字符串通过 UNICODE ~码转化成“字节串”时Q一?UNICODE 字符可能转化成一个字节或多个字节?br /> 与“ANSI ~码”不同的是: 1. q些“UNICODE ~码”能够处理所有的 UNICODE 字符?br />2. “UNICODE 字符”与“{换出来的字节”之间是可以通过计算得到的?/td> |
在这里,我们可以看到Q前面所讲的“误解一”,即采用每“一个字节”就是“一个字W”的转化ҎQ实际上也就{同于采?iso-8859-1 q行转化。因此,我们常常使用 bytes = string.getBytes("iso-8859-1") 来进行逆向操作Q得到原始的“字节串”。然后再使用正确?ANSI ~码Q比?string = new String(bytes, "GB2312")Q来得到正确的“UNICODE 字符东y?/p>
![]() | |||
|
?UNICODE E序中的字符Ԍ都是以某U?ANSI ~码形式存在的。如果程序运行时的语a环境与开发时的语a环境不同Q将会导?ANSI 字符串的昄p|?/p>
比如Q在日文环境下开发的?UNICODE 的日文程序界面,拿到中文环境下运行时Q界面上显CZؕ码。如果这个日文程序界面改为采?UNICODE 来记录字W串Q那么当在中文环境下q行Ӟ界面上将可以昄正常的日文?/p>
׃客观原因Q有时候我们必d中文操作pȝ下运行非 UNICODE 的日文YӞq时我们可以采用一些工P比如Q南极星QAppLocale {,暂时的模拟不同的语言环境?/p>
![]() | |||
|
当页面中的表单提交字W串Ӟ首先把字W串按照当前面的编码,转化成字节串。然后再每个字节{化成 "%XX" 的格式提交到 Web 服务器。比如,一个编码ؓ GB2312 的页面,提交 "? q个字符串时Q提交给服务器的内容?"%D6%D0"?/p>
在服务器端,Web 服务器把收到?"%D6%D0" 转化?[0xD6, 0xD0] 两个字节Q然后再Ҏ GB2312 ~码规则得到 "? 字?/p>
?Tomcat 服务器中Qrequest.getParameter() 得到qӞ常常是因为前面提到的“误解一”造成的。默认情况下Q当提交 "%D6%D0" l?Tomcat 服务器时Qrequest.getParameter() 返?[0x00D6, 0x00D0] 两个 UNICODE 字符Q而不是返回一?"? 字符。因此,我们需要?bytes = string.getBytes("iso-8859-1") 得到原始的字节串Q再?string = new String(bytes, "GB2312") 重新得到正确的字W串 "??/p>
![]() | |||
|
通过数据库客LQ比?ODBC ?JDBCQ从数据库服务器中读取字W串Ӟ客户端需要从服务器获知所使用?ANSI ~码。当数据库服务器发送字节流l客LӞ客户端负责将字节按照正的~码转化?UNICODE 字符丌Ӏ?/p>
如果从数据库d字符串时得到qQ而数据库中存攄数据又是正确的,那么往往q是因ؓ前面提到的“误解一”造成的。解决的办法q是通过 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的字节串Q再重新使用正确的编码{化成字符丌Ӏ?/p>
![]() | |||
|
当一D?Text 或?HTML 通过电子邮g传送时Q发送的内容首先通过一U指定的字符~码转化成“字节串”,然后再把“字节串”通过一U指定的传输~码QContent-Transfer-EncodingQ进行{化得到另一东y字节串”。比如,打开一电子邮件源代码Q可以看到类似的内容Q?/p>
Content-Type: text/plain; charset="gb2312" Content-Transfer-Encoding: base64 sbG+qcrQuqO17cf4yee74bGjz9W7+b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg== |
最常用?Content-Transfer-Encoding ?Base64 ?Quoted-Printable 两种。在对二q制文g或者中文文本进行{化时QBase64 得到的“字节串”比 Quoted-Printable 更短。在对英文文本进行{化时QQuoted-Printable 得到的“字节串”比 Base64 更短?/p>
邮g的标题,用了一U更短的格式来标注“字W编码”和“传输编码”。比如,标题内容?"?Q则在邮件源代码中表CZؓQ?/p>
// 正确的标题格?/span> Subject: =?GB2312?B?1tA=?= |
其中Q?/p>
如果“传输编码”改?Quoted-PrintableQ同P如果标题内容?"?Q?/p>
// 正确的标题格?/span> Subject: =?GB2312?Q?=D6=D0?= |
如果阅读邮g时出Cؕ码,一般是因ؓ“字W编码”或“传输编码”指定有误,或者是没有指定。比如,有的发邮件组件在发送邮件时Q标?"?Q?/p>
// 错误的标题格?/span> Subject: =?ISO-8859-1?Q?=D6=D0?= |
q样的表C,实际上是明确指明了标题ؓ [0x00D6, 0x00D0]Q即 "ÖÐ"Q而不?"??/p>
![]() | |||
|
非也。iso-8859-1 只是单字节字W集中最单的一U,也就是“字节编号”与“UNICODE 字符~号”一致的那种~码规则。当我们要把一个“字节串”{化成“字W串”,而又不知道它是哪一U?ANSI ~码Ӟ先暂时地把“每一个字节”作为“一个字W”进行{化,不会造成信息丢失。然后再使用 bytes = string.getBytes("iso-8859-1") 的方法可恢复到原始的字节丌Ӏ?/p>
Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符Ԍ不是 ANSI 字符丌Ӏ我们只需要把字符串作为“抽象的W号的串”来看待。因此不存在字符串的内码的问题?/p>