Unicode,UTF和Emoji

Last Updated: 2023-07-02 13:20:35 Sunday

-- TOC --

Unicode统一为2字节宽(UCS-2),非常适合程序代码进行各种处理,UTF-8基本上都用来存储和传输。

Unicode

Unicode,统一码,也叫万国码、单一码,是计算机科学领域里的一项业界标准。

Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符都设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode从1990年开始研发,1994年正式发布1.0版本,2021年9月14日发布14.0版本。

查看Unicode的所有符号:https://unicode-table.com

Unicode的学名是Universal Multiple-Octet Coded Character Set,简称为UCS。 UCS也可以看作是Unicode Character Set的缩写。

Unicode有两种编码方案,UCS-2和UCS-4,可使用空间(Code Point)分别达到2^16和2^32个。

Unicode兼容ASCII编码

在 Unicode 中,每个字符被分配了一个数值(Code Point,代码点)和一个名称。比如字母A的名称是LATIN CAPITAL LETTER A(大写拉丁字母A)。它对应的数值是 65,通常写作 U+0041(41是十六进制数,等于10进制的65)。除此之外,Unicode 还定义了各个字符的一系列属性,比如是否是大写字母,是否代表数字,书写方向(左到右还是右到左),宽度(半角还是全角)等。基于这些属性,Unicode 提供了大小写转换,文本换行,双向书写显示等相关算法。

Unicode 字符集被分为十七个子集(Plane,平面或位面),每个子集最多可包含 65536 个字符,因此总共可以有 1,114,112 个字符。其中第一个子集(Plane 0)包含最常用的字符,被称为 BMP(Basic Multilingual Plane, 基本多文种平面)。 BMP 中为 UTF-16 中的代理对(Surrogate Pair)保留了 2048 个位置,只剩下 63488 个有效字符空间(因此 Unicode 中实际最多有1,112,064个字符)。BMP 中的字符可以用四位十六进制数(U+xxxx)表示,其它的字符需要五位或更多。

用Python处理Unicode

Python在内存中,使用Unicode编码!

用下面这段Python代码,自行设置起始区间,即可查看对应编码的Unicode符号:

>>> for i in range(0x4E00,0x4E0F):
...   print(i, ':', chr(i), '\\u'+hex(i)[2:])
...
19968 :  \u4e00
19969 :  \u4e01
19970 :  \u4e02
19971 :  \u4e03
19972 :  \u4e04
19973 :  \u4e05
19974 :  \u4e06
19975 :  \u4e07
19976 :  \u4e08
19977 :  \u4e09
19978 :  \u4e0a
19979 :  \u4e0b
19980 :  \u4e0c
19981 :  \u4e0d
19982 :  \u4e0e

基本汉字Unicode编码范围:[0x4e00,0x9fa5](或十进制[19968,40869])

下面这段代码,可以将任意符号转换成Unicode:

>>> a
'计算机编程'
>>> for i in a:
...   print(ord(i), i, '\\u'+hex(ord(i))[2:])
...
35745  \u8ba1
31639  \u7b97
26426  \u673a
32534  \u7f16
31243  \u7a0b

注意\u后面是2个byte的16进制表示,以此表示这2个byte组合成一个Unicode编码。

部分中文标点符号的Unicode编码

名称 Unicode 符号
句号  3002   。
问号  FF1F   ?
叹号  FF01   !
逗号  FF0C   ,
顿号  3001   、
分号  FF1B   ;
冒号  FF1A   :
引号  300C   「
300D   」
引号  300E   『
- 300F   』
引号  2018   ‘
2019   ’
引号  201C   “
201D   ”
括号  FF08   (
FF09   )
括号   3014   〔
3015   〕
括号   3010   【
3011 
破折号  2014 
省略号 2026   …
连接号  2013   –
间隔号  FF0E   .
书名号 300A 
300B   》
书名号 3008 
3009   〉

BMP

用途或意思相近的码点被划分到不同的组当中,叫做“平面”(plane)。目前一共规定了16个平面,但只使用到了其中少数几个。BMP,就是基本平面。

unicode_bmp.png

CJK,CJKV

CJK表示中日韩统一表意文字(CJK Unified Ideographs)。

CJK的目的是要把分别来自中文、日文、韩文、越文、壮文中,本质和意义相同、形状一样或稍异的表意文字(主要为汉字,但也有仿汉字如日本国字、韩国独有汉字、越南的喃字、古壮字,还包括汉字笔画、汉字偏旁)于ISO 10646及Unicode标准内赋予相同编码。

CJK是中文(Chinese)、日文(Japanese)、韩文(Korean)三国文字的缩写。顾名思义,它能够支持这三种文字。实际上,CJK 能够支持在 LaTeX 中使用包括中文、日文、韩文在内的多种亚洲双字节文字。

国家标准 GB13000.1 完全等同于国际标准《通用多八位编码字符集 (UCS)》 ISO 10646.1。《GB13000.1》中最重要的也经常被采用的是其双字节形式的基本多文种平面。在这65536个码位的空间中,定义了几乎所有国家或地区的语言文字和符号。其中从 0x4E00 到 0x9FA5 的连续区域包含了 20902 个来自中国(包括台湾)、日本、韩国的汉字,称为 CJK (Chinese Japanese Korean) 汉字。CJK 是《GB2312-80》、《BIG5》等字符集的超集。

那么CJKV是什么意思呢?我特地查了一下,原来越南曾经也使用过汉字,被称为喃字。Unicode后来也包含了喃字,因此有了CJKV这个缩写。

UTF

UTF: Unicode(UCS) Transfer Format

UTF-32

UTF-32,顾名思义,是用32位,也就是四个字节来表达一个字符的编码方案。多字节的时候,就有字节序的问题:

>>> t = '\x01\x02'
>>> t
'\x01\x02'
>>> t.encode('utf-32')
b'\xff\xfe\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00'
>>> t.encode('utf-32le')
b'\x01\x00\x00\x00\x02\x00\x00\x00'
>>> t.encode('utf-32be')
b'\x00\x00\x00\x01\x00\x00\x00\x02'

当直接使用UTF-32(大小写都可以)对字符串做encode的时候,默认第一组4字节是BOM(Byte Order Mark),如果指明字节序,就没有第一组4字节了!

BOM是Byte Order Mark的缩写,就是字节序标记。

在UCS 编码(就是Unicode)中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FE FF。而FF FE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。这样如果接收者收到FEFF,就表明这个字节流是Big-Endian(大字节序)的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。

UTF-16

与UTF-32一样,只是2字节编码。

>>> t
'\x01\x02'
>>> t.encode('utf-16')
b'\xff\xfe\x01\x00\x02\x00'
>>> t.encode('utf-16be')
b'\x00\x01\x00\x02'
>>> t.encode('utf-16le')
b'\x01\x00\x02\x00'

UTF-16的BOM只有2个字节。

UTF-8

UTF-8的RFC:https://www.rfc-editor.org/rfc/rfc3629.txt,此文中有说明,UTF-8是在Plan9系统时期开发的。

UTF-8编码特点是使用变长字节数来存储数据,一般是1到4个byte。当然,也可以更长,但实际上4个byte可以表示2的32次方个不同字符,即4294967296个(约43亿),已经足以编码人类使用的所有字符了。

为什么要变长呢?实际上变长编码有其优势也有其劣势,优势是节省空间、适合网络传输、自动纠错性能好、扩展性强,劣势是不利于程序内部处理,比如正则表达式检索,UTF-16、UTF-32这些等宽字符编码就比较适合程序处理,但是又比较耗存储空间。

我们已经知道UTF-8编码至少有一个字节,从首字节就可以判断一个字符的UTF-8编码有几个字节。如果首字节以0开头,肯定是单字节编码,如果以110开头,肯定是双字节编码,如果是1110开头,肯定是三字节编码,以此类推。除了首字节外,多字节UTF-8码的后续字节均以10开头。

所以1~4字节UTF-8编码看起来是这样的:

0xxxxxxx (ASCII)

110xxxxx 10xxxxxx

1110xxxx 10xxxxxx 10xxxxxx

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

单字节可编码的Unicode范围:\u0000~\u007F(0~127)

双字节可编码的Unicode范围:\u0080~\u07FF(128~2047)

三字节可编码的Unicode范围:\u0800~\uFFFF(2048~65535)

四字节可编码的Unicode范围:\u10000~\u1FFFFF(65536~2097151)

UTF-8中的-8表示它是以byte为单位存储或传输,它的每一个字节的特征都是确定的(要么前面是10,要么前面的bit中的1的个数表示字节数,要么第1个bit为0,ASCII的编码范围),可以在不需要BOM的情况下解码(低地址字节总是首字节,其值表达了该字符总的字节数,按顺序存放和读取,读取网络字节流也是如此)。因此,使用UTF-8编码方式保存的文件,可以不需要BOM(Byte Order Mark)。(UTF-8 without BOM)

Python在内存中使用unicode,而python代码文件,我们总是选择使用UTF-8格式,我想很多其它的系统也是这样的设计:

bianma1 bianma2

UTF-8最适合存储和传输吗?

还是要看一下情况!

一般程序代码,基本都是ASCII符号,使用UTF-8编码方式来存储传输,是最节省空间的,因为每个符号都只需要1个byte!系统间的通信,也基本是这种情况。

但是如果是中文文档,采用UTF-8来保存,并不是最节省空间的编码方式,此时UTF-16可能是最合适的!

统一使用UTF-8,最方便!

计算机编码的发展

下面的文字,转自知乎上的一个用户的文章(这个用户说他也是转来的),略有编辑,介绍ASCII,UNICODE,UTF-8等各种编码方案:

很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为“字节”。再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始变来变去。他们看到这样是好的,于是它们就这机器称为“计算机”。

开始计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。 他们把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上0x10,终端就换行,遇上0x07, 终端就向人们嘟嘟叫,遇上0x1b,打印机就打印反白的字,或者终端就用彩色显示字母。他们看到这样很好,于是就把这些0x20以下的字节状态称为“控制码”。他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。大家看到这样,都感觉很好,于是大家都把这个方案叫做ANSI的ASCII编码(American Standard Code for Information Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。

后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255(ASCII最高位原始用来做奇偶校验的)。从128到255这一页的字符集被称“扩展字符集”。从此之后,贪婪的人类再没有新的状态可以用了,美帝国主义可能没有想到还有第三世界国家的人们也希望可以用到计算机吧!

等到中国人得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。但这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉,规定,一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在127号以下的那些就叫“半角”字符了。 中国人民看到这样很不错,于是就把这种汉字方案叫做“GB2312”。GB2312是对ASCII的中文扩展。

但是中国的汉字太多了,我们很快就发现有许多人的人名没有办法打出来。于是我们不得不继续把GB2312没有用到的码位找出来不客气地用上。后来发现还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为“GBK”标准,GBK包括了GB2312的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。 后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK扩成了“GB18030”。从此之后,中华民族的文化就可以在计算机时代中传承了。

中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做“DBCS”(Double Byte Charecter Set 双字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字符串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。那时候凡是受过加持,会编程的计算机僧侣们都要每天念下面这个咒语数百遍: “一个汉字算两个英文字符!一个汉字算两个英文字符……”

因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用着同一语言的地区,也分别采用了不同的DBCS编码方案。当时的大陆人想让电脑显示汉字,就必须装上一个“汉字系统”,专门用来处理汉字的显示、输入的问题,但是那个台湾的愚昧封建人士写的算命程序就必须加装另一套支持BIG5编码的什么“倚天汉字系统”才可以用,装错了字符系统,显示就会乱了套!这怎么办?而且世界民族之林中还有那些一时用不上电脑的穷苦人民,他们的文字又怎么办? 真是计算机的巴比伦塔命题啊!

正在这时,大天使加百列及时出现了,一个叫ISO(国际标准化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它“Universal Multiple-Octet Coded Character Set”,简称UCS, 俗称“Unicode”。

Unicode开始制订时,计算机的存储器容量有了极大的发展,空间再也不成为问题了。于是ISO就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ASCII里的那些“半角”字符,Unicode保持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于“半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

这时候,从旧社会里走过来的程序员开始发现一个奇怪的现象,他们的strlen函数靠不住了,一个汉字不再是相当于两个字节了,而是一个字节!是的,从Unicode开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的“一个字符”!同时,也都是统一的“两个字节”,请注意“字符”和“字节”两个术语的不同,“字节”是一个8位的物理存储单元,而“字符”则是一个文化相关的符号。在Unicode中,一个字符就是两个字节。一个汉字算两个英文字符的时代已经快过去了。

Unicode同样也不完美,这里就有两个的问题,一个是,如何才能区别Unicode和ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储空间来说是极大的浪费,文本文件的大小会因此大出二三倍,这是难以接受的。

Unicode在很长一段时间内无法推广,直到互联网的出现,为解决Unicode如何在网络上传输的问题,于是面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次传输16个位,UTF-32每次传输32位。UTF-8就是在互联网上使用最广的一种Unicode的实现方式(早期的互联网带宽资源很贵,所有UTF-8开始流行),这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,当字符在ASCII码的范围时,就用一个字节表示,保留了ASCII字符一个字节的编码做为它的一部分,需要注意的是,Unicode一个中文字符占2个字节,而UTF-8一个中文字符占3个字节。从Unicode到UTF-8并不是直接的对应,而是要过一些算法和规则来转换。

Python中的encode和decode

Python在内存中,使用Unicode编码方式保存字符串,在各种IO场景下,就存在各种encode和decode的需求。

encode和decode这两个接口,默认使用utf-8,这两个接口还有个非常有用的参数,errors='strict',用来处理编解码过程中可能出现的错误:

>>> a = 'abc123'
>>> a.encode()
b'abc123'
>>> a.encode().decode()
'abc123'

为了让程序不要raise UnicodeEncodeError或UnicodeDecodeError,就需要指定errors这个参数:

>>> a = 'abc\xAA\xBB'
>>> a.encode('ascii', 'replace')
b'abc??'
>>> a.encode('ascii', 'backslashreplace')
b'abc\\xaa\\xbb'
>>> a.encode('ascii', 'ignore')
b'abc'
>>>
>>> a = b'abc\xAA\xBB'
>>> a.decode('ascii', 'replace')
'abc��'
>>> a.decode('ascii', 'backslashreplace')
'abc\\xaa\\xbb'
>>> a.decode('ascii', 'ignore')
'abc'

Unicode 中定义了一个特殊字符「�」即 U+FFFD,称作 Replacement Character。用来表示无法显示的字符或是无法解析的数据。

还可以通过codecs.register_error来注册自定义的错误处理函数

surrogateescape

Surrogate 是 Unicode 中位于 BMP 外的一组不会有对应字符的码位,Python3中使用这些码位来「代表」无法编码的字节。

如上面示例代码中 Python 的 replace 编解码错误处理机制:无法解码的字节会被替换成U+FFFD,无法编码的码位会被替换成?。这样做有一个明显的缺点就是不可逆

Python3中新增的 surrogateescape 则是一种可逆的错误处理机制,利用 Surrogate 码位保存无法解码的字节,编码时则将其还原为对应的原始字节。

>>> a = b'\xCD'
>>> a.decode('ascii', 'surrogateescape')
'\udccd'
>>> '\udccd'.encode('ascii', 'surrogateescape')
b'\xcd'

在解码时,将字节替换为 U+DC80 至 U+DCFF 范围内的单个代理代码。 当在编码数据时使用 'surrogateescape' 错误处理方案时,此代理将被转换回相同的字节。

Emoji

Emoji早先由日本企业发明,日文将其称为“絵文字”。后来随着智能手机的推广,全世界都在用,于是被Unicode收编了。截至Unicode 10.0,共有1144个emoji被收录。

简单地说,Emoji已经成为Unicode字符,可以直接print:

>>> chr(0x231b)
'⌛'
>>> chr(0x2757)
'❗'
>>> chr(0x2B50)
'⭐'
>>> print(chr(0x2B50))

>>> print(chr(0x2B50)*8)
⭐⭐⭐⭐⭐⭐⭐⭐

单色与彩色

emoji比我最初的认识还要复杂很多。Unicode的emoji除了可以使用大家平时见到的彩色来展示,还可以用单色来展示,以适应一些非常简单的显示设备。

怎么做呢?规则就是在普通的emoji码点之后,紧跟一个用来表示颜色版本的“变幻符”,这个变幻符有两个取值:VS15(U+FE0E)和VS16(U+FE0F)。其中VS15表示强制使用单色版,而VS16则表示强制使用彩色版。如果没有变幻符呢,每个emoji可以使用自己默认的展示。

举个例子来说,U+26A0这个emoji可以有两种样子:

各位还记得Unicode当中的“组合字”么,异曲同工。

Unicode并不是一个码点一个符号这么简单!!

在网页中使用Emoji

在网页中使用Emoji的好处是,不用上传图片,输入更简单快捷,也没有增加带宽占用,由浏览器直接渲染。

这是一份不完整的emoji的编码对照表:https://www.w3school.com.cn/charsets/ref_emoji.asp

示例: ⌚ ⏰ ⏳

本文链接:https://cs.pynote.net/sf/202109105/

-- EOF --

-- MORE --