编码相关内容详解

我们在计算机编程中,经常会遇见输出的字符串不是我们想要的,或者同样的字符串在不同环境中输出却不一致,第一部分主要是搞懂以下这些内容究竟有什么区别:

strUnicode通用字符集UTF-8GBK烫烫烫 锟斤拷ASCIIbytes16进制字符串字符串的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”。

根据上面的介绍,我们可以概括的来理解这两个东西。

  1. 两者都是一种字符编码方案,即为每一个字符而非字形定义唯一的代码,这个方案是一种标准,只是在计算机科学领域,给某一个字符一个确定的值,例如a的Unicode值为97,0的Unicode值为48,的Unicode值为20320
  2. Unicode码通用字符集是两个不同组织出于同样的目的所制定的两套标准,两者相互发展,慢慢的走向一致,因为世界上不需要两套不兼容的字符集。所以两者的字码值是一致的
  3. 使用U+XXXX来表示码值为XXXX的字符
  4. 两者都是字符编码标准,不是实现标准,即其只考虑字符与代码的一一对应,而不考虑计算机中的具体实现。
  5. 统一码的编码方式与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-8utf-7utf-16utf-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下需要看下记事本编码格式):

1
你好 123abc!@#

以二进制格式打开如下:

utf-8

我们来分析一下,以为例:

1
2
3
4
5
6
'你'的Unicode码值为 20320 转为16进制 4f60
按utf-8编码方式 码值位数为16 所以需要3个字节 前三个字节为E4BDA0
我们将其写为2进制格式 看是否正确
E4BDA0: 11100100 10111101 10100000
4F60: 0100 111101 100000

可以发现确实如编码格式所叙述的那样。我们再来看一下后面的字符,例如@:

1
2
'@' Unicode 码 为 64 转为16进制 40
发现均为40 也说明了UTf-80-127与ASCII保持一致

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,即:

1
2
3
高位时0x810xFE 也就是 1000 000111111110
低位时0x400xFE 也就是 0100 000011111110
但不包括低字节是0x7F的组合。

简单看一下,可以看到GBK高位最高位为1,可以有效与ascii区分,但低位开头却有0100 0000这种,与单个ASCII码值一致,这就造成了发生冲突的可能性。

顺便提一下,GBK编码不像UTF-8,可以通过计算得出一个字符的编码结果,举个栗子:

我们要计算这个字的UTF-8编码值,之前讲过了,由于的unicode码值为4f60,然后把其填充进1110xxxx 10xxxxxx 10xxxxxx即可。
那么对于GBK编码呢?由于其与Unicode兼容,即也认为的unicode码值为4f60,然后呢,没有然后了。GBK编码是无法计算的!需要使用GBK编码表进行转换 就是类似下边这个玩意:

gbk

GBK的内码不是连续的,而且和unicode标准没有必然的关系,所以大部分程序在做转换的时候都是依赖的转换表。

烫烫烫 屯屯屯 锟斤拷

烫烫烫 屯屯屯
大名鼎鼎的乱码,写过C语言的人没有没遇见过的吧。

这种乱码最常见的地方是Visual Studio里。
Visual Studio中,未初始化的栈空间用0xCC填充,而未初始化的堆空间用0xCD填充。
0xCCCC0xCDCD在中文GB2312编码中分别对应字和字。

锟斤拷
这涉及到UTF-8GBK编码 的转换

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,那么如果多个无法识别的字符连续出现,其编码后的值如下格式:

1
0xEF 0xBF 0xBD 0xEF 0xBF 0xBD 0xEF 0xBF 0xBD ...

那么如果有人并不知道其使用的是utf-8编码格式,而直接将其用GBK编码打开,即被解析为0xEFBF 0xBDEF 0xBFBD ...这三个汉字,而这三个汉字,正是:

1
2
3
锟(0xEFBF
斤(0xBDEF
拷(0xBFBD

str、bytes、unicode字符串、16进制数、16进制字符串等相互转换

以下内容均以python为例

字符编码

在平常编写程序过程中,经常会遇见对于字符串处理时,所要求的参数不正确,举个例子:

1
2
3
4
5
# 求MD5值:
import hashlib
s = 'abc'
md5 = hashlib.md5(s).hexdigest()

我们觉得应该能得到结果,但会抛出

1
TypeError: Unicode-objects must be encoded before hashing

查看这个函数会发现,You can now feed this object with bytes-like objects (normally bytes)即其要求一个bytes-like objects,改为:

1
hashlib.md5(s.encode('utf-8')).hexdigest()

即可。还有例如接收服务器字符串时有时候是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

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> type('abc')
<type 'str'>
>>> type(u'abc')
<type 'unicode'>
>>> 'str'.decode('utf-8')
u'str'
>>> print '\xe4\xbd\xa0'
>>> print u'\xe4\xbd\xa0'
ä½
>>> '\xe4\xbd\xa0'.decode('utf-8')
u'\u4f60'

python3.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> type('你')
<class 'str'>
>>> type(u'你')
<class 'str'>
>>> '你'.encode('utf-8')
b'\xe4\xbd\xa0'
>>> b'\xe4\xbd\xa0'.decode('utf-8')
'你'
>>> '你'.encode('gbk')
b'\xc4\xe3'
>>> '你'.encode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\u4f60' in position 0: ordinal not in range(128)

我相信上面的讲解和例子足以让你完全弄明白字符串的编解码。
python3.x中,还有一个bytearray,如字面意思,byte数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> a = bytearray(b'\xe4\xbd\xa0')
>>> a
bytearray(b'\xe4\xbd\xa0')
>>> a[0]
228
>>> a[1]
189
>>> a = bytearray('好',encoding='utf-8')
>>> a
bytearray(b'\xe5\xa5\xbd')
>>> a = bytearray('好')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: string argument without an encoding

我相信你能够很容易理解它。

16进制数转换

经常遇见16进制原始字符串或者16进制数表示一个字符串的情况,其实就是字符串bytes格式去掉相关的\x等内容。

1
2
3
4
'你好abc'
E4BDA0E5A5BD616263
0xE4BDA0E5A5BD616263
'E4BDA0E5A5BD616263'

在某些语言中可以使用这种16进制数或字符串来表示一个字符串,在python中,相关的转换可以使用binascii模块或者hex 编码来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import binascii
>>>
>>> binascii.b2a_hex('你好abc123'.encode('utf-8'))
b'e4bda0e5a5bd616263313233'
>>> binascii.hexlify('你好abc123'.encode('utf-8'))
b'e4bda0e5a5bd616263313233'
>>> binascii.b2a_hex('你好abc123'.encode('utf-8')).upper()
b'E4BDA0E5A5BD616263313233'
>>> '你好abc123'.encode('utf-8').hex()
'e4bda0e5a5bd616263313233'
>>> binascii.a2b_hex(b'e4bda0e5a5bd616263')
b'\xe4\xbd\xa0\xe5\xa5\xbdabc'
>>> binascii.unhexlify(b'e4bda0e5a5bd616263')
b'\xe4\xbd\xa0\xe5\xa5\xbdabc'