细说编码与字符集

细说编码与字符集

文章还没写完,提前放出防止出现 404。稍后慢慢更新,敬请期待: 细说编码与字符集 - "地瓜哥"博客网

文章还没写完,提前放出防止出现 404。稍后慢慢更新,敬请期待: 细说编码与字符集 - "地瓜哥"博客网

文章还没写完,提前放出防止出现 404。稍后慢慢更新,敬请期待: 细说编码与字符集 - "地瓜哥"博客网

前段时间要研究 Hessian 编码格式,为了搞清楚 Hessian 对字符串的编码,就顺路查了好多编码和字符集的工作,理清了很多以前模糊的知识点。下面整理一下笔记,也梳理一下自己的思路和理解。

ASCII 码

计算机起源于美国,他们对英语字符与二进制位之间的对应关系做了统一规定,并制定了一套字符编码规则,这套编码规则被称为 American Standard Code for Information Interchange,简称为 ASCII 编码

其实,ASCII 最早起源于电报码。最早的商业应用是贝尔公司的七位电传打字机。后来于 1963 年发布了该标准的第一版。在网络交换中使用的 ASCII 格式是在 1969 年发布的,该格式在 2015 年发展成为互联网标准。点击 RFC 20: ASCII format for network interchange,感受一下 1969 年的古香古色。

ASCII 编码一共定义了128个字符的编码规则,用七位二进制表示(0x00 - 0x7F), 这些字符组成的集合就叫做 ASCII 字符集。完整列表如下:

ASCII Table

ASCII 码可以说是现在所有编码的鼻祖。

编码乱战及 Unicode 应运而生

ASCII 编码是为专门英语指定的编码标准,但是却不能编码英语外来词。比如 résumé,其中 é 就不在 ASCII 编码范围内。

随着计算机的发展,各个国家或地区,甚至不同公司都推出了不同的编码标准,比如中国推出了 GB2312、GBK 以及 GB18030;微软推出了 Windows character sets 。

由于不同编码标准是不兼容的,不同的国家有不同的字母,因此,哪怕它们都使用 256 个符号的编码方式,代表的字母却不一样。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (ג),在俄语编码中又会代表另一个符号。

同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。这些问题,如果只是在一个国家或地区内部使用还好。但是,随着互联网的发展,这种编码不兼容的问题将会严重阻碍信息的交流和互联网的发展。

于是,ISO 组织与统一码联盟分别推出了 UCS(Universal Multiple-Octet Coded Character Set) 与 Unicode。后来,两者意识到没有必要用两套字符集,于是进行了一次整合。这样,所有的字符都可以采用同一个字符集,有着相同的编码,可以愉快的进行交流了。

Unicode

Unicode (统一码、万国码、单一码、标准万国码)编码就是为了表达任意语言的任意字符而设计

在这套系统中: 一个字符代表一个 code point,不存在二义性。Unicode 是一个标准,只规定了某个字符应该对应哪一个 code point,但是并没有规定这个字符应该用即为字节来存储。有些关键术语,在这里也做个说明。

码点(Code Point)

码点(Code Point)是分配给每个 Unicode 字符的唯一编号。D瓜哥的理解是给每个字符分配了一个身份证号。

通常会使用十六进制进行表示,表示格式: U+xxxxU+xxxxx。例如, 的码点是 U+74DC

字符集(Charset)

字符集(Character Set,又简称 Charset)是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

Unicode 就是一个字符集。

字符编码(Character Encoding)

字符编码(Character Encoding):是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。

常见的 UTF-8、 UTF-16 等就是编码格式。

对于一个字符集来说要正确编码转码一个字符需要三个关键元素:字库表(Character Repertoire)、编码字符集(Coded Character Set)、字符编码(Character Encoding Form)。其中,字库表是一个相当于所有可读或者可显示字符的数据库,字库表决定了整个字符集能够展现表示的所有字符的范围。编码字符集,即用一个码点(Code Point)来表示一个字符在字库中的位置,如果把字符比喻成人,那么码点(Code Point)就是这个字符的身份证号。字符编码,就是编码字符集和实际存储数值之间的转换关系。一般来说都会直接将码点(Code Point)的值经过编码后的值直接存储。

很多读者都会有和瓜哥当初一样的疑问:字库表编码字符集看来是必不可少的,那既然字库表中的每一个字符都有一个自己的序号,直接把序号作为存储内容就好了。为什么还要多此一举通过字符编码把序号转换成另外一种存储格式呢?

这里有两个原因:

  1. 汉字 的 Unicode 是十六进制数 74DC,转换成二进制数足足有15位(111010011011100),也就是说,这个符号的表示至少需要2个字节。表示其他的符号,可能需要3个字节或者4个字节,甚至更多,比如:😂,感兴趣可以查一下😂的 Unicode 的编码值。如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?所以,这就需要需要一个编码格式方便计算机正确地识别出编码方式,然后让计算机正确解析出其中的字符来。

  2. 如果把每个字符都用字库表中的序号来存储的话,每个字符就需要 3 个字节(这里以 Unicode 中大部分常用中文编码长度为例),这样对于原本用仅占一个字符的 ASCII 编码的英语地区国家显然是一个额外成本(存储体积是原来的三倍)。算的直接一些,同样一块硬盘,用 ASCII 可以存 1500 篇文章,而用 3 字节 Unicode 序号存储只能存 500 篇。这是多么巨大的浪费。

另外,在主流计算机中,一般是以 8 个比特进行编码的,上面两个问题可以汇总成一个问题:如何高效地对字符进行编码?何为“高效”呢?可以这样理解:在保证正确解析的前提下,充分利用每 1 个比特位,不浪费任意 1 个比特位。下面结合不同字符编码规则再来体会。

字节序标记(Byte Order Mark,简称 BOM)

字节序标记,即 Byte Order Mark,简称 BOM。它常被用来当做标识文件是以 UTF-8、UTF-16 或 UTF-32 编码的标记。

在 Unicode 编码中有一个叫做 零宽度非换行空格(Zero Width No-Break Space) (U+FEFF) @ Graphemica 的字符, 用字符 U+FEFF 来表示。对于 UTF-16 ,如果接收到以 FEFF 开头的字节流, 就表明是大端字节序;如果接收到 FFFE,就表明字节流是小端字节序。

除了 UTF-16 之外,UTF-8 也可以添加 BOM,它的 BOM 固定为 0xEFBBBF,选择编码方式为 UTF-8 with BOM 时,生成的文件流中就会出现这个 BOM。为什么 UTF-8 可以不需要bom呢,因为 UTF-8 是变长的,它根据第一个字节信息判断每个字符的长度,不存在正反顺序的问题,我们日常使用的 UTF-8 都是不带 BOM 的。但是,现在

Little endian 和 Big endian

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

第一个字节在前,就是"大头方式"(Big endian),第二个字节在前就是"小头方式"(Little endian)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用FEFF表示。这正好是两个字节,而且FF比FE大1。

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

在 Java 中,使用 byte[] utf16Bytes = string.getBytes(StandardCharsets.UTF_16); 获得的字节数组,头两位都是 FEFF,这和 Java 的采用大头方式的规范是吻合的。

U+0041 U+2262 U+0391 U+002E 41 | E2 89 A2 | CE 91 | 2E

U+D55C U+AD6D U+C5B4 ED 95 9C | EA B5 AD | EC 96 B4

Unicode 与 UTF-8 的转换
Figure 1. Unicode 与 UTF-8 的转换
UTF-16 surrogate decoder
Figure 2. UTF-16 surrogate decoder

常见字符集之间的转换方法(Unicode、UTF-8、UTF-16、UTF-32 和 GB18030 等)

Unicode ↔ / ⇌ UTF-16

UnicodeUTF-16

U ∈ [U+0000, U+D7FF] or U ∈ [U+E000, U+FFFF]

2 Byte存储,编码后等于 Unicode 值。

U+10000 ~ U+10FFFF

  1. 4 Byte存储,现将 Unicode 值减去 0x10000,得到 20bit 长的值。再将 Unicode 分为高 10 位和低 10 位。

  2. UTF-16 编码的高位是 2 Byte,高 10 位 Unicode 范围为 0 - 0x3FF,将 Unicode 值加上 0XD800,得到高位代理(或称为前导代理,存储高位);

  3. 低位也是 2 Byte,低十位 Unicode 范围一样为 0 ~ 0x3FF,将 Unicode 值加上 0xDC00,得到低位代理(或称为后尾代理,存储低位)。

TODO:

  1. 为啥要这样处理?

  2. 从 Unicode 向 UTF-16 转换时,需要减去 0x10000。那么,反向转换时,怎么判断要不要加回去 0x10000? 是遇到 U+Fxxxx 的情况就加 0x10000 吗? UTF-16 - Wikipedia 有解释,其实不需要判断,只要确定是是两个字符就需要加 0x10000

  3. 怎么区分是一个字符?还是两个字符?:[U+D800, U+DFFF] 之间的编码没有分配,留给两个字符的编码做前缀使用:[0xD800, 0xDBFF] 用于标注 high surrogate;[0xDC00, 0xDFFF] 用于标注 low surrogate。参考 UTF-16: Code points from U+010000 to U+10FFFF - Wikipedia

😂 = U+1f602
U+1f602 - 0x10000 = 0x0f602
0x0f602 = 00 0011 1101, 10 0000 0010
00 0011 1101 + 0XD800 = 00 0011 1101 + 11011000 0000 0000 = 11011000 0011 1101 = d83d
10 0000 0010 + 0xDC00 = 10 0000 0010 + 11011100 0000 0000 = 11011110 0000 0010 = de02

0010 FFFF - 0x10000 = 0xF FFFF

常见术语的解释说明

常见乱码及解释

结合相关标注,指定汉字的正则表达式

TODO: 结合相关标注,指定汉字的正则表达式

中日韩统一表意文字 - Wikipedia — 这里提到很多汉字区块。

vim 编码设置

Java的内部表示

Java originally used UCS-2, and added UTF-16 supplementary character support in J2SE 5.0.

UTF-16 - Wikipedia — 这里说明这个问题了!

Java Properties 文件的编码

MySQL 编码问题

十分钟搞清字符集和字符编码 — 这篇文章介绍使用 SQL 来对字体做转码。

-- D瓜哥 · https://www.diguage.com · 出品
-- 查询字符对应的编码
> select hex(convert('寰堝睂' using gbk));
+-------------------------------------+
| hex(convert('寰堝睂' using gbk))    |
+-------------------------------------+
| E5BE88E5B18C                        |
+-------------------------------------+

-- 查询编码在指定字符集下对应的文字。
> select convert(0xE5BE88E5B18C using utf8);
+------------------------------------+
| convert(0xE5BE88E5B18C using utf8) |
+------------------------------------+
| 很屌                               |
+------------------------------------+

-- 尝试了一下,对 Emoji 的支持也可以
mysql> select hex(convert('D瓜哥' using utf8mb4));
╔═══════════════════════════════════════╗
 hex(convert('D瓜哥' using utf8mb4))   
╟───────────────────────────────────────╢
 44E7939CE593A5                        
╚═══════════════════════════════════════╝
1 row in set (0.00 sec)

mysql> select convert(0x44E7939CE593A5 using utf8mb4);
╔═════════════════════════════════════════╗
 convert(0x44E7939CE593A5 using utf8mb4) 
╟─────────────────────────────────────────╢
 D瓜哥                                   
╚═════════════════════════════════════════╝
1 row in set (0.00 sec)


mysql> select CAST('D瓜哥' AS BINARY);
╔══════════════════════════════════════════════════════╗
 CAST('D瓜哥' AS BINARY)                              
╟──────────────────────────────────────────────────────╢
 0x44E7939CE593A5                                     
╚══════════════════════════════════════════════════════╝
1 row in set (0.00 sec)

-- 可以直接查字符的 Unicode 编码
mysql> select hex(convert('👍' using utf32));
╔═══════════════════════════════╗
 hex(convert('?' using utf32)) 
╟───────────────────────────────╢
 0001F44D                      
╚═══════════════════════════════╝
1 row in set (0.00 sec)

TODO: 怎样把字符转成二进制形式?

在 MySQL 中存入 Emoji 表情。

JavaScript 编码

在JavaScript中,所有的string类型(或者被称为DOMString)都是使用UTF-16编码的。

字体的渲染方法(待选)

字体相关信息

  1. 浅谈计算机字体 - 掘金

  2. Glossary | FontShop — 字体各种参数说明。

  3. 参数化设计与字体战争:从 OpenType 1.8 说起 — 写了各种字体技术的发展历史,读起来酣畅淋漓!

根据实验以及看到的一些资料,有一个感觉:UTF-8、UTF-16 以及 UTF-32 相互转换时,需要将字符集编码转化成 code point,然后再根据范围转换为对应的编码。

这块的知识还需要用实验来验证!

Java

Java 中的 char对应的是Unicode的基本平面BMP。Java里的char是编译器里定死了的,它对应的就是BMP,也可以认为是utf-16的2字节部分。

如何渲染字体?

首先字体内部是有一个自己的编码号的,用于索引图元(Glyph),但是外界不会知道它。字体内部的各种数据比如 GSUB 和 GPOS 都是用这个索引号编的。

将图元和文字关联起来的东西是 cmap 表,这表的格式十分多,用来支持不同的外部编码:最常用的 UCS-2 外部编码(FontForge 里面称 UnicodeBMP)使用 Format 4,UCS-4 外部编码(FontForge 称 UnicodeFull)使用 Format 8、Format 12 等。

然后是绘图的时候,WINAPI 或者其他的 API 会对文字编码进行转换。我记得 Windows 是默认把其他编码转换成 UTF16LE 的。

Windows 里分为两种类型的编码系统,其实就是两个系统编码函数,用于转换字符串为unicode,一个是 codepage,这个是可以在系统中切换语言选项中进行切换的,代表当前的位于unicode表中的第几页,另一个是UTF-16的小端序,这个是自windows 2000 之后就开始内核(Window NT)内置的一个编码,因为当时没有utf-8,所以选择这个编码作为了内核的内置编码。

对于上层软件来说,需要通过utf 或者 iso 等等上层复合编码转换成系统支持的编码 然后根据charcode 去字体系统里取字形, 每一个字体都提供一个charMap,然后系统中用charcode去里边筛选,找出glyph图元,然后再交给软件渲染

参考资料

  1. ASCII - Wikipedia

  2. 字符编码笔记:ASCII,Unicode 和 UTF-8

  3. 十分钟搞清字符集和字符编码

  4. Unicode Utilities: Character Properties — 查找字符对应的 Unicode 编码。

  5. 进制转换工具

  6. graphemica · l♥ve letters — 可以查询 Unicode 字符的编码。

  7. ICU Demonstration - Converter Explorer — ICU 各种编码相互转换的工具。

  8. Glossary — Unicode 相关术语表。

  9. The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) – Joel on Software .

  10. Unicode - Wikipedia

  11. UTF-8 - Wikipedia

  12. Code point - Wikipedia

  13. List of Unicode characters - Wikipedia

  14. Unihan Database

  15. Unicode 14.0.0

  16. Unicode 14.0 Character Code Charts

  17. Latin-script alphabet - Wikipedia

  18. FAQ - UTF-8, UTF-16, UTF-32 & BOM

  19. Java Language Specification: Chapter 3. Lexical Structure

  20. UTN #23: To the BMP and Beyond

  21. To the BMP and beyond!-Eric Muller

  22. encoding - What are Unicode, UTF-8, and UTF-16? - Stack Overflow

  23. Unicode Chart — 费了很大劲,找了一个比较全的 Unicode Code Point。美中不足的时,没有展示出来 UTF-8、UTF-16 等编码。

  24. Unihan data for U+74DC — 可以直接在这个页面上查找相关文字的编码信息。有一个地方有待改进,就是对 Emoji 表情支持的不好。尝试了一下查找 Emoji 表情,直接提示报错了。

  25. Full Emoji List, v14.0 — 这里有一个 Emoji 表情的完整列表。

  26. Unicode?UTF-8?GBK?……聊聊字符集和字符编码格式

  27. 一次性搞懂字符集,编码,Unicode,Utf-8/16,BOM…​ - 简书

  28. 字符编码笔记:ASCII,Unicode 和 UTF-8 - 阮一峰的网络日志

  29. 程序员趣味读物:谈谈Unicode编码-太平洋电脑网

  30. Unicode?UTF-8?GBK?……聊聊字符集和字符编码格式

  31. Roadmap to the BMP — 从这里也可以看出,除了 BMP,其余还有 SMPSIPTIPTIPSSP。不止部分文章描述的只有 BMP 和 SMP 两个平面。看样子,以后可能还会有其他的什么 Plane。(中间从 4 到 13 的序号是空着的。)

  32. Plane (Unicode) - Wikipedia

  33. UTF-8/16/32 C++ library

  34. ASCII Table - Openclipart — 感谢他们制作出来的精美 ASCII Table 图表。