(转自个人博客)
问题起源于,从网上下载的高清电影外挂字幕放到 QNap 中去,从 Qvideo 中访问竟全是乱码。查询得知,QNap 中的 Video Station 只能解析识别以 UTF-8 编码的字幕文件。虽然采用 QNap 上更强大的 Plex 可以自然解决该问题,还是促使我要弄明白文本编码到底是怎么一回事。本以为文本编码是一个简单的问题,却在网上查阅的过程中牵连出了一系列的问题。下面分别介绍。
想要弄明白文本的编码是怎么回事,绕不过去字符集和字符编码这两个概念。
字符集:说白了就是某些特定字符的集合,如果把世界上不同国家文明的所有字符都放在一起组成一个集合,那么我们常见的 ASCII、GB2312、GBK、GB18030、BIG5 字符集都只是包含了该集合的一部分而已。而 Unicode 字符集是可以包含所有国家文明中的所有字符的。
字符编码:所有的文件在计算机中最终是以二进制序列来保存的,不同的序列就可以表示不同的内容。字符编码的目的就是对不同的字符编码设计合理的唯一二进制序列在计算机中进行存储表示。
我们知道,字节是计算机中对二进制序列表达最常用的单位,一字节的长度是8位,能够表达256种不同的状态,也就是256个不同的字符。
每一种技术的出现,伴随其制定的标准在最初都无法囊括未来出现的所有情况。字符编码就是如此。
最开始,美国发明了计算机。与之相伴的,美国规定了能够表达包括英文字母大小写在内的128个字符(也包括了一些控制符,比如响铃、退格等等),我们可以把这128个字符集合称作 ASCII 字符集。伴随着的是对应在计算机中的二进制字节编码,也就可以称作 ASCII 编码。
为了表示更多的欧洲等国家使用的字符,对原始的 ASCII 编码范围进行了扩充,采用一个字节 256 种不同状态来表示 256 种不同的字符。
ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
ISO-8859-1收录的字符除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在ISO-8859-1当中。
ISO-8859-1 的较低部分(从 1 到 127 之间的代码)是最初的7位 ASCII。ISO-8859-1 的较高部分(从 160 到 255 之间的代码)全都有实体名称。
因为ISO-8859-1编码范围使用了单字节内的所有空间(即8位,0-255),在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。ASCII编码是一个7位的容器,ISO-8859-1编码是一个8位的容器。
Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。
为了解决汉字在计算机中的编码问题,最开始推行的方案是采用两个字节对常用的 6763 个汉字和其它一些符号进行编码。同时为了保证对 ASCII 编码的兼容性,每个字节的最高一位比特总是为 1。这样计算机遇到高位为1的字节就会采用汉字编码方案,遇到高位为0的字节采用 ASCII 编码方案。这种解决方法我们就可以称为 GB2312 编码方案。
虽然 GB2312 编码能够覆盖99.75%使用频率的汉字,毕竟还是有无法编码的字存在。GBK 的出现弥补了少数汉字无法进行编码解析的问题,它是 GB2312 编码的扩展,向下兼容 GB2312,同时包含了繁体字。
GB18030 进一步扩展了 GBK 所包含的字符集范围,囊括了中国少数民族所用的字符等。同时也是向下兼容 GBK、GB2312的。
与 GB2312 等编码标准的出现相似,BIG5编码 主要是台湾地区为了解决对繁体字的处理。Shift_JIS 为日本电脑系统常用的编码方案,EUC-KR 为韩国电脑系统常用编码方案。
第2节中提到的各个编码方案可以统归为 ANSI 编码。
如上 ANSI 编码条例中所述,世界上存在着多种编码方式,在 ANSI 编码下,同一个编码值,在不同的编码体系里代表着不同的字。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码,可能最终显示的是中文,也可能显示的是日文。在 ANSI 编码体系下,要想打开一个文本文件,不但要知道它的编码方式,还要安装有对应编码表,否则就可能无法读取或出现乱码。为什么电子邮件和网页都经常会出现乱码,就是因为信息的提供者可能是日文的 ANSI 编码体系而信息的读取者可能是中文的编码体系,他们对同一个二进制编码值进行显示,采用了不同的编码,导致乱码。这个问题促使了 unicode 码的诞生。
如果有一种编码,将世界上所有的符号都纳入其中,无论是英文、日文、还是中文等,大家都使用这个编码表,就不会出现编码不匹配现象。每个符号对应一个唯一的编码,乱码问题就不存在了。这就是 Unicode 编码。
Unicode 现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,“汉”这个字的Unicode编码是U+6C49。
需要注意的是,Unicode只是一个字符集,它只规定了符号与二进制代码之间的对应关系,却没有规定这个二进制代码应该如何存储。
比如,汉字"严"的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
UTF-8 是 Unicode 的实现方式之一。
UTF-8(UCSTransformation Format 8bit)就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示)等,不过在互联网上用的很少。
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode 符号范围(十六进制) | UTF-8 编码方式(二进制) |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面以汉字"严"为例,演示如何实现 UTF-8 编码。
已知"严"的 unicode 是 4E25(100111000100101),根据上表,可以发现 4E25 处在第三行的范围内(0000 0800-0000 FFFF),因此"严"的 UTF-8 编码需要三个字节,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"严"的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严"的UTF-8编码是"11100100 10111000 10100101”,转换成十六进制就是E4B8A5。
经过上边的介绍,我们可以大致认为,现在流行的一些编码方案都是在兼容 ASCII 的基础上来实现的。为了满足各国家地区的更多字符的编码需求,出现了 ANSI 编码标准,但是该编码标准在具体各地区国家的实现上是彼此不兼容的。为了满足世界各国字符编码的兼容性需求,Unicode 定义了一个统一、完备的字符集。为了实现 Unicode 字符集在编码上的需求,又诞生了 UTF-8、UTF-16等等编码方案。
最后用一张图来说明。