Jade Dungeon

字符与编码

Unicode字符集

为了提高计算机的信息处理和交换功能,使得世界各国的文字都能在计算机中处理,从1984 年起,ISO组织就开始研究制定一个全新的标准:通用多八位(即多字节)编码字符集( Universal Multiple-Octet Coded Character Set),简称UCS。标准的编号为: ISO 10646。这一标准为世界各种主要语言的字符(包括简体及繁体的中文字)及附加符号, 编制统一的内码。

统一码(Unicode)是Universal Code的缩写,是由另一个叫「Unicode学术学会」(The Unicode Consortium)的机构制定的字符编码系统。Unicode与ISO 10646国际编码标准从 内容上来说是同步一致的。具体可参考:Unicode 。 Unicode编码是和字符表一一映射的。比如56DE代表汉字,这种映射关系是固定不变 的。通俗的说Unicode编码就是字符表的坐标,通过56DE就能找到汉字。Unicode 编码的实现包括UTF8、UTF16、UTF32等等。

Unicode本身定义的就是每个字符的数值,是字符和自然数的映射关系,而UTF-8或者 UTF-16甚至UTF-32则定义了如何在字节流中断字,是计算机领域的概念。

字符编码

ANSI

ANSI不代表具体的编码,它是指本地编码。比如在简体版windows上它表示GB2312编码,在 繁体版windows上它表示Big5编码,在日文操作系统上它表示JIS编码。所以如果您新建 了个文本文件并保存为ANSI编码,那么您现在应该知道这个文件的编码为本地编码。

UTF-8

Unicode与UTF-8转换

UCS-4编码(Unicode) UTF-8
U+00000000 - U+0000007F 0xxxxxxx
U+00000080 - U+000007FF 110xxxxx 10xxxxxx
U+00000800 - U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+00010000 - U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U+00200000 - U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U+04000000 - U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF8是采用变长的编码方式,为1~6个字节,但通常我们只把它看作单字节或三字节的实现 ,因为其它情况实在少见。UTF8编码通过多个字节组合的方式来显示,这是计算机处理 UTF8的机制,它是无字节序之分的,并且每个字节都非常有规律,详见上图,在此不再 详述。

通过上图我们知道,UTF-8编码为变长的编码方式,占1~6个字节,可通过Unicode编码值的 区间来判断,并且每个组成UTF8字符的字节都是有规律可循的。

本文只讨论UTF8和UTF16这两种编码。

UTF16

UTF16编码使用固定的2个字节来存储。因为是多字节存储,所以它的存储方式分为2种: 大端序和小端序。

大端存储

字节顺序标记(BOM)

如果你经常要在高低字节序的系统间转换文档,并且希望区分字节序,还有一种奇怪的约定,被称作BOM。BOM是一个设计得很巧妙的字符,用来放在文档的开头告诉阅读器该文档的字节序。在UTF-16中,它是通过在第一个字节放置FE FF来实现的。在不同字节序的文档中,它会被显示成FF FE或者FE FF,清楚的把这篇文档的字节序告诉了解释器。 BOM尽管很有用,但并不是很简洁,因为还有一个类似的概念,称作「魔术字」(Magic Byte),很多年来一直被用来表明文件的格式。BOM和魔术字间的关系一直没有被清楚的定义过,因此有的解释器会搞混它们。

恭喜你读到这里,你一定是一个很有耐心的读者。

UTF16编码是Unicode最直接的实现方式,通常我们在windows上新建文本 文件后保存为Unicode编码,其实就是保存为UTF16编码。UTF16编码在windows上采用 小端序的方式存储,以下我新建了个文本文件并保存为Unicode编码来测试,文件中只 输入了一个汉字,之后我用Editplus打开它,切换到十六进制方式查看,如图所示:

00000000 FF FE DE 56

我们看到有4个字节:

  • 前2个字节FF FE是文件头,表示这是一个UTF16编码的文件
  • DE 56则是'回'的UTF16编码的十六进制。

小端存储

我们经常使用的JavaScript语言,它内部就是采用UTF16编码,并且它的存储方式为大端序 ,来看一个例子:

console.group('Test Unicode: ');
console.log(('回'.charCodeAt(0)).toString(16).toUpperCase());   // 56DE

编码转换

UTF16转UTF-8

判断Unicode码所在的区间就可以得到这个字符在UTF-8中是由几个字节所组成。比如'回' 的Unicode码是0x56DE,它介于U+00000800 – U+0000FFFF之间,所以它是用三个字节 来表示的。

之后通过移位来实现。

  • 0x56DE中取出4位放在低位,并和二进制的1110结合,这就是第一个字节。
  • 0x56DE中剩下的字节中取出6位放在低位,并和二进制的10结合,是第二个字节。
  • 第三个字节依照类似的方式实现。
/* '回'的Unicode编码为:0x56DE,它介于U+00000800 – U+0000FFFF之间,
	 所以它占用三个字节。 U+00000800 – U+0000FFFF   1110xxxx 10xxxxxx 10xxxxxx */
var ucode = 0x56DE;

var byte1 = 0xE0 | ((ucode >> 12) & 0x0F); // 1110xxxx
var byte2 = 0x80 | ((ucode >> 6) & 0x3F);  // 10xxxxxx
var byte3 = 0x80 | (ucode & 0x3F);         // 10xxxxxx

var utf8 = String.fromCharCode(byte1) + String.fromCharCode(byte2) + 
	String.fromCharCode(byte3);
 
console.group('Test UTF16ToUTF8: ');
console.log(utf8);                    // å
console.groupEnd();

注意输出的结果是å。看起来像乱码,这是因为JavaScript不知道如何显示UTF8的字符。 您或许会说输出不正常转换还有什么用,但您应该知道转换的目的还经常用于传输或API的 需要。

UTF8转UTF16

这是UTF16转换到UTF8的逆转换,同样需要对照转换表来实现。还是接上一个例子,我们 已经得到了汉字'回'的UTF-8编码,这是三个字节的,我们只需要按照转换表来转成双字节 ,如图所示,我们需要保留下所有的x。

U+00000800 – U+0000FFFF   1110xxxx 10xxxxxx 10xxxxxx

Javascript代码:

// 接上部分代码

// 由三个字节组成,所以分别取出
var c1 = utf8.charCodeAt(0);
var c2 = utf8.charCodeAt(1);
var c3 = utf8.charCodeAt(2);
/* 需要通过判断特定位的方式来转换,但这里是已知是三个字节,所以忽略判断,而是
	 直接拿到所有的x,组成16位。
   U+00000800 – U+0000FFFF   1110xxxx 10xxxxxx 10xxxxxx */
   
// 丢弃第一个字节的高四位并和第二个字节的高四位组成一个字节
var b1 = (c1 << 4) | ((c2 >> 2) & 0x0F);
// 同理第二个字节和第三个字节组合
var b2 = ((c2 & 0x03) << 6) | (c3 & 0x3F);

// 将b1和b2组成16位
var ucode = ((b1 & 0x00FF) << 8) | b2;

console.group('Test UTF8ToUTF16: ');
console.log(ucode.toString(16).toUpperCase(), String.fromCharCode(ucode)); // 56DE 回
console.groupEnd();