我们在计算机编程中,经常会遇见输出的字符串不是我们想要的,或者同样的字符串在不同环境中输出却不一致,第一部分主要是搞懂以下这些内容究竟有什么区别:
str
、Unicode
、通用字符集
、UTF-8
、GBK
、烫烫烫 锟斤拷
、ASCII
、bytes
、16进制字符串
、字符串的16进制数
Unicode码 与 通用字符集
Unicode
(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。Unicode伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2017年6月20日公布的10.0.0,已经收录超过十万个字符(第十万个字符在2005年获采纳)
Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。
Unicode发展由非营利机构统一码联盟(区别于ISO组织)负责,该机构致力于让Unicode方案取代既有的字符编码方案。因为既有的方案往往空间非常有限,亦不适用于多语环境。
Unicode备受认可,并广泛地应用于电脑软件的国际化与本地化过程。有很多新科技,如可扩展置标语言(Extensible Markup Language,简称:XML)、Java编程语言以及现代的操作系统,都采用Unicode编码。
Unicode wiki 这里可以看到相关的介绍,可以看一下。
通用字符集
(英语:Universal Character Set, UCS)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。
通用字符集的初衷是包括世界上所有字符,那么其他所有字符集的内容都能在通用字符集中找到。它保证了与其他字符集的双向兼容,即,如果你将任何文本字符串翻译到UCS格式,然后再翻译回原编码,你不会丢失任何信息。UCS包含了已知语言的所有字符。除了拉丁语、希腊语、斯拉夫语、希伯来语、阿拉伯语、亚美尼亚语、格鲁吉亚语,还包括中文、日文、韩文这样的方块文字,UCS还包括大量的图形、印刷、数学、科学符号。
UCS不仅给每个字符分配一个代码,而且赋予了一个正式的名字。表示一个UCS或Unicode值的十六进制数通常在前面加上“U+”,例如“U+0041”代表字符“A”。
根据上面的介绍,我们可以概括的来理解这两个东西。
- 两者都是一种
字符编码方案
,即为每一个字符而非字形定义唯一的代码,这个方案是一种标准,只是在计算机科学领域,给某一个字符一个确定的值,例如a
的Unicode值为97
,0
的Unicode值为48
,你
的Unicode值为20320
Unicode码
和通用字符集
是两个不同组织出于同样的目的所制定的两套标准,两者相互发展,慢慢的走向一致,因为世界上不需要两套不兼容的字符集。所以两者的字码值是一致的- 使用
U+XXXX
来表示码值为XXXX
的字符 - 两者都是字符编码标准,不是实现标准,即其只考虑字符与代码的一一对应,而不考虑计算机中的具体实现。
- 统一码的编码方式与
ISO 10646
的通用字符集概念相对应。目前实际应用的统一码版本对应于UCS-2
,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示216(即65536)个字符。基本满足各种语言的使用。
上述16位统一码字符构成基本多文种平面。基本多文种平面的字符的编码为U+hhhh
,其中每个h代表一个十六进制数字,与UCS-2编码完全相同。而其对应的4字节UCS-4编码后两个字节一致,前两个字节则所有位均为0。
最新(但未实际广泛使用)的统一码版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到ISO 10646-1实现级别3,即涵盖UCS-4的所有字符。UCS-4是一个更大的尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。
UTF (Unicode Transformation Format)
Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)
例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用UTF-8编码,这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以7位ASCII字符为主的西文文档就大幅节省了编码长度(具体方案参见UTF-8)。类似的,对未来会出现的需要4个字节的辅助平面字符和其他UCS-4扩充字符,2字节编码的UTF-16也需要通过一定的算法进行转换。
再如,如果直接使用与Unicode编码一致(仅限于BMP字符)的UTF-16编码,由于每个字符占用了两个字节,在麦金塔电脑(Mac)机和个人电脑上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在Mac OS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS环境下打开会显示成“奎”。此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian,简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案,目前在PC机上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16 LE。(具体方案参见UTF-16)
所以常见的UTF-X
系列都是编码方式的实现,没有规定要求必须使用哪种编码方式,但会有一些常见的编码方式,如utf-8
、utf-7
、utf-16
、utf-32
UTF-8
8-bit Unicode Transformation Format
是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。
UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节)
互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。
UTF-8就是以8位为单元对UCS进行编码,而UTF-8不使用大尾序和小尾序的形式,每个使用UTF-8存储的字符,除了第一个字节外,其余字节的头两个比特都是以”10”开始,使文字处理器能够较快地找出每个字符的开始位置。下面是一个UTF-8编码格式表:
码值位数 | 码值起始 | 码值终止 | 字节数 | byte 1 | byte 2 | byte 3 | byte 4 | byte 5 | byte 6 |
---|---|---|---|---|---|---|---|---|---|
7 | U+0000 | U+007F | 1 | 0xxxxxxx | |||||
11 | U+0080 | U+07FF | 2 | 110xxxxx | 10xxxxxx | ||||
16 | U+0800 | U+FFFF | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | |||
21 | U+10000 | U+1FFFFF | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
26 | U+200000 | U+3FFFFFF | 5 | 111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
31 | U+4000000 | U+7FFFFFFF | 6 | 1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
举个栗子,假设用文本编辑器输入以下内容(mac下默认是utf-8编码格式,win下需要看下记事本编码格式):
|
|
以二进制格式打开如下:
我们来分析一下,以你
为例:
|
|
可以发现确实如编码格式所叙述的那样。我们再来看一下后面的字符,例如@
:
|
|
UTF-7
16-bit Unicode Transformation Format
UTF-7本身并非Unicode的标准之一,即使在目前最新的Unicode 5.0里也仅列出UTF-8、UTF-16和UTF-32。由于在过去SMTP的传输仅能接受7比特的字符,而当时Unicode并无法直接满足既有的SMTP传输限制,在这样地背景下UTF-7被提出。严格来说UTF-7不能算是Unicode所定义的字符集之一,较精确的来说,UTF-7是提供了一种将Unicode转换为7比特US-ASCII字符的转换方式。
有些字符本身可以直接以单一的ASCII字符来呈现。
第一个组群被称作direct characters
,其中包含了62个数字与英文字母,以及包含了九个符号字符:' ( ) , - . / : ?
。这些direct characters
被认为可以很安全的直接在文件里呈现。另一个主要的组群称作optional direct characters
,其中包含了所有可被打印的字符,这些字符在U+0020~U+007E
之间,除了~ \ +
和`空白字符以外。这些
optional direct characters`的使用虽可减少空间的使用也可增加人的可阅读性,但却会因为一些不良设计的邮件网关而会产生一些错误,导致必须使用额外的转义字符。
空白字符、Tab字符、以及换行字符一般虽也可直接是为单一的ASCII字符来使用,然而,若是邮件中有使用了编码过的字符串,则必须特别注意这些字符有无被使用在其他地方。而加号字符+
的一种编码方式可以是+-
。
其他的字符则必须被编码成UTF-16然后转换为修改的Base64。这些区块的开头会以+符号来标示,结尾则以任何不在Base64里定义的字符来标示。若是在Base64区块之后使用-(连字暨减号)标示为结束的话,则解码器会从下个字符继续解码,反之则用此字符当非Base64的区块的开头继续解码。
UTF-7
在现今不太常见,一些CTF题目可能使用这个方式来加密,了解一下就好了。
UTF-16
16-bit Unicode Transformation Format
具体可以看UTF-16 wiki,不再赘述
ASCII
美国信息交换标准代码
7 bit
定义了128个字符,其中包括33个无法显示的控制字符和95个可显示的字符
ASCII码
要早于Unicode
,与其所做的事情是一致的。其局限在于只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,因此只能用于显示现代美国英语。虽然有ASCII的扩展版(8bit最高位不为0,又扩展了128个),但仍然无法满足世界众多种语言。
目前常用的编码,都保留了对ASCII码的兼容,即0-127
码值与其ASCII码保持一致。
GBK
汉字内码扩展规范
中国人那么叼,肯定要开发一套属于自己的编码,于是GBK编码诞生了。这套编码格式为中国程序员带来无数的坑。这个编码是和Unicode兼容的,也就是说映射表是一样的。
GBK编码严格使用2字节
来表示,因为给中国人使用,汉字和字母足够了。
GBK编码范围是0x8140
-0xFEFE
,即:
|
|
简单看一下,可以看到GBK高位最高位为1,可以有效与ascii区分,但低位开头却有0100 0000
这种,与单个ASCII码值一致,这就造成了发生冲突的可能性。
顺便提一下,GBK编码
不像UTF-8
,可以通过计算得出一个字符的编码结果,举个栗子:
我们要计算你
这个字的UTF-8
编码值,之前讲过了,由于你
的unicode码值为4f60
,然后把其填充进1110xxxx 10xxxxxx 10xxxxxx
即可。
那么对于GBK编码
呢?由于其与Unicode
兼容,即也认为你
的unicode码值为4f60
,然后呢,没有然后了。GBK编码是无法计算的!需要使用GBK编码表进行转换 就是类似下边这个玩意:
GBK的内码不是连续的,而且和unicode标准没有必然的关系,所以大部分程序在做转换的时候都是依赖的转换表。
烫烫烫 屯屯屯 锟斤拷
烫烫烫 屯屯屯
大名鼎鼎的乱码,写过C语言的人没有没遇见过的吧。
这种乱码最常见的地方是Visual Studio里。
Visual Studio中,未初始化的栈空间用0xCC
填充,而未初始化的堆空间用0xCD
填充。
而0xCCCC
和0xCDCD
在中文GB2312编码
中分别对应烫
字和屯
字。
锟斤拷
这涉及到UTF-8
到GBK编码
的转换
在UTF-8
中有一个特殊字符�
(Replacement character)
其Unicode
码值为0xFFFD
其作用是:
It is used to indicate problems when a system is unable to render a stream of data to a correct symbol. It is usually seen when the data is invalid and does not match any character.
也就是当前设备无法识别其Unicode
或版本过低不认识时,使用这个Unicode码
值代替,即用来容错的。因为随着UCS标准的更新,之前的设备并不能及时有效的更新。我们用utf-8
编码实现以下,填充进1110xxxx 10xxxxxx 10xxxxxx
中得到0xEF 0xBF 0xBD
,那么如果多个无法识别的字符连续出现,其编码后的值如下格式:
|
|
那么如果有人并不知道其使用的是utf-8编码
格式,而直接将其用GBK编码
打开,即被解析为0xEFBF 0xBDEF 0xBFBD ...
这三个汉字,而这三个汉字,正是:
|
|
str、bytes、unicode字符串、16进制数、16进制字符串等相互转换
以下内容均以python
为例
字符编码
在平常编写程序过程中,经常会遇见对于字符串处理时,所要求的参数不正确,举个例子:
|
|
我们觉得应该能得到结果,但会抛出
|
|
查看这个函数会发现,You can now feed this object with bytes-like objects (normally bytes)
即其要求一个bytes-like objects
,改为:
|
|
即可。还有例如接收服务器字符串时有时候是unicode字符串
,发现在调用函数时,需要传递一个str
字符串,他们之间的转换也是一个常见的问题。再例如在你想在终端中打印你想看到的字符串,但却发现显示是\x40\x45\x46...
等内容,更多的情景不再赘述。
首先摔个锅,python2.x关于字符编码方面非常混乱,导致使用的时候容易出错,再加上从2.x到3.x的迁移,python3.x的设计好很多,但是相同名称的类表示不同的含义,往往会造成不深入了解的人们很大的误会。我们来看下2.x和3.x对字符串的处理:
python2.x
关键字 | 含义及说明 |
---|---|
str | 某种编码类型编码后的字符串 UTF-8、GBK等 取决于python文件本身保存的编码格式 类似 # -*- coding: utf-8 -*- )这种 |
unicode | unicode类型字符串 直接 u'abc' 或者unicode() |
python3.x
关键字 | 含义及说明 |
---|---|
bytes | 某种编码类型编码后的字符串 UTF-8、GBK等 b'abc' 例如中文 你 utf8编码后的b'\xe4\xbd\xa0' |
str | unicode类型字符串 直接 '' 就是unicode类型字符串 |
我们来做一个分析:
发现不同版本都有2个类型,一个是unicode
类型字符串,一个是用某种编码编码后的字符串
,而2.x和3.x同样是str
却代表了不同的含义,这使得在版本迁移的时候,如果不看文档,很容易误以为是一样的。
unicode
字符串是一个基准,它用2字节来存储字符串每个字符的unicode码值
,这样在不同环境中迁移时才能保持一致。某种编码类型编码后的字符串
是使用编码方式之后的存储方式。两者类似unicode码
和utf-8
值一样。
python2.x
|
|
python3.x
|
|
我相信上面的讲解和例子足以让你完全弄明白字符串的编解码。
在python3.x
中,还有一个bytearray
,如字面意思,byte数组:
|
|
我相信你能够很容易理解它。
16进制数转换
经常遇见16进制原始字符串或者16进制数表示一个字符串的情况,其实就是字符串bytes格式去掉相关的\x
等内容。
|
|
在某些语言中可以使用这种16进制数或字符串来表示一个字符串,在python中,相关的转换可以使用binascii
模块或者hex 编码来实现
|
|