字符编码,从 ASCII 到 Unicode

编码指为了信息交流或存储将信息(文字、声音、图像等)从一种形式转换为另一种形式的过程

字符编码:表示字符集的规则

计算机系统内数据的计算、存储、传输都涉及到字符编码。

字母A的ASCII编码和UniCode编码:

二进制     字符编码
1000001   065 (ASCII)
1000001   U+0041 (UniCode)

基础名词解释

  • 字素(Grapheme): 文字书写系统中的最小单元。比如单个的汉字或单个的英文字母。

  • 字素簇(Grapheme clusters):多个字素的集合。如拼音字母ǎ(U+01CE),由字母a(U+0061)和音调符号号̌(U+030C)组合而成。 再比如越南语字母中的字符ế(U+1EBF)由U+0065(e)、U+0302(抑扬符)、U+0301(尖音符)三个字素组合而成。

  • 字形(Glyph):字素的具体表现形式。下图包含了不同字体(typeface)内字母a的表现形式 glyphs

  • 字符(Character):计算机中字素对应的信息单元。

  • 字符集(Character repertoire):字符的抽象集合,比如Unicode是一个字符集,它只是字符的注册登记表,不负责具体的编码实现。

  • 编码空间(Code Space):编码空间指字符编码值的范围。如ASCII码采用7比特进行编码,那么编码空间就是0 - 2^7。

  • 码点(Code Point)也叫码位(Code Position):码点指字符集中,一个字符的值或者它在编码空间中的位置。如ASCII码包含128个字符编码,那么就有个128个码点。 每个码点有一个对应值,比如字母a的码点值是97,一个码点由一到多个码元组成。

  • 码元(Code Unit):码元指表达一个字符所需的最小二进制序列。如UTF-8编码表示一个字符最少需要8比特,那么UTF-8的的码元就是由8比特二进制构成的序列。

  • 自同步(Self-synchronizing):如果一种编码方式不需要通过向前查找来判断一个码元是否是一个字符的起始码元,则称为自同步编码,如UTF-8编码。

ASCII

计算机系统是基于二进制建立的,因此需要用一连串的 0 或 1来表示字符。计算机最早由美国人发明, 那时候也没有互联网,所以他们制定了一套适用于英语文字系统的编码,即ASCII(American Standard Code for Information Interchange)码。 ASCII码使用一个字节(实际使用低位7比特,最高比特位统一置0)进行编码,一共包含128(2^7)个字符编码。

ASCII - Binary Character Table

Unicode

Unicode并不是具体的编码实现,Unicode相当于一张编码表,它给每个字符分配了一个16进制的值。不同的Unicode编码实现都要遵循这张码表的规定。

Unicode的实现方式称为统一码转换格式(UTF, Unicode Transformation Format)。 目前主要有三种实现方式:UTF-8, UTF-16, UTF-32(数字 8 16 32 表示编码最少需要的比特位,即码元长度)。

Unicode发展概况

全球几百个国家和地区使用的语言文字千差万别多种多样,光CJK(中日韩)文字体系就包含上万个不同的字符。 随着计算机的普及,只能表示128个字符的ASCII编码显然完全不适用。 因此各个国家和地区根据自身的需求发明了适合自己的编码方案,如ISO-8859-1 GB-2312 BIG5 Shift_JIS等等。

不同国家地区采用不同的编码方案就带来了新的问题。A B两人的计算机使用互不兼容的编码,B打开A发给ta的文件时就会出现乱码问题。 为了确保文件的内容能以正确的编码形式得到正确的展示计算机系统和软件就需要做额外的转换工作。这对软件开发者、软件使用者和计算机厂商而言也都不是一件好事。 特别是文件中如果包含了多种不同文字的情况,处理起来更加的麻烦。所以需要一套完备的能表示全人类所有字符的编码方案, 80年代末计算机硬件和软件厂商组成的统一码联盟和ISO标准版组织各自制定了用于表示全球所有字符的统一字符编码。

国际标准化组织(ISO)制定了ISO/IEC 10646(UCS, Universal Coded Character Set)编码方案,由Xerox、Apple等厂商组成的统一码联盟制定了Unicode编码方案, 但我们不需要两套不兼容的字符集,所以两种标准进行了融合统一。在两套标准里,所有的字符都具有相同的码点和名字。

Unicode最早采用UCS方案的16位定长的UCS-2编码模型(这也是为什么Java等编程语言以及Windows操作系统采用了UTF-16编码作为默认编码), UCS-2没有将那些古老的、过时的文字考虑在内,最多能编码65,536(2^16)个字符。为了解决编码空间不足的问题, Unicode 2.0在UCS-2的基础上扩展了编码长度并加入了代理对机制,即为UTF-16编码方案。 UTF-16编码方案使Unicode的编码空间扩充到了1,112,064个,范围为U+0000到U+10FFFF。

UTF-16之后产生的UTF-8和UTF-32编码可以表示的字符远超1,112,064个,但因为Unicode采用了UTF-16编码制定码表,所以UTF-8, UTF-32目前能编码的字符也不超出这个范围。

Unicode编码空间限定为U+0000到U+10FFFF(1,114,111个码点),根据字符的使用频率将这些字符细分为17个平面进行管理。 第零个平面称为基本多语言平面(Basic Multilingual Plane,简称BMP),该平面包含了全球各个区域最常用的字符。剩余16个平面称为补充平面(Supplementary Planes)。 因为代理对机制的需要,基本平面内U+D800到U+DFFF(2048个码点)为保留区块用于对补充平面的字符的码点进行编码。 所以Unicode理论上总共有1,112,064个码点(1,114,111 - 2048 = 1,112,064),但这其中又规定了66个非字符以及保留137,468个码点做私有用途(任何个人、机构、厂商可按需使用)。 所以Unicode最终可分配使用的码点为 974,530 个。

17个平面的详细划分请参见维基 https://en.wikipedia.org/wiki/Plane_(Unicode)

https://unicode-table.com/ 这个网站可以方便的查看、查找Unicode编码。

字节序(Endianness)

字(word)内字节(byte)的排列顺序,通常多字节编码存在字节序问题。

大端序(Big endian)

高位字节在前(左)低位字节在后(右)。以字母a为例,采用 UTF-16 编码时大端序表示为0061 大端序便于人类阅读和处理,大端序也被称为网络字节序,因为网络协议通常都采用大端序。

小端序(Little endian)

与大端序相反,低位字节在前(左)高位字节在后(右)。以字母a为例,采用 UTF-16 编码时小端序表示为6100。 微处理器通常采用小端序

字节内的比特序列也存在大端序和小端序之分,但是通常字节是当做一个整体处理,所以其内部的比特顺序不影响其值。

字节序标记(Byte order mark)

许多Windows程序会在以UTF-8编码保存的文档的开头添加0xEF, 0xBB, 0xBF(字节序符号的UTF-8表示)来标识文档使用UTF-8编码。 前面说了UTF-8并不存在字节序问题,所以添加的这个BOM和文档实际字节序没有任何关系╮(╯▽╰)╭,Unicode标准并没有要求或者推荐这样做。 而采用UTF-16或UTF-32编码的文档开头的BOM就真的是用来标记字节序的。

UTF-16

UTF-16是Unicode最早的实现方案,在UCS-2的基础上扩充实现的。UTF-16用2个字节表示基本平面内的字符,用4个字节表示补充平面的字符,因此UTF-16是可变长编码。 对于连续的多个字节组成的文本我们无法界定哪些字节构成基本平面字符哪些字节构成补充平面字符。比如0041 0042 0041, 我们应该把它当做三个基本平面的字符还是当做一个基本平面字符和一个补充平面的字符来进行处理呢。 对于第二种情况我们应该把前两个字节当做一个补充平面字符,把后面四个字节当做一个补充平面字符还是反过来呢。 为了解决这个问题,实现编码自同步,UTF-16引入了代理对的机制。

Windows 使用UTF-16(Windows 10 1803版本开始添加了对UTF-8的支持)做为系统默认编码,Linux/Unix、MacOS均使用UTF-8。

代理对(Surrogate Pair)

表示非ASCII字符的多字节串的第一个字节总是在0xC0到0xFD的范围里,并指出这个字符包含多少个字节。 多字节串的其余字节都在0x80到0xBF范围里,这使得重新同步非常容易,并使编码无国界,且很少受丢失字节的影响。 理论上可以最多到6个字节长,16位BMP字符最多只用到3字节长。

UTF-16规定基本平面内0xD800-0xDFFF(2 * (2 ^ 10) = 2048个码点)用做补充平面内字符编码的代理对,不用做正常的基本平面字符编码。 0xD800到0xDBFF(2^10)的值用做高位代理,0xDC00到0xDFFF(2^10)的值用做低位代理,组合起来总共可以表示1,048,576‬(2^20)个字符。

U+D800—U+DBFF 高位代理 U+DC00—U+DFFF 低位代理 U+E000—U+FFFF U+10000—U+10FFFF(非基本平面)

注意代理对的范围是基本平面中间的一段,因为如果取开头或者结束的某段编码空间,就又存在无法正确界定字符的问题。 另外代理对的编码空间是 2 ^ 20,高位代理和低位代理各10个比特。如果想要能表示更多,代理对的范围就要继续扩大, 但是这样基本平面正常的编码空间就会减少。比如把代理对的空间扩大到2 ^ 22,高位代理和低位代理各11个比特, 那么基本平面可用的编码空间就要减少2 * (2 ^ 11)。基本平面内的字符都是使用率非常高的字符,存储只需要1个字节, 减少基本平面正常的编码空间意味着大概率的存储空间增加。2 ^ 20 的辅助平面编码空间已经足够大,进一步扩大得不偿失。

假设代理对 SP = xxxxxxxxxxyyyyyyyyyy

高位代理加上0xD800得到16位码元在0xD800-0xDFFF之间值

1101 1000 0000 0000‬
       xx xxxx xxxx +
1101 10xx xxxx xxxx

低位代理加上0xDC00得到16位码元为0xDC00–0xDFFF之间的值

1101 1100 0000 0000‬
       yy yyyy yyyy +
1101 11yy yyyy yyyy

代理对 xxxxxxxxxxyyyyyyyyyy 加上0x10000 得到补充平面字符32位实际码元为0x10000-0x10FFFF之间的值。

以表情符号😁(U+1F600)为例:0x1F601 - 0x10000 = 0xF601,表示成二进制为 1111 0110 0000 0000,右移补齐20位二进制为0000 1111 0110 0000 0000。所以高位 代理值为0000 1111 01(D83D),低位代理值为 10 0000 0000(DE00)。

1101 1000 0000 0000‬
       00 0011 1101 +
1101 1000 0011 1101(D83D)
1101 1100 0000 0000‬
       10 0000 0000 +
1101 1110 0000 0001(DE00)

所以😁(U+1F601)的码元为D83DDE00,程序在进行解析时发现前两个字节D83D在0xD800-0xDFFF内为高位代理, 就继续读两个字节的低位代理,然后根据代理对规则算出码点得到对应的Unicode字符😁。

'\ud83d\ude00' = 😁

需要注意的是因为常用字符都在基本平面内,UTF-16的代理对机制没有经过完备的测试存在一些安全漏洞,WHATWG 建议不要使用。

UTF-8

UTF-8 编码由C语言作者Ken Thompson和Rob Pike于1992年设计实现,是万维网(World Wide Web, WWW)使用率最高的字符编码,WHATWG钦定编码方式。 IMC(Internet Mail Consortium)建议所有邮件程序使用UTF-8编码,W3C推荐XML和HTML默认使用UTF-8编码。

UTF-8 编码使用8比特作为一个码元,用1个字节来表示ASCII字符,用2-4个字节表示其他字符。因为 UTF-8 码元为单字节,所以不存在字节序问题。

UTF-8 编码规则

开始码点   结束码点    字节1     字节2    字节3    字节4     字节5    字节6
U+0000    U+007F     0xxxxxxx
U+0080    U+07FF     110xxxxx 10xxxxxx
U+0800    U+FFFF     1110xxxx 10xxxxxx 10xxxxxx
U+10000   U+1FFFFF   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U+200000  U+3FFFFFF  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U+4000000 U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

从编码规则可以看出 UTF-8 是兼容ASCII码的,U+0000到U+007F的字符编码和ASCII完全一致。UTF-8每个字节都存在一定数量比特值是固定的。 因为UTF-8是可变长编码,存在用多个码元表示一个完整字符的情况。为了实现自同步(self-synchronization), 需要牺牲一定比特位来做标记位。UTF-8可使用更多字节来编码更多字符,但因为前面提到的历史原因Unicode限定了编码空间大小为 974,530, 所以UTF-8实际最多使用4个字节进行编码。

UTF-32

UTF-32总是使用32比特表示一个字符,为定长编码。UTF-32编码理论编码空间大小为2^31,但按照Unicode规范规定实际编码空间为 974,530。 因为UTF-32任何一个字符都需要4个字节的空间,不利于存储与传输,所以很少被使用。

Unicode预组合字符的处理

由其它字符组合而成的字符称之为预组合字符(Precomposed character)。

如汉语拼音字母 ǎ, ǎ由字母a和音调符号̌组合而成。再比如罗马数字Ⅳ和Ⅵ都是由Ⅰ和Ⅴ组合而成,只是顺序不同而已。

对于这类组合字符Unicode规定附加符号放在主符号的后面。为了使对Unicode支持不完整的系统也能正常显示这类字符, Unicode也会把它们当做一个整体对待给这类组合字符添加单独的编码(但并不是所有组合字符都有单独的编码)。 这也就导致了同一个字符有多个码点,在对文字做处理时就需要进行Unicode正规化(Normalization)。 预组合字符的存在给Unicode字符串的长度计算增加了难度。

两个Unicode字符编码组合起来表示另一个Unicode字符编码的形式称为分解字符序列,预先组合字符和分解字符序列是等价的。 ǎ的Unicode编码是U+01CE,U+0061和U+030C组合起来也可以表示ǎ。Ⅳ的编码是U+2163,U+2164和U+2160组合起来也表示Ⅳ。

以采用UTF-16编码的Javascript为例

'\u0061\u030c' = 
'\u01ce' = 

'\u2164\u2160' = ⅤⅠ
'\u2163' = ⅤⅠ

'\u2160\u2164' = ⅠⅤ
'\u2165' = ⅠⅤ

此处使用了转义序列来表示Unicode字符串。

由反斜杠 () 后接字母或数字组合构成的字符组合称为转义序列(Escape Sequences)。 通常用于表示一些特殊字符,或者告诉程序按特定逻辑对字符进行处理。

  • 表示某些控制符:如换行符 \n 制表符 \t
  • 转义字符串中的引号 ‘I'm’ “say "Hello"”
  • 表示Unicode字符串 ‘\u4e16\u754c’

计算机程序如何识别文件编码

文档内容是以字节流的形式存在的,要能正常展示文件内容,程序得先知道文件内容编码。所以在编码未知的情况下,是没有办法通过读取字节流来准确判断文件的编码的。

How to determine the encoding of text? How to guess the encoding of a document?

乱码(Mojibake)是如何产生的

程序根据编码判断出对应的字符,再根据字符去查找相应的字体内对应的字形(glyph)。所以用B编码打开A编码的文件就产生现乱码,乱码产生的情况有3种。

第一种情况是根据B编码能解析文件内容,但系统根据编码加载的字形和本来的字形不匹配。

Shift-JIS    GB
文字化けテスト 暥帤壔偗僥僗僩

UTF-16       GB
쳌           烫

UTF-8        ISO-8859-1
Smörgås      SmörgÃ¥s

第二种情况是系统内默认字体和备选字体缺少匹配的字形,这时会以豆腐块的形式展示字符。

编码错误,根据错误编码找到错误字形或系统字体内字体无对应字形
Big5               GB
三國志11威力加強版  瓣в眏

编码正确,系统字体内无对应字形
GB                 GB
栞                 

谷歌的Noto(No tofu)系列字体制作的初衷就是为了提供字形完备的字体避免显示豆腐块的情况。

第三种情况是字符编码无法正常解析,用替换符(Replacement character)替代展示

ISO-8859-1    UTF-8
für           f�r

für 的ISO-8859-1编码为0x66 0xFC 0x72,使用UTF-8无法正确解析中间的\0xFC, 程序就自动用�替代了无法解析的字符ü。

参考资料

Character Encoding

Universal Coded Character Set(UCS)

Unicode

The Unicode Frequently Asked Questions

https://en.wikipedia.org/wiki/Comparison_of_Unicode_encodings

https://en.wikipedia.org/wiki/UTF-7

Unicode and ISO 10646

Endianness

Precomposed_character

Character encodings for beginners

An Introduction to Writing Systems & Unicode

Unicode 及编码方式概述

浅谈文字编码和Unicode(上)

浅谈文字编码和Unicode(下)

其实你并不懂 Unicode

计算机系统是如何显示一个字符的?

字符编码的前世今生