【天堂论坛】玩机到天堂 买机找海洋西门子 6688……黑白经典.MP3机王 → [转贴]关于编码的一些知识
查看完整版本:[转贴]关于编码的一些知识
2006/4/25 11:07:01

汉字编码

GB2312
范围: 0xA1A1 - 0xFEFE
汉字范围: 0xB0A1 - 0xF7FE

GBK
范围: 0x8140 - 0xFEFE

BIG5
范围: 0xA140 - 0xF9FE, 0xA1A1 - 0xF9FE

详细信息:

编码 第一个字节 第二个字节 第三个字节 第四个字节
GB2312 0xB0 - 0xF7 0xA0 - 0xFE
GBK 0x81 - 0xFE 0x40 - 0xFE
GB18030 的双字节 0x81 - 0xFE 0x40 - 0x7E, 0x80 - 0xFE
GB18030 的四字节 0x81 - 0xFE 0x30 - 0x39 0x81 - 0xFE 0x30 - 0x39

GB2312:
GB2312码是中华人民共和国国家汉字信息交换用编码,全称《信息交换用汉字编码字符集--基本集》,由国家标准总局发布,1981年5月1日实施,通行于大陆。新加坡等地也使用此编码。

GB2312收录简化汉字及符号、字母、日文假名等共7445个图形字符,其中汉字占6763个。GB2312规定“对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示”,习惯上称第一个字节为“高字节”,第二个字节为“低字节”。GB2312-80包含了大部分常用的一、二级汉字,和9区的符号。该字符集是几乎所有的中文系统和国际化的软件都支持的中文字符集,这也是最基本的中文字符集。其编码范围是高位0xa1-0xfe,低位也是0xa1-0xfe;汉字从0xb0a1开始,结束于0xf7fe。

GB2312将代码表分为94个区,对应第一字节(0xa1-0xfe);每个区94个位(0xa1-0xfe),对应第二字节,两个字节的值分别为区号值和位号值加32(2OH),因此也称为区位码。01-09区为符号、数字区,16-87区为汉字区(0xb0-0xf7),10-15区、88-94区是有待进一步标准化的空白区。GB2312将收录的汉字分成两级:第一级是常用汉字计3755个,置于16-55区,按汉语拼音字母/笔形顺序排列;第二级汉字是次常用汉字计3008个,置于56-87区,按部首/笔画顺序排列。故而GB2312最多能表示6763个汉字。

GB2312的编码范围为2121H-777EH,与ASCII有重叠,通行方法是将GB码两个字节的最高位置1以示区别。

BIG5:
每个字由两个字节组成,其第一字节编码范围为0xA1~0xF9,第二字节编码范围为0x40~0x7E与0xA1~0xFE,总计收入13868个字(包括5401个常用字、7652 个次常用字、7个扩充字、以及808个各式符号),其中可以大致划分为以下几个字区:

第一字节 第二字节 字区 制定
A1..A2 40..7E, A1..FE 各种符号区 1984
A3 40..7E, A1..BF 各种符号区 (包括标点符号、ASCII 全角符号、注音符号等) 1984
A3 E1 欧元符号 CP950
A4..C5 40..7E, A1..FE 常用字区 1984
C6 40..7E 常用字区 1984
C6 A1..FE 罕用符号区 倚天
C7 40..7E, A1..FE 罕用符号区 (包括日文、俄文等) 倚天
C8 40..7E, A1..D3 罕用符号区 (包括俄文、输入法特殊符号等) 倚天
C9..F8 40..7E, A1..FE 次常用字区 1984
F9 40..7E, A1..D5 次常用字区 1984
F9 D6..DC 七个扩充字 倚天
F9 DD..FE 格符号区 倚天

扩充字 BIG5 码 Unicode 码 BIG5_1984 的同义字
0xF9D6 0x88CF
0xF9D7 0x92B9
0xF9D8 0x7CA7
0xF9D9 0x58BB
0xF9DA 0x6052
0xF9DB 0x7881
0xF9DC 0x5AFA

GBK:

起源

GB2312-80 仅收汉字 6763 个,这大大少于现有汉字,随着时间推移及汉字文化的不断延伸推广,有些原来很少用的字,现在变成了常用字,例如:朱鎔基的“鎔”字,未收入 GB2312-80,现在大陆的报业出刊只得使用(金+容)、(金容)、(左金右容)等来表示,形式不一而同,这使得表示、存储、输入、处理都非常不方便,对于搜索引擎等软件的构造来说也不是好消息,而且这种表示没有统一标准。从我们对人民日报 98 年数据的处理过程中,得出这样的经验:回填外字最困难的就是如何得到这种表示方法的集合。

为了解决这些问题,以及配合 UNICODE 的实施,全国信息技术化技术委员会于 1995 年 12 月 1 日《汉字内码扩展规范》。GBK 向下与 GB2312 完全兼容,向上支持 ISO-10646 国际标准,在前者向后者过渡过程中起到的承上启下的作用。

GBK是GB2312-80的扩展,是向上兼容的。它包含了20902个汉字,其编码范围是0x8140-0xfefe,剔除高位0x80的字位。其所有字符都可以一对一映射到Unicode2.0。

字集

GBK 共收入21886个汉字和图形符号,包括:

  • GB2312 中的全部汉字、非汉字符号。
  • BIG5 中的全部汉字。
  • 与 ISO-10646 相应的国家标准 GB13000 中的其它 CJK 汉字,以上合计 20902 个汉字。
  • 其它汉字、部首、符号,共计 984 个。

GBK 编码区分三部分:

  • 汉字区 包括
    • GBK/2:OXBOA1-F7FE, 收录 GB2312 汉字 6763 个,按原序排列;
    • GBK/3:OX8140-AOFE,收录 CJK 汉字 6080 个;
    • GBK/4:OXAA40-FEAO,收录 CJK 汉字和增补的汉字 8160 个。
  • 图形符号区 包括
    • GBK/1:OXA1A1-A9FE,除 GB2312 的符号外,还增补了其它符号
    • GBK/5:OXA840-A9AO,扩除非汉字区。
  • 用户自定义区
    • 即 GBK 区域中的空白区,用户可以自己定义字符。
編碼

GBK 亦采用双字节表示,总体编码范围为 8140-FEFE 之间,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 XX7F 一条线。

微软公司自 Windows 95 简体中文版开始支持 GBK 代码,標準叫法是 Windows codepage 936,也叫做 GBK(國標擴展),它也是 8-bit 的變長編碼。據我所知 GBK 從來沒成爲過正式的國家標準,只不過因爲 Windows 的普及,它已經成爲事實上的標準了。但目前的多数搜索引擎都不能很好地支持 GBK 汉字。

from: http://www.unihan.com.cn/cjk/ana17.htm

由前电子部科技质量司和国家技术监督局标准化司于1995年12月颁布的指导性规范。(GBK的 K是“扩展”的汉语拼音第一个字母)
GBK作为非 UCS ( ISO/IEC 10646 ) 体系的代码页,适用于中文信息的处理、交换、存储、传输、显现、输入和输出。
GBK与国家标准 GB 2312-80 信息处理交换码所对应的、事实上的内码标准兼容;同时,在字汇一级支持 ISO/IEC 10646-1 和GB 13000-1 的全部中日韩 (CJK) 汉字(20902字)。GBK除了包含GB2312-80 和GB12345-90中包括的全部非汉字符号外,还涵盖我国台湾地区中文标准交换码TCA-CNS 11643 -92 ( 与其对应的内码为Big5;以下用Big5泛指二者。) 中的绝大多数符号。
从Windows95中文版起,Windows NT 3.51, 4.0, Windows2000, Windows CE, Linux已经全面支持GBK,起到了从GB 2312向Unicode过渡的承上启下的重要作用。
GBK尽管在字汇一级支持CJK,是目前最大的Code Page ;它在体系结构、代码空间上,仍然是完全不同于ISO/IEC 10646 和Unicode的。

GB18030:

GB18030-2000(GBK2K)在GBK的基础上进一步扩展了汉字,增加了藏、蒙等少数民族的字形。GBK2K从根本上解决了字位不够,字形不足的问题。它有几个特点:

它并没有确定所有的字形,只是规定了编码范围,留待以后扩充。

编码是变长的,其二字节部分与GBK兼容;四字节部分是扩充的字形、字位,其编码范围是首字节0x81-0xfe、二字节0x30-0x39、三字节0x81-0xfe、四字节0x30-0x39。

它的推广是分阶段的,首先要求实现的是能够完全映射到Unicode3.0标准的所有字形。

它是国家标准,是强制性的。

 

中文信息编码标准,常用的是GB2312-1980,GB12345,GB13000(GBK),
以及最新标准GB18030。

GB2312的汉字编码规则为:第一个字节的值在0xB0到0xF7之间,第
二个字节的值在0xA0到0xFE之间。

GB12345和GB13000是对GB2312-1980的扩充,所有已经包含在GB2312
中的汉字编码不变,另外增加更多的码位。其编码规则大致为:第一
个字节的值在0x81到0xFE之间,第二个字节的值在0x40到0xFE之间。
由于GB13000是对GB2312的扩展,所以也被称为GBK。

GB18030 是最新的汉字编码字符集国家标准, 向下兼容 GBK 和 GB2312 标准。
GB18030 编码是一二四字节变长编码。 一字节部分从 0x0~0x7F 与 ASCII
编码兼容。 二字节部分, 首字节从 0x81~0xFE, 尾字节从 0x40~0x7E 以及
0x80~0xFE, 与 GBK标准基本兼容。 四字节部分,
第一字节从 0x81~0xFE, 第二字节从 0x30~0x39, 第三和第四字节的范围和前
两个字节分别相同。 四字节部分覆盖了从 0x0080 开始, 除去二字节部分已经
覆盖的所有 Unicode 3.1 码位。也就是说, GB18030 编码在码位空间上做到
了与 Unicode 标准一一对应,这一点与 UTF-8 编码类似。


常见的GB编码ISO-2202-CN, EUC-CN, HZ,和 GB系列,其中HZ码是为了在英文新闻
组中发中文文章由留学生自己发明的,现在已经没有用了.EUC的意思是Extended
Unix Code,ISO-2202-CN是7bit,EUC-CN是8bit

Big5是台湾的IIIT1984年发明的,CNS 11643-1992( Chinese National Standard)
是扩展版本,主要大家用的还是big5

Hong Kong GCCS是香港政府为big5加的3049个字,(Government Chinese Character Set)
香港增补字符集(HKSCS)是后来的标准,包括了Big5和ISO10646的编码,所以HKSCS的big5
版是补充了GCCS的增强版,ISO10646是UCS(universal character set),ISO是政府组织
Unicode是电脑业界组织,不过UCS和Unicode的字库一样


推荐看Ken Lunde写的CJKV Information Processing.

编码字数统计:

GB2312 6763个汉字
GB12345 6866个汉字
GBK 21003个汉字
GB18030 27000
Big5 13053
CNS11643 48,027

[此帖子已被 ambode 在 2006-4-25 12:59:50 编辑过]

2006/4/25 11:14:45

沙发!

PS:楼主把你今天发的几个关于编码的贴子都集中在这里吧。

我给你加精!!

[此帖子已被 seacore 在 2006-4-25 11:18:42 编辑过]

2006/4/25 12:30:39

Unicode版本和Ansi版本的软件的区别

首先,Ansi 是一个通用版本,可以运行在所有版本的 Windows
·
然后,Unicode 版只能运行在 Win2K/XP/2003 下,也就是说基于 NT5 或更高内核的 Windows

看起来 Unicode 版本好像没什么用,不过现在的重点就是在 Unicode 版:

·Ansi
版迄今仍有已知的 Bug 没有解决,同样版本的 Unicode 版本早就解决了
·
如果你正好运行在 Win2K/XP/2003 下,而且偏偏正好在你的区域和语言设置里面的几项不统一,例如里面的位置是中国,可是你常常要和国外的朋友交流,于是标准和格式选用了美国,哪怕仅仅是香港特别行政区,这时候 Ansi 版本的选项里面就全是乱码,而 Unicode 一切正常……这是编码的支持特性所决定的,别忘了 Win2K/XP/2003 的一个特色就是多区域多语言支持,Ansi 版本你享受不到这一点
·
如果你正好运行在其他语言版本的 Win2K/XP/2003 下,Ansi 版本将打不开中文文件名的文件,哪怕目录里面有中文也不行,Unicode 没事儿
·Ansi
版本内置的字幕引擎和 VSFilter 配合实现双字幕的时候经常无效,Unicode 版本没有任何问题

 

Unicode编码,简要解释UCSUTFBMPBOM等名词

0big endianlittle endian

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

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

我们一般将endian翻译成字节序,将big endianlittle 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

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

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

这里还有一些细节:

  • GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0
  • DBCS中,GB内码的存储格式始终是big endian,即高位在前。
  • GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBKGB18030的低字节最高位都可能不是1。不过这不影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。

2UnicodeUCSUTF

前面提到从ASCIIGB2312GBKGB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(更准确地说,是与ISO-8859-1兼容),与GB码不兼容。例如字的Unicode编码是6C49,而GB码是BABA

Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCSUCS可以看作是"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.0ISO的最新标准是10646-3:2003

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

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

3UCS-2UCS-4BMP

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

UCS-22^16=65536个码位,UCS-42^31=2147483648个码位。

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

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

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

4UTF编码

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

UCS-2编码(16进制)

UTF-8 字节流(二进制)

0000 - 007F

0xxxxxxx

0080 - 07FF

110xxxxx 10xxxxxx

0800 - FFFF

1110xxxx 10xxxxxx 10xxxxxx

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

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

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

5UTF的字节序和BOM

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

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

UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFEUCS中是不存在的字符,所以不应该出现在实际传输中。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)

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

  1. "Understanding Unicode A general introduction to the Unicode Standard" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a)
  2. "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)

[此帖子已被 ambode 在 2006-4-25 12:58:57 编辑过]

2006/4/25 13:02:49

 字符与编码

引言

“字符与编码”是一个被经常讨论的话题。即使这样,时常出现的乱码仍然困扰着大家。虽然我们有很多的办法可以用来消除乱码,但我们并不一定理解这些办法的内在原理。而有的乱码产生的原因,实际上由于底层代码本身有问题所导致的。因此,不仅是初学者会对字符编码感到模糊,有的底层开发人员同样对字符编码缺乏准确的理解。

1. 编码问题的由来,相关概念的理解

1.1 字符与编码的发展

从计算机对多国语言的支持角度看,大致可以分为三个阶段:

 系统内码说明系统
阶段一ASCII计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。英文 DOS
阶段二ANSI编码
(本地化)
为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。

不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。

不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
中文 DOS,中文 Windows 95/98,日文 Windows 95/98
阶段三UNICODE
(国际化)
为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。Windows NT/2000/XP,Linux,Java

字符串在内存中的存放方法:

在 ASCII 阶段,单字节字符串使用一个字节存放一个字符(SBCS)。比如,"Bob123" 在内存中为:

426F6231323300
Bob123\0

在使用 ANSI 编码支持多种语言阶段,每个字符使用一个字节或多个字节来表示(MBCS),因此,这种方式存放的字符也被称作多字节字符。比如,"中文123" 在中文 Windows 95 内存中为7个字节,每个汉字占2个字节,每个英文和数字字符占1个字节:

D6D0CEC431323300
123\0

在 UNICODE 被采用之后,计算机存放字符串时,改为存放每个字符在 UNICODE 字符集中的序号。目前计算机一般使用 2 个字节(16 位)来存放一个序号(DBCS),因此,这种方式存放的字符也被称作宽字节字符。比如,字符串 "中文123" 在 Windows 2000 下,内存中实际存放的是 5 个序号:

2D4E87653100320033000000     ← 在 x86 CPU 中,低字节在前
123\0 

一共占 10 个字节。

1.2 字符,字节,字符串

理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:

 概念描述举例
字符人们使用的记号,抽象意义上的一个符号。'1', '中', 'a', '$', '¥', ……
字节计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。0x01, 0x45, 0xFA, ……
ANSI
字符串
在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串"中文123"
(占7字节)
UNICODE
字符串
在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串L"中文123"
(占10字节)

由于不同 ANSI 编码所规定的标准是不相同的,因此,对于一个给定的多字节字符串,我们必须知道它采用的是哪一种编码规则,才能够知道它包含了哪些“字符”。而对于 UNICODE 字符串来说,不管在什么环境下,它所代表的“字符”内容总是不变的。

1.3 字符集与编码

各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的“字符”。比如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:

  1. 使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。
  2. 规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。

各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。

UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。

2. 字符与编码在程序中的实现

2.1 程序中的字符与字节

在 C++ 和 Java 中,用来代表“字符”和“字节”的数据类型,以及进行编码的方法:

类型或操作C++Java
字符wchar_tchar *
字节charbyte
ANSI 字符串char[]byte[]
UNICODE 字符串wchar_t[]String
字节串→字符串mbstowcs(), MultiByteToWideChar() *string = new String(bytes, "encoding")
字符串→字节串wcstombs(), WideCharToMultiByte()bytes = string.getBytes("encoding")

以上需要注意几点:

  1. Java 中的 char 代表一个“UNICODE 字符(宽字节字符)”,而 C++ 中的 char 代表一个字节。
  2. 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 = string.getBytes("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 容易产生的误解

 对编码的误解
误解一在将“字节串”转化成“UNICODE 字符串”时,比如在读取文本文件时,或者通过网络传输文本时,容易将“字节串”简单地作为单字节字符串,采用每“一个字节”就是“一个字符”的方法进行转化。

而实际上,在非英文的环境中,应该将“字节串”作为 ANSI 字符串,采用适当的编码来得到 UNICODE 字符串,有可能“多个字节”才能得到“一个字符”。

通常,一直在英文环境下做开发的程序员们,容易有这种误解。
误解二在 DOS,Windows 98 等非 UNICODE 环境下,字符串都是以 ANSI 编码的字节形式存在的。这种以字节形式存在的字符串,必须知道是哪种编码才能被正确地使用。这使我们形成了一个惯性思维:“字符串的编码”。

当 UNICODE 被支持后,Java 中的 String 是以字符的“序号”来存储的,不是以“某种编码的字节”来存储的,因此已经不存在“字符串的编码”这个概念了。只有在“字符串”与“字节串”转化时,或者,将一个“字节串”当成一个 ANSI 字符串时,才有编码的概念。

不少的人都有这个误解。

第一种误解,往往是导致乱码产生的原因。第二种误解,往往导致本来容易纠正的乱码问题变得更复杂。

3.2 常用的编码简介

简单介绍一下常用的编码规则,为后边的章节做一个准备。在这里,我们根据编码规则的特点,把所有的编码分成三类:

分类编码标准说明
单字节字符编码ISO-8859-1最简单的编码规则,每一个字节直接作为一个 UNICODE 字符。比如,[0xD6, 0xD0] 这两个字节,通过 iso-8859-1 转化为字符串时,将直接得到 [0x00D6, 0x00D0] 两个 UNICODE 字符,即 "ÖÐ"。

反之,将 UNICODE 字符串通过 iso-8859-1 转化为字节串时,只能正常转化 0~255 范围的字符。
ANSI 编码GB2312,
BIG5,
Shift_JIS,
ISO-8859-2 ……
把 UNICODE 字符串通过 ANSI 编码转化为“字节串”时,根据各自编码的规定,一个 UNICODE 字符可能转化成一个字节或多个字节。

反之,将字节串转化成字符串时,也可能多个字节转化成一个字符。比如,[0xD6, 0xD0] 这两个字节,通过 GB2312 转化为字符串时,将得到 [0x4E2D] 一个字符,即 '中' 字。

“ANSI 编码”的特点:
1. 这些“ANSI 编码标准”都只能处理各自语言范围之内的 UNICODE 字符。
2. “UNICODE 字符”与“转换出来的字节”之间的关系是人为规定的。
UNICODE 编码UTF-8,
UTF-16, UnicodeBig ……
与“ANSI 编码”类似的,把字符串通过 UNICODE 编码转化成“字节串”时,一个 UNICODE 字符可能转化成一个字节或多个字节。

与“ANSI 编码”不同的是:
1. 这些“UNICODE 编码”能够处理所有的 UNICODE 字符。
2. “UNICODE 字符”与“转换出来的字节”之间是可以通过计算得到的。

在这里,我们可以看到,前面所讲的“误解一”,即采用每“一个字节”就是“一个字符”的转化方法,实际上也就等同于采用 iso-8859-1 进行转化。因此,我们常常使用 bytes = string.getBytes("iso-8859-1") 来进行逆向操作,得到原始的“字节串”。然后再使用正确的 ANSI 编码,比如 string = new String(bytes, "GB2312"),来得到正确的“UNICODE 字符串”。

3.3 非 UNICODE 程序在不同语言环境间移植时的乱码

非 UNICODE 程序中的字符串,都是以某种 ANSI 编码形式存在的。如果程序运行时的语言环境与开发时的语言环境不同,将会导致 ANSI 字符串的显示失败。

比如,在日文环境下开发的非 UNICODE 的日文程序界面,拿到中文环境下运行时,界面上将显示乱码。如果这个日文程序界面改为采用 UNICODE 来记录字符串,那么当在中文环境下运行时,界面上将可以显示正常的日文。

由于客观原因,有时候我们必须在中文操作系统下运行非 UNICODE 的日文软件,这时我们可以采用一些工具,比如,南极星,AppLocale 等,暂时的模拟不同的语言环境。

3.4 网页提交字符串

当页面中的表单提交字符串时,首先把字符串按照当前页面的编码,转化成字节串。然后再将每个字节转化成 "%XX" 的格式提交到 Web 服务器。比如,一个编码为 GB2312 的页面,提交 "中" 这个字符串时,提交给服务器的内容为 "%D6%D0"。

在服务器端,Web 服务器把收到的 "%D6%D0" 转化成 [0xD6, 0xD0] 两个字节,然后再根据 GB2312 编码规则得到 "中" 字。

在 Tomcat 服务器中,request.getParameter() 得到乱码时,常常是因为前面提到的“误解一”造成的。默认情况下,当提交 "%D6%D0" 给 Tomcat 服务器时,request.getParameter() 将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,而不是返回一个 "中" 字符。因此,我们需要使用 bytes = string.getBytes("iso-8859-1") 得到原始的字节串,再用 string = new String(bytes, "GB2312") 重新得到正确的字符串 "中"。

3.5 从数据库读取字符串

通过数据库客户端(比如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。

如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的“误解一”造成的。解决的办法还是通过 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。

3.6 电子邮件中的字符串

当一段 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 = string.getBytes("iso-8859-1") 的方法可恢复到原始的字节串。

误解:“Java 中,怎样知道某个字符串的内码?”

Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。

[此帖子已被 ambode 在 2006-4-25 14:09:30 编辑过]

2006/4/25 14:01:09

不懂,但顶!!

2006/4/25 23:12:08
引用
原文由 lk1208 发表于 2006-4-25 14:01:09 :

不懂,但顶!!


Powered by BBSXP 2007 ACCESS © 1998-2024
Processed in 0.02 second(s)