字符集编码详解

来源:互联网 发布:淘宝格子铺免费推广 编辑:程序博客网 时间:2024/05/16 04:24

http://blog.chinaunix.net/uid-90129-id-132794.html

 

参考文章一

 

字符,字节和编码

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/XPLinuxJava

字符串在内存中的存放方法:
 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 个序号:

2D

4E

87

65

31

00

32

00

33

00

00

00

       x86 CPU 中,低字节在前

1

2

3

\\0

一共占 10 个字节。

[img][/img]

 

 

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

概念描述

举例

字符

人们使用的记号,抽象意义上的一个符号。

\'1\', \'\', \'a\', \'$\', \'\', ……

字节

计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。

0x01, 0x45, 0xFA, ……

ANSI
字符串

在内存中,如果字符是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串

中文123
(占7字节)

UNICODE
字符串

在内存中,如果字符是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串

L中文123
(占10字节)

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

[img][/img]

 

 

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

1.       使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含字符的集合就叫做字符集

2.       规定每个字符分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做编码

各个国家和地区在制定编码标准的时候,字符的集合编码一般都是同时制定的。因此,平常我们所说的字符集,比如:GB2312, GBK, JIS 等,除了有字符的集合这层含义外,同时也包含了编码的含义。
UNICODE 
字符集包含了各种语言中使用到的所有字符。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。

[img][/img]

 

 

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

分类

编码标准

说明

单字节字符编码

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 
字符转换出来的字节之间是可以通过计算得到的。

我们实际上没有必要去深究每一种编码具体把某一个字符编码成了哪几个字节,我们只需要知道编码的概念就是把字符转化成字节就可以了。对于“UNICODE 编码,由于它们是可以通过计算得到的,因此,在特殊的场合,我们可以去了解某一种“UNICODE 编码是怎样的规则。

[img][/img]

 

 

2. 字符与编码在程序中的实现2.1 程序中的字符与字节在 C++  Java 中,用来代表字符字节的数据类型,以及进行编码的方法:

类型或操作

C++

Java

字符

wchar_t

char

字节

char

byte

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 函数。

[img][/img]

 

 

2.2 C++ 中相关实现方法声明一段字符串常量:

// ANSI 字符串,内容长度 7 字节
char     sz[20] = 中文123;

// UNICODE 字符串,内容长度 5  wchar_t10 字节)
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() 在运行时起作用。

[img][/img]

 

 

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 操作的编码,前者是在编译时起作用,后者是在运行时起作用。

[img][/img]

 

 

3. 几种误解,以及乱码产生的原因和解决办法3.1 容易产生的误解

对编码的误解

误解一

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

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

通常,一直在英文环境下做开发的程序员们,容易有这种误解。

误解二

 DOSWindows 98 等非 UNICODE 环境下,字符串都是以 ANSI 编码的字节形式存在的。这种以字节形式存在的字符串,必须知道是哪种编码才能被正确地使用。这使我们形成了一个惯性思维:字符串的编码

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

不少的人都有这个误解。

第一种误解,往往是导致乱码产生的原因。第二种误解,往往导致本来容易纠正的乱码问题变得更复杂。
在这里,我们可以看到,其中所讲的误解一,即采用每一个字节就是一个字符的转化方法,实际上也就等同于采用 iso-8859-1 进行转化。因此,我们常常使用 bytes = string.getBytes(iso-8859-1) 来进行逆向操作,得到原始的字节串。然后再使用正确的 ANSI 编码,比如 string = new String(bytes, GB2312),来得到正确的“UNICODE 字符串

[img][/img]

 

 

3.2  UNICODE 程序在不同语言环境间移植时的乱码非 UNICODE 程序中的字符串,都是以某种 ANSI 编码形式存在的。如果程序运行时的语言环境与开发时的语言环境不同,将会导致 ANSI 字符串的显示失败。
比如,在日文环境下开发的非 UNICODE 的日文程序界面,拿到中文环境下运行时,界面上将显示乱码。如果这个日文程序界面改为采用 UNICODE 来记录字符串,那么当在中文环境下运行时,界面上将可以显示正常的日文。
由于客观原因,有时候我们必须在中文操作系统下运行非 UNICODE 的日文软件,这时我们可以采用一些工具,比如,南极星,AppLocale 等,暂时的模拟不同的语言环境。

[img][/img]

 

 

3.3 网页提交字符串当页面中的表单提交字符串时,首先把字符串按照当前页面的编码,转化成字节串。然后再将每个字节转化成 %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) 重新得到正确的字符串 中。

[img][/img]

 

 

3.4 从数据库读取字符串通过数据库客户端(比如 ODBC  JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。
如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的误解一造成的。解决的办法还是通过 string = new String( string.getBytes(iso-8859-1), GB2312) 的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。

[img][/img]

 

 

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 = string.getBytes(iso-8859-1) 的方法可恢复到原始的字节串。
误解:“Java 中,怎样知道某个字符串的内码?”Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为抽象的符号的串来看待。因此不存在字符串的内码的问题。

 

参考文章二

 

Unicode字符编码规范


http://www.aoxiang.org 2006-4-2 10:48:02 
Unicode是一种字符编码规范 

  先从ASCII说起。ASCII是用来表示英文字符的一种编码规范,每个ASCII字符占用1个字节(8bits 

  因此,ASCII编码可以表示的最大字符数是256,其实英文字符并没有那么多,一般只用前128个(最高位为0),其中包括了控制字符、数字、大小写字母和其他一些符号 


  而最高位为1的另128个字符被成为“扩展ASCII”,一般用来存放英文的制表符、部分音标字符等等的一些其他符号,这种字符编码规范显然用来处理英文没有什么问题。(实际上也可以用来处理法文、德文等一些其他的西欧字符,但是不能和英文通用),但是面对中文、阿拉伯文之类复杂的文字,255个字符显然不够用 

  于是,各个国家纷纷制定了自己的文字编码规范,其中中文的文字编码规范叫做“GB2312-80”,它是和ASCII兼容的一种编码规范,其实就是利用扩展ASCII没有真正标准化这一点,把一个中文字符用两个扩展ASCII字符来表示。 

  但是这个方法有问题,最大的问题就是,中文文字没有真正属于自己的编码,因为扩展ASCII码虽然没有真正的标准化,但是PC里的ASCII码还是有一个事实标准的(存放着英文制表符),所以很多软件利用这些符号来画表格。这样的软件用到中文系统中,这些表格符就会被误认作中文字,破坏版面。而且,统计中英文混合字符串中的字数,也是比较复杂的,我们必须判断一个ASCII码是否扩展,以及它的下一个ASCII是否扩展,然后才“猜”那可能是一个中文字 


  总之当时处理中文是很痛苦的。而更痛苦的是GB2312是国家标准,台湾当时有一个Big5编码标准,很多编码和GB是相同的,所以……,嘿嘿。 

  这时候,我们就知道,要真正解决中文问题,不能从扩展ASCII的角度入手,也不能仅靠中国一家来解决。而必须有一个全新的编码系统,这个系统要可以将中文、英文、法文、德文……等等所有的文字统一起来考虑,为每个文字都分配一个单独的编码,这样才不会有上面那种现象出现。 

  于是,Unicode诞生了。 

  Unicode有两套标准,一套叫UCS-2(Unicode-16),用2个字节为字符编码,另一套叫UCS-4(Unicode-32),用4个字节为字符编码。 

  以目前常用的UCS-2为例,它可以表示的字符数为2^16=65535,基本上可以容纳所有的欧美字符和绝大部分的亚洲字符 


  UTF-8的问题后面会提到 

  在Unicode里,所有的字符被一视同仁。汉字不再使用“两个扩展ASCII”,而是使用“1Unicode”,注意,现在的汉字是“一个字符”了,于是,拆字、统计字数这些问题也就自然而然的解决了 


  但是,这个世界不是理想的,不可能在一夜之间所有的系统都使用Unicode来处理字符,所以Unicode在诞生之日,就必须考虑一个严峻的问题:和ASCII字符集之间的不兼容问题。 

  我们知道,ASCII字符是单个字节的,比如“A”的ASCII65。而Unicode是双字节的,比如“A”的Unicode0065,这就造成了一个非常大的问题:以前处理ASCII的那套机制不能被用来处理Unicode 


  另一个更加严重的问题是,C语言使用'\0'作为字符串结尾,而Unicode里恰恰有很多字符都有一个字节为0,这样一来,C语言的字符串函数将无法正常处理Unicode,除非把世界上所有用C写的程序以及他们所用的函数库全部换掉 


  于是,比Unicode更伟大的东东诞生了,之所以说它更伟大是因为它让Unicode不再存在于纸上,而是真实的存在于我们大家的电脑中。那就是:UTF

  UTF= UCS Transformation Format UCS转换格式,它是将Unicode编码规则和计算机的实际编码对应起来的一个规则。现在流行的UTF2种:UTF-8UTF-16 


  其中UTF-16和上面提到的Unicode本身的编码规范是一致的,这里不多说了。而UTF-8不同,它定义了一种“区间规则”,这种规则可以和ASCII编码保持最大程度的兼容 


  UTF-8有点类似于Haffman编码,它将Unicode编码为00000000-0000007F的字符,用单个字节来表示; 

    00000080-000007FF的字符用两个字节表示 

    00000800-0000FFFF的字符用3字节表示 

  因为目前为止Unicode-16规范没有指定FFFF以上的字符,所以UTF-8最多是使用3个字节来表示一个字符。但理论上来说,UTF-8最多需要用6字节表示一个字符。 

  在UTF-8里,英文字符仍然跟ASCII编码一样,因此原先的函数库可以继续使用。而中文的编码范围是在0080-07FF之间,因此是2个字节表示(但这两个字节和GB编码的两个字节是不同的),用专门的Unicode处理类可以对UTF编码进行处理。 

  下面说说中文的问题。 

  由于历史的原因,在Unicode之前,一共存在过3套中文编码标准。 

  GB2312-80,是中国大陆使用的国家标准,其中一共编码了6763个常用简体汉字。Big5,是台湾使用的编码标准,编码了台湾使用的繁体汉字,大概有8千多个。HKSCS,是中国香港使用的编码标准,字体也是繁体,但跟Big5有所不同。 

  这3套编码标准都采用了两个扩展ASCII的方法,因此,几套编码互不兼容,而且编码区间也各有不同 

  因为其不兼容性,在同一个系统中同时显示GBBig5基本上是不可能的。当时的南极星、RichWin等等软件,在自动识别中文编码、自动显示正确编码方面都做了很多努力。

  他们用了怎样的技术我就不得而知了,我知道好像南极星曾经以同屏显示繁简中文为卖点。 

  后来,由于各方面的原因,国际上又制定了针对中文的统一字符集GBKGB18030,其中GBK已经在WindowsLinux等多种操作系统中被实现。 

  GBK兼容GB2312,并增加了大量不常用汉字,还加入了几乎所有的Big5中的繁体汉字。但是GBK中的繁体汉字和Big5中的几乎不兼容。 

  GB18030相当于是GBK的超集,比GBK包含的字符更多。据我所知目前还没有操作系统直接支持GB18030 

  谈谈Unicode编码,简要解释UCSUTFBMPBOM等名词
  这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级。整理这篇文章的动机是两个问题:

  问题一:
  使用Windows记事本的“另存为”,可以在GBKUnicodeUnicode big 
endian
UTF-8这几种编码方式间相互转换。同样是txt文件,Windows是怎样识别编码方式的呢?

  我很早前就发现UnicodeUnicode big endianUTF-8编码的txt文件的开头会多出几个字节,分别是FFFEUnicode,FEFFUnicode big endian,EFBBBFUTF-8)。但这些标记是基于什么标准呢?

  问题二:
  最近在网上看到一个ConvertUTF.c,实现了UTF-32UTF-16UTF-8这三种编码方式的相互转换。对于Unicode(UCS2)GBKUTF-8这些编码方式,我原来就了解。但这个程序让我有些糊涂,想不起来UTF-16UCS2有什么关系。
查了查相关资料,总算将这些问题弄清楚了,顺带也了解了一些Unicode的细节。写成一篇文章,送给有过类似疑问的朋友。本文在写作时尽量做到通俗易懂,但要求读者知道什么是字节,什么是十六进制。

  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.0
ISO的最新标准是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编码是594E,“乙”的Unicode编码是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)


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

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

 

参考文章三

 

  • 计算机只有数字,计算机软件里的一切都是用数字来表示的,屏幕上显示的一个个字符也不例外。                                                                                   
  • 字符a对应数字97,字符b对应数字98等,这种字符与数字对应的编码规则被称为ASCII(美国标准信息交换码)。ASCII的最高bit位都为0,也就是说这些数字都在0到127之间。
  • 中国大陆将每一个中文字符都用两个字节的数字来表示,中文字符的每个字节的最高位都为1,中国大陆为每个中文字符制定的编码规则称为GB2312(国标码)。
  • 在GB2312的基础上,对更多的中文字符(包括繁体)进行了编码,新的编码规则称为GBK。
  • 在中国大陆使用的计算机系统上,GBK和GB2312就被称为该系统的本地字符集。
  • “中国”的“中”字,在中国大陆的编码是十六进制的D6DO,而在中国台湾的编码是十六进制的A4A4,台湾地区对中文字符集的编码规则称为BIG5(大五码)。
  • 在一个国家的本地化系统中出现的一个字符,通过电子邮件传送到另外一个国家的本地化系统中,看到的就不是那个字符了,而是另个那个国家的一个字符或乱码。

 

Unicode编码(是一种全球通用的字符编码)

  • ISO(国际标准化组织)将全世界所有的符号进行了统一编码,称之为Unicode编码。
  • “中”这个符号,在全世界的任何角落始终对应的都是一个十六进制的数字4E2D。
  • 如果所用的计算机系统都使用Unicode编码,在中国大陆的本地化系统中显示的“中”这个符号,发送到伊拉克的本地化系统中,显示的仍然是“中“这个符号。
  • Unicode编码的字符都占用两个字节的大小,对于ASCII码所表示的字符,只是简单地在ASCII码原来占用的一个字节前面,增加一个所有bit为0的字节。
  • Unicode只占用两个字节,在全世界范围内所表示的字符个数不会超过2的16次方(65536),实际上,Unicode编码中还保留了两千多个数值没有用于字符编码。
  • 在相当长的一段时期内,本地化字符编码将与Unicode编码共存。
  • Java中的字符使用的都是Unicode编码,Java在通过Unicode保证跨平台特性的前提下,也支持本地平台字符集。

UTF-8编码

 

ASCII码字符保持原样,仍然只占用一个字节,对于其它国家的字符,UTF-8使用两个或三个字节来表示。使用UTF-8编码的文件,通常都要用EF BB BF作为文件开头的三个字节数据。

字符的UTF-8编码与Unicode编码之间的转换关系对应下列规则:

      -  \u0001和\u007f之间的字符,UTF-8编码为:(byte)c.

      -  \u0000或其范围在\u0080和\u07ff之间的字符,UTF-8编码为:

              (byte)(0xc0|(0x1f&(c>>6))),(byte)(0x80|(0x3f&c)).

      -  \u0800和\uffff之间的字符,UTF-8编码为:

              (byte)(0xe0|0x0f&(c>>12)))),(byte)(0x80|(0x3f&(c>>6))),(byte)(0x80|(0x3f&c))

从上图可以看出,应用程序软件很容易根据UTF-8编码中那些固定不变的比特值来确定一个字符占用的是一个字节呢,还是两个或是三个字节的,如果一个字节的第一个比特位为“0”,那么说明这个字符只占用一个字节;如果一个字节的前三个比特为“110”,这说明这个字符占用两个字节;如果一个字节的前四个比特为“1110”,这说明这个字符占用三个字节。对于需要二个或三个字节表示的UTF-8字符,它们的第二个和第三个字节的前两个比特位总是“10”。这样很容易与UTF-8中只占用一个字节的字符相区分,非常便于应用程序检测数据在传输过程中是不是发生了错误。

 

相对Unicode编码,UTF-8有一些显著的优点:

  • 不出现内容为0x00字节(字节内容全为“0”)
  • 便于应用程序检测数据在传输过程中是否发了错误
  • 直接处理使用ASCII码的英文文档

 

UTF-8的缺点:

  • 其中有些字符需要使用三个字节,是Unicode编码的1.5倍,由其是对中日韩字符如此

使用UTF-8编码的文件,通常都要用EF BB BF作为文件开头的三个字节数据。

 

UTF-16编码(两个字节或四个字节)

  • UTF-16编码在Unicode基础上进行了一些细节上的扩充,增加了对Unicode编码没有包括的那些字符的表示方式。
  • UTF-16对Unicode的扩充并没有影响Unicode编码所包括的那些字符,只是增加了对Unicode编码没有包括的那些字符的表示方式,一个使用Unicode编码的字符就是UTF-16格式的。
  • Unicode编码将OXD800-0XDFFF区间的数值保留出来,UTF-16扩充的字符,占用四个字节,前面两个字节的数值为OXD800-OXD8FF之间,后面两个字符的数值为OXDC00-OXDFFF之间。
  • 为什么不让前面和后面的两个字节的数值都位于0XD800-OXDFFF之间呢?方便用于判断字符的边界。
  • 在不同体系结构的计算机系统中,UTF-16编码的Unicode字符在内存中的字节存储顺序不同。
  • 对于0X1234这样的一个双字节数据,使用Little-Endian和Big-Endian两种方式在内存中存储的格式,如图所示:

  • 如果文件以OXFF OXFF这两个字节开头,则表明文本的其余部分是Big-Endian的UTF-16编码;如果文件以OXFF OXFE这两个字节开头,则表明文本的其余部分是Little-Endian的UTF-16编码。

 

 

参考文章四

 

ISO-8859-1

来自ITwiki,开放的信息技术大百科

Jump to: navigation<jumptoSearch>

ISO/IEC 8859-1,又称Latin-1西欧语言,是国际标准化组织ISO/IEC 8859的第一个8位字符集。它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,藉以供使用变音符号拉丁字母语言使用。

此字符集支援部分于欧洲使用的语言,包括阿尔巴尼亚语巴斯克语布列塔尼语加泰罗尼亚语丹麦语荷兰语法罗语弗里西语加利西亚语德语格陵兰语冰岛语爱尔兰盖尔语意大利语拉丁语卢森堡语挪威语葡萄牙语里托罗曼斯语苏格兰盖尔语西班牙语瑞典语

英语虽然没有重音字母,但仍会标明为ISO 8859-1编码。除此之外,欧洲以外的部分语言,如南非荷兰语斯瓦希里语印尼语马来语、菲律宾他加洛语等也可使用ISO 8859-1编码。

法语及芬兰语本来也使用ISO 8859-1来表示。但因它没有法语使用的 œŒ Ÿ 三个字母及芬兰语使用的 ŠšŽž ,故于1998年被ISO/IEC 8859-15所取代。(ISO 8859-15同时加入了欧元符号)

ISO/IEC 8859-1

 

-0

-1

-2

-3

-4

-5

-6

-7

-8

-9

-A

-B

-C

-D

-E

-F

0-

 

1-

 

2-

SP

!

"

#

$

%

&

'

(

)

*

+

,

-

.

/

3-

0

1

2

3

4

5

6

7

8

9

:

;

=

?

4-

@

A

B

C

D

E

F

G

H

I

J

K

L

M

N

O

5-

P

Q

R

S

T

U

V

W

X

Y

Z

[

\

]

^

_

6-

`

a

b

c

d

e

f

g

h

i

j

k

l

m

n

o

7-

p

q

r

s

t

u

v

w

x

y

z

{

|

}

~

 

8-

 

9-

 

A-

NBSP

¡

¢

£

¤

¥

¦

§

¨

©

ª

«

¬

SHY

®

¯

B-

°

±

²

³

´

µ

·

¸

¹

º

»

¼

½

¾

¿

C-

À

Á

Â

Ã

Ä

Å

Æ

Ç

È

É

Ê

Ë

Ì

Í

Î

Ï

D-

Ð

Ñ

Ò

Ó

Ô

Õ

Ö

×

Ø

Ù

Ú

Û

Ü

Ý

Þ

ß

E-

à

á

â

ã

ä

å

æ

ç

è

é

ê

ë

ì

í

î

ï

F-

ð

ñ

ò

ó

ô

õ

ö

÷

ø

ù

ú

û

ü

ý

þ

ÿ

在上表中,0x20空格0xA0是不换行空格、0xAD是选择性連接號

0x00-0x1F0x7F0x80-0x9F在此字符集中未有定义。(控制字符是由ISO 6429ISO 4873定义)