计算机只能识别 0 和 1,任何数据格式,包括数字、文本、图像、音乐、视频等等都表示为一串由 0 和1 组成的二进制数据串。如何将一串二进制数据识别为有意义的数据类型,是软件层面的问题,是一种格式和约定。
ASCII编码
现代计算机的软硬件设计最早都是由一群以英语为母语的人设计的,英语只有52个字母(大小写区别对待),再加上常用的一些标点符号和控制符号(比如换行、回车等),也就一百来个左右。1个字节8个比特位,能表示256种状态,因此纯英文环境下的各种字符用1个字节就足够了。于是自然而然的,就诞生了ASCII 这个最早也是使用最广泛的单字节编码。
ASCII 编码使用1个字节的低7位,最高位为0,编码范围是00000000-01111111,用十进制表示的话就是0-127,其编码分布如下图所示。
Latin1 编码
ASCII 码表只包含了英文字母,显然是不够的。首先就是西欧国家,主要是拉丁语言。和英文一样,也是字母语言,字符不算多,大概几十个。于是ASCII最高的1位尚未使用的比特位就被利用起来了,编码范围是 10000000-11111111,即十进制的128-256,包含了拉丁字母和一些特殊符号,如欧元符号等,这就是 Latin1 编码。可以看出,Latin1 编码是 ASCII 的超集,一共能够表示 256个字符。
GB2312、GBK和GB18030编码
到了亚洲国家,情况就很复杂了。以中文为例,仅仅是最常使用,满足日常阅读书写的最少汉字,也要三千个左右,显然用单字节是无法容纳了。很自然地,人们就想,单字节不够用,那就双字节呗。
首先形成标准的是 GB2312。GB2312 采用的是变长码,即 ASCII 字符仍然用单字节表示,中文则用 2 个字节表示。具体的技术规定是:码值小于127的字符的意义和ASCII码相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1 到 0xF7,后面一个字节(低字节)从 0xA1到 0xFE。
但汉字的数量实际上是远大于这个数目的,再加上还有繁体字,GB2312 很快就无法满足需要了。于是 GBK 出现了,具体的技术方案从两个方面着手,一是把之前 GB2312 还没用完的码点继续用完,二是放宽了两个字符的码值都大于127才是中文的限制,只要第一个字符大于127就表示中文。GBK 是 GB2312 的超集,完全兼容 GB2312,收录了2万多个汉字及相关字符,包括繁体字。
到了 GBK,基本上沟通交流就不存在太大问题了,但仍然无法满足文化传播的需要,一些生僻字以及我国少数名族的字仍然无法表达。于是又引入了 GB18030 (全称《信息技术 中文编码字符集)。在 GBK 的基础上进一步扩大了覆盖范围,共收录七万多个汉字、少数民族字以及日语和韩语中的汉字。因为超过了65535 的两字节极限,GB18030 采用了变长编码,即一个字符有可能是1个字节(ASCII字符),2个字节(完全兼容GB2312,基本兼容GBK)和4个字节。
Unicode
为了适应本国需要,很多国家和地区都制定了自己的文字编码,要识别不同编码下的文字,就要安装相应的解码程序,很是不方便。浏览网页时常常出现的乱码基本上都是编码问题导致的。
为了解决这一问题,开发者就提出用一种编码统一对全世界的各种文字进行编码,给每一个字符一个唯一的码点(Code point),这就是 Unicode 。目前 Unicode 的编码空间共包含 0x10FFFF(1114111)个编码点,被划分为17个平面,每个平面包含0xFFFF 即 65535 个字符。其中最重要的是基本多文种平面(Basic Multilingual Plane),包含了各语种最常用的字符编码,码点范围为 0x0000-0xFFFF,其它平面称之为辅助平面。
从1991年发布的第一个版本开始,每一年都会有新的字符被编入Unicode中,最新的 Unicode 标准是14.0.0,共收录了 144,697 个字符。需要注意的是,Unicode 是一个符号集,一种规范,为每一个字符规定了唯一对应的数字,即码点。但并不是编码标准,因为不涉及如何存储这些码点,即前面我们所说的用几个字节存储一个字符,采用的具体技术方案等等。具体的编码标准目前主要有UTF-8、UTF-16和UTF-32。
UTF-32
UTF-32 是一种定长编码,用4个字节来统一表示 Unicode 字符。这个方案简单明了,码点值是多少,内存中就存多少。好处是每个字符的做占用的空间都是相同的,因此随机获取任意位置的字符就非常简单,直接在首字符的地址加上一个固定的偏移量就可以了,时间复杂度是O(1)。
UTF-32 的缺点也是显而易见的,那就是空间浪费。本来英文字母用1个字节就可以表示,现在同样需要4个字节。
UTF-16
UTF-16 是变长编码,用2个字节或者4个字节存储 Unicode 码点。
前面说过,Unicode 码点的范围是 0x000000~0x10FFFF。对于 0x0000~0xFFFF 即基本多语种平面的字符,UTF-16 用2个字节来表示,直接存储码点值。
超出 0xFFFF 码点的值,2字节存不下,采用4字节存储。那么4个字节如何表示一个 Unicode 码点呢?首先,不能直接存储码点值,因为如果直接存储码点值,前2个字节的值就有可能和基本多语言平面内的码点值相同而导致无法区分。其次,4个字节理论上能够存储42.9亿多个字符,但Unicode 规范只定义了 1114111 个字符,因为 2 的20次方大致是1,048,576,因此差不多只需要20位,即2个半字节就可以了。
具体采取的办法就是将超出 0xFFFF 的码点值分为前后两个部分,每个部分各10位。其中前面10位是基本多语种平面尚未分配的码点范围:即 0xD800~0xDBFF,共可以表示1024个状态。后面10位则规定为 0xDC00~0xDFFF,也可以表示1024个状态,合起来一共可以表示1024 * 1024 = 1,048,576 个字符,再加上基本多语言平面的字符,差不多等价于Unicode 码点范围,稍微少一点,影响不大。
UTF-8
UTF-8 也是一种变长码。编码规则是:如果首字节的高位为0,即码点在 0 - 127 之间,就是单字节编码(ASCII码)。如果第1个字节的高位为1,就是多字节编码,至于是2个字节、3个字节疑惑是4个字节,取决于第一个字节高位有连续几个1。若属于多字节编码,那么 UTF-8 除首字节外,其余字节均以10 开头。也就是说,UTF-8 理论上可以到8字节,但由于 Unicode 目前只有一百多万个码点,因此最多使用4个字节就足够了。
- 单字节编码,形式是 0xxxxxxx,完全兼容ASCII 编码,包含127个字符。
- 双字节编码:形式是 110xxxxx 10xxxxxx,能够容纳 2048 个字符,
- 三字节编码:1110xxxx 10xxxxxx 10xxxxxx,能够容纳65536个字符。
- 四字节编码:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:能够容纳 2,097,152 个字符,超出了 Unicode 码点范围
UTF-8 最大的好处是灵活,如果文本完全是英文,那么只需要单字节就可以了。汉字绝大部分是三字节编码,少部分是四字节。
大端序和小端序
计算机以字节为最小存储单位,也就是说一个字节之内是不会拆分的。但如果有两个或以上的字节(或单元),存放时谁放在前面就有区别了.比如"马"字的Unicode码值是 0x9A6C ,采用2个字节存储时,就有两种不同的考量,即第一个字节 9A 放在存储器的前面(低地址处),还是放在后面(存储器的高地址处)呢?大端序就是前面的字节放在低地址处,后面的放在高地址处;否则就是小端序。
字节序 BOM 即 Byte Order Mark。BOM 是Unicode标准中的一个特殊字符 FEFF. 它没有定义为任何字符,但在Unicode标准中推荐放在文件或数据流的开头, 用来标识字节存贮的顺序. 我们都知道它的码值是 FEFF. 如果存储为FE FF,则表明这种方式是大端序(BE), 如果存储为FF FE,则表明为小尾(LE)。
C# 中处理文本编码
在 C# 中,System.Text.Encoding 为文本编码提供了基础支持,可以通过 GetEncoding 方法获取任意编码实例。
// 在 .NET Core中,必须休闲调用 RegisterProvider:
Encoding.RegisterProvider (CodePagesEncodingProvider.Instance);
// 获取当前系统支持的所有编码
foreach (EncodingInfo info in Encoding.GetEncodings())
{
Console.WriteLine (info.Name)
}
// 获取 GB18030 编码
var chineseEncoding = Encoding.GetEncoding ("GB18030");
var utf8Encoding = Encoding.UTF8;
对于 ASCII、Latin1、 UTF-8 、UTF-16 和 UTF-32 等编码,C# 还提供了专门的子类。
- Encoding.ASCII
- Encoding.Latin1
- Encoding.UTF8
- Encoding.Unicode ,UTF-16编码,小端序
- Encoding.BigEndianUnicod,UTF-16编码,大端序
- Encoding.UTF32,UTF-32编码,小端序
在读取文件时,可以指定编码,如果不指定,一般默认为 UTF-8。
System.IO.File.WriteAllText ("data.txt", "床前明月光", Encoding.Unicode);