这几天花了些时间在学习这个内容, 网上有好些编写得非常好的文章,受益匪浅. 印象最深的是知乎上"笨笨阿林"的《刨根究底字符编码》系列,非常详细,非常深入。特意先分享一下其地址:https://zhuanlan.zhihu.com/p/27012715
在开始枯燥的编码方式内容之前,先有几则小tips:
几种大字库,我从网上辛苦弄下来的,打了个包共享一下:
https://pan.baidu.com/s/1DONu24mpzWVxj1JmzgZ7VQ 提取码: iz8h
Unicode标准的PDF文件中有所有字符,但复制出来时可能不准确复制为它的Unicode. 比较靠谱的方法是直接在Word中输入十六进制的Unicode码再按Alt+X生成这个字符. 如果不能正确显示,请选中字符后再选择合适的大字库字体.如上面分享的那个98WB-U字体. 如果字体也不支持,那就显示不来了.
发现一个有意思的五笔输入法,也是用多多输入法生成的, 好用, 界面漂亮. 名字比较接地气--"黄狗输入法", 下载
几个有关Unicode的网址: 字海网 Unicode 字符百科 Unicode官方网站
对编码理论不感兴趣的可以不用往下看了. 下面是我的笔记了
🏄
UTF-8, UTF-16, UTF-32这些都是编码方式,不是Unicode的码值.
UTF-8以8位(也就是一个字节)为一个单元进行处理, UTF-16 以16位(两个字节)为一个单元进行处理,也就是如果2个字节不够,则启用4个字节,不能用3个字节,因为以两个字节为一个单元; UTF-32则以32位也就是4个字节为一个单元进行处理,如果4个字节不够用,则启用8个字节来处理.当然,以目前Unicode的编码数,4个字节足够使用了.
大尾(BE)、小尾(LE)、字节序(BOM)
大尾(Big-Endian),简写为BE; 小尾(Little-Endian),简写为LE.
在存贮时,计算机至少要以字节为单位进行存贮,有时也以两字节或四字节为一个单元进行存贮.一个字节之内是不会拆分的. 但如果有两个或以上的字节(或单元),存放时谁放在前面就有区别了.比如"马"字的Unicode码值是9A6C(16进制, 以下同), 在计算机上存储时, 如果按字节进行存储, 第一个字节9A(9A是数字的高位) 放在存储器的前面(低地址处),还是放在后面(存储器的高地址处)?
其实怎么放都可以,但是要让计算机知道怎么存的就怎么读,因此产生了两种方式: 大尾和小尾.
大尾方式就是前面的字节(数字的高位,左边部分)放在存储器的前面(低地址处), 后面部分(数字的低位, 右边部分) 放在存储器的后面(低地址处); 简单来讲就是"高位放低地址". 比如上面的9A 6C 在存储器中,存放成9A 6C两个字节.
小尾方式就是前面的字节(数字的高位,左边部分)放在后面(存储器的高地址处),后面的字节(数字的低位,右边部分)放在前面(存储器的低地址处). 简单讲就是"高位放高地址". 比如上面的9A 6C在存储器中,存放成6C 9A两个字节.
类似地, 在UTF-16编码中,比如有个码值是 1A 2B 3C 4D的,按大尾方式存贮,存储器上就是 1A 2B 3C 4D,按小尾方式存贮,就是4D 3C 2B 1A. 如果在UTF-32编码中也是类似. LE和BE的存储顺序正好是按字节倒过来的.
字节序BOM即Byte Order Mark.BOM是Unicode标准中的一个特殊字符FEFF. 它没有定义为任何字符,但在Unicode标准中推荐放在文件或数据流的开头, 用来标识字节存贮的顺序. 我们都知道它的码值是FEFF. 如果存储为FE FF,则表明这种方式是大尾(BE), 如果存储为FF FE,则表明为小尾(LE).
文件头标识一般指的是字节顺序标记BOM(Byte Order Mark),位于文件的最开始。当打开一个文本文件时,就BOM而言,有如下几种情形:BOM为:EF BB BF ——表示编码方式为UTF-8; BOM为:FF FE ——表示编码方式为UTF-16LE(小端序); BOM为:FE FF ——表示编码方式为UTF-16BE(大端序); BOM为:FF FE 00 00 ——表示编码方式为UTF-32LE(小端序); BOM为:00 00 FE FF ——表示编码方式为UTF-32BE(大端序); 没有BOM ——要么显式地提示用户手动选择一种编码方式,要么隐式地由软件按规则自行推断出编码方式。
UTF-8, UTF-16 和UTF-32各自的编码方式
UTF-8
UTF-8 的编码规则:
① 对于单字节的符号,字节的第一位设为 0,后面的7位为这个符号的 Unicode 码,因此对于小于0x7F的字符,如英文字母,UTF-8 编码和 ASCII 码是相同的。
② 对于n字节的符号 (n>1),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10,剩下的没有提及的二进制位,全部为这个符号的 Unicode 码 。
这种方法目前比较通用,是一种变长编码,最小处理单元是一个字节.
1) 对于Unicode码值在0x0~0x7F(含两端)的, UTF-8编码直接就是它Unicode码值,即一个开头为0的字节.此时UTF-8编码与ASCII编码相同.
2) Unicode码值为0x80~0x7FF的, 编码为两个字节,形式是
110xxxxx 10xxxxxx.
3) Unicode码值为0x800~0xFFFF的,编码为三个字节,形式是
1110xxxx 10xxxxxx 10xxxxxx.
大部分汉字在这一段的子区间(0x4E00~0x9FFF), 因此用正则表达式匹配汉字时,可以利用这一点,比如VBScript的正则表达式用[u4E00-u9FFF]+作为模式.
4) Unicode码值为0x10000~0x10FFFF,编码为四个字节,形式是
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
Unicode → UTF-8变换方式:
如何将Unicode码值转换为UTF-8编码呢? 先看码值在上述四个区间的那一个. 再将码值转换成二进制,将二进制值从右到左填入上面的样板的x的位置上, 最后如果还有x没有填满,则用0补满.
比如, 上面的"马"字的Unicode码值为9A 6C,先判断一下它处于上面第3种情况, 应该编码为3个字节. 模板是
1110xxxx 10xxxxxx 10xxxxxx
按每个字符转化为4个二进制位处理, 得到 1001 1010 0110 1100. 去除空间后将这一串二进制从右到左但顺序不变的往上面的模子里填,就得到了
11101001 10101001 10101100
你可以看到,从右往左的填入模板时, 0110被拆分了.这里一定要强调这个从右至左的顺序. 如果最后还有x没有填完,应该填入0替换掉所有x, 而不能不填!
然后,如果你对十六进字符与二进制的换算还熟悉,上面的二进制可以转换成6位的十六进制:
E9 A9 AC
这正是"马"字的UTF-8编码.
由于 UTF-8 的处理单元为一个字节(也就是一次处理一个字节),所以处理器在处理的时候就不需要考虑这一个字节的存储是在高位还是在低位,直接拿到这个字节进行处理就行了,因为大小端是针对大于一个字节的数的存储问题而言的.
UTF-16
UTF-16也是使用变长字节表示的,分为两种情况:
1)码值(十六进制)在0~0xFFFF的字符,直接用两个字节表示;
对多数汉字,用两个字节足够编码了,因此UTF-16直接用它们的Unicode码值来存贮.
2)码值在0x10000~10FFFF之间的字符,用四个字节表示;这时不再是使用码值,而是使用"代理对"来映射这些码值. 但要注意的是,四个字节,前两个字节,后两个字节是不是也是构成了一个字符呢? 为了定义清楚, 这前后各2字节的选择范围应该是没有定义过字符的范围, 规定是前两个字节(也称码元)选自0xD800~0xDBFF, 共1024个点(8到B是4身位); 后一码元(也是两个字节)选自0xDC00~0xDFFF(C到F也是4个身位),也是1024个点位. 这两者组合起来就有一百多万个(1048576)码值可用.
UTF-16的编码方法:
1)将码值(0x10000~0x10FFFF)减去一个0x10000, 得到一个中间数,其取值范围为 0~0xFFFFF,即可转化为一个20位的二进制值(每个Hex变成4个Bin),不够20位时在高位(也就是左边)补0得到;
2)将这个中间数的20位二进制值拆分为高10位和低10位. 高10位加上0xD800(二进制为1101 1000 0000 0000)得到前一个代理码元(又称引导代理), 低10位加上0xDC00(二进制为1101 1100 0000 0000)得到后一个代理码元(又称尾随代理).
3)将引导代码码元放在高位, 尾随代理码元放在低位,即组合出UTF-16的编码值.
光说不练不行, 举例来说,有个码值为0x1F215的符号.我们要将它编码为UTF-16,处理过程如下:
1) 首先判断码值大于0x10000, 要四个字节来表示,也就是要用代理对来搞定.
2) 码值 0x1F215 减去0x10000, 得到 0xF215, 展开为二进制为 1111 0010 0001 0101, 因为不足20位,需要高位补0凑足20位,得到 0000 1111 0010 0001 0101 ,这里红色的为高10位, 蓝色的为低10位.
3)高10位 0000 1111 00(对应Hex值为0x3C,转化时记得从右到左4位一段的重新排一下再转), 加0xD800,得到0xD83C,即引导代理; 低10位1000 0101 01(对应Hex值为0x215), 加上0xDC00,得到 0xDE15,即尾随代理.
4)组合得到D8 3C DE 15. 即UTF-16的码值.如果按BE方式,则存储为D8 3C DE 15.
在UltraEdit中测试如下:
网上还有一篇总结得极好的文章,可以参考,下图即来自该文. https://zhuanlan.zhihu.com/p/27827951
UTF-32
这个比较简单, 以四个字节即32位为一个单元处理, 因为目前的Unicode编码用不完四个字节, UTF-32编码直接用码值作为编码即可. 比如英文字串“ABC”这三个字符的UTF-32码元序列为:00 00 00 41 00 00 00 42 00 00 00 43;因为一个字符需要几个字节来保存, 因此也需要字节序的标识. 详解如下:
很明显这种方式虽然很简单,但存贮空间占用大,不太经济.特别是对英文文件, 几乎是ASCII码的4倍. 因此实际上这一编码方式几乎无人使用,连大名鼎鼎的UltraEdit中都没有支持.
最后,虽然Unicode标准定义了很多新的字符,但是你的电脑中可能没有支持所有这些字符的字体来支持.当字体不能支持时,你可能只能看到方块或者问号或是空白.如果碰到这种情况,请跳回去看本文的开头,那里分享了几个大字集的字体,安装一下或许能解决你的问题.比如我的电脑上, Unicode 13.0扩展G的最后一个字符U+3134A可以显示出来哦.
再补一个你可能认识的罕用字,陕西名吃Biáng Biáng面的字,
Unicode CJK 扩展G里有两个这样的字符,一个简体一点,一个繁体一点,编码是相邻的.
按上面的编码方法,U+31EDD字符转化为(后一个编码不列出,只是+1即可):
UTF-16BE编码, 共4个字节 D8 83 DE DD.
以下显示中,一定要先设置显示字体为支持这些字符显示的字体,如98WB-U.
UTF-8的编码:
UTF-16的编码:
最后贴一个写Biáng字的口诀,网上找来的: