Unicode、bytes和bytearray
前言:
字符串是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),如果要表示更大的整数,就必须用更多的字节。比如两个字节可以表示的最大整数是65535
,4个字节可以表示的最大整数是4294967295
。由于计算机是美国人发明的,因此,最早只有127个字母被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII
编码,比如大写字母A
的编码是65
,小写字母z
的编码是122
。
Unicode编码:
Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。
Unicode标准也在不断发展,但最常用的是用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。现代操作系统和大多数编程语言都直接支持Unicode。
现在,捋一捋ASCII编码和Unicode编码的区别:ASCII编码是1个字节,而Unicode编码通常是2个字节。
字母A
用ASCII编码是十进制的65
,二进制的01000001
;
字符0
用ASCII编码是十进制的48
,二进制的00110000
,注意字符'0'
和整数0
是不同的;
汉字已经超出了ASCII编码的范围,用Unicode编码是十进制的20013
,二进制的01001110 00101101
。
如果把ASCII编码的A
用Unicode编码,只需要在前面补0就可以,因此,A
的Unicode编码是00000000 01000001
。
新的问题又出现了:如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
所以,又出现了把Unicode编码转化为“可变长编码”的UTF-8
编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:
字符 | ASCII | Unicode | UTF-8 |
---|---|---|---|
A | 01000001 | 00000000 01000001 | 01000001 |
中 | x | 01001110 00101101 | 11100100 10111000 10101101 |
从上面的表格还可以发现,UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:
浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:
所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />
的信息,表示该网页正是用的UTF-8编码。
1 . Python字符串使用Unicode编码来表示文本,且处理字符串和文本文件的Python代码很多,所有我觉得学习一下是非常有必要的。
大致而言,每个Unicode字符都是用一个码点(code point)表示,而码点是Unicode标准给每个字符指定的数字。这让你能够以任何现代软件都能识别的方式表示129个文字系统中的12万个以上的字符。当然,鉴于计算机键盘不可能包含几十万个键,因此有一种指定Unicode字符的通用机制:
使用16或32位的十六进制字面量(分别加上前缀\u
或\U
)或者使用字符的Unicode名称(\N{name}
)。
>>> "\u00C6" #16进制编码 'Æ' >>> "\U0001F60A" #32进制编码 '☺' >>> "This is a cat: \N{Cat}" 'This is a cat:🐱 '
要获悉字符的Unicode码点和名称,可在网上使用有关该字符的描述进行搜索,也可参阅特定的网站,如http://unicode-table.com。
Unicode的理念很简单,却带来了一些挑战,其中之一是编码问题。在内存和磁盘中,所有对象都是以二进制数字(0和1)表示的(这些数字每8个为一组,即1字节),字符串也不例外。在诸如C等编程语言中,这些字节完全暴露,而字符串不过是字节序列而已。为与C语言互操作以及将文本写入文件或通过网络套接字发送出去,Python提供了两种类似的类型:不可变的bytes
和可变的bytearray
。如果需要,可直接创建bytes
对象(而不是字符串),方法是使用前缀b
:
>>> b'Hello, world!' b'Hello, world!'
然而,1字节只能表示256个不同的值,离Unicode标准的要求差很远。Python bytes
字面量只支持ASCII标准中的128个字符,而余下的128个值必须用转义序列表示,如\xf0
表示十六进制值0xf0(即240)。
唯一的差别好像在于可用的字母表规模,但实际上并非完全如此。乍一看,好像ASCII和Unicode定义的都是非负整数和字符之间的映射,但存在细微的差别:Unicode码点是使用整数定义的,而ASCII字符是使用对应的数及其二进制编码定义的。这一点好像无关紧要,原因之一是整数0~255和8位二进制数之间的映射是固定的,几乎没有任何机动空间。问题是超过1字节后,情况就不那么简单了:直接将每个码点表示为相应的二进制数可能不再可行。这是因为不仅存在字节顺序的问题(即便对整数值进行编码,也会遇到这样的问题),而且还可能浪费空间:如果对于每个码点都使用相同数量的字节进行编码,就必须考虑到文本可能包含安那托利亚象形文字或皇家亚兰字母。有一种Unicode编码标准是基于这种考虑的,它就是UTF-32(32位统一编码转换格式,Unicode Transformation Format 32 bits),但如果你主要处理的是使用互联网上常见语言书写的文本,那么使用这种编码标准将很浪费空间。
然而,有一种非常巧妙的替代方式:不使用全部32位,而是使用变长编码,即对于不同的字符,使用不同数量的字节进行编码。这种编码方式主要出自计算机先锋Kenneth Thompson之手。通过使用这种编码,可节省占用的空间,就像摩尔斯码使用较少的点和短线表示常见的字母,从而减少工作量一样。具体地说,进行单字节编码时,依然使用ASCII编码,以便与较旧的系统兼容;但对于不在这个范围内的字符,使用多个字节(最多为6个)进行编码。下面来使用ASCII、UTF-8和UTF-32编码将字符串转换为bytes
。
>>> "Hello, world!".encode("ASCII") b'Hello, world!' >>> "Hello, world!".encode("UTF-8") b'Hello, world!' >>> "Hello, world!".encode("UTF-32") b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00,\x00\ x00\x00 \x00\x00\x00w\x00\x00\x00o\x00\x00\x00r\x00\x00\x00l\x00\x00\x00d\x00\x00\x00!\x00\ x00\x00'
从中可知,使用前两种编码的结果相同,但使用最后一种编码的结果长得多。再来看一个示例:
>>> len("How long is this?".encode("UTF-8")) 17 >>> len("How long is this?".encode("UTF-32")) 72
只要字符串包含较怪异的字符,ASCII和UTF-8之间的差别便显现出来了:
>>> "Hællå, wørld!".encode("ASCII") Traceback (most recent call last): ... UnicodeEncodeError: 'ascii' codec can't encode character '\xe6' in position 1: ordinal not in range(128)
斯堪的纳维亚字母没有对应的ASCII编码。如果必须使用ASCII编码(这样的情况肯定会遇到),可向encode
提供另一个实参,告诉它如何处理错误。这个参数默认为strict
,但可将其指定为其他值,以忽略或替换不在ASCII表中的字符。
>>> "Hællå, wørld!".encode("ASCII", "ignore") b'Hll, wrld!' >>> "Hællå, wørld!".encode("ASCII", "replace") b'H?ll?, w?rld!' >>> "Hællå, wørld!".encode("ASCII", "backslashreplace") b'H\\xe6ll\\xe5, w\\xf8rld!' >>> "Hællå, wørld!".encode("ASCII", "xmlcharrefreplace") b'Hællå, wørld!'
几乎在所有情况下,都最好使用UTF-8。事实上,它也是默认使用的编码。
>>> "Hællå, wørld!".encode() b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!'
这相比于Hello, world!,编码结果要长些;但使用UTF-32编码时,结果一样长。
可将字符串编码为bytes
,同样也可将bytes
解码为字符串。
>>> b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!'.decode() 'Hællå, wørld!'
与前面一样,默认编码也是UTF-8。你可指定其他编码,但如果指定的编码不正确,将出现错误消息或得到一堆乱码。bytes
对象本身并不知道使用的是哪种编码,因此你必须负责跟踪这一点。可不使用方法encode
和decode
,而直接创建bytes
和str
(即字符串)对象,如下所示:
>>> bytes("Hællå, wørld!", encoding="utf-8") b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!' >>> str(b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!', encoding="utf-8") 'Hællå, wørld!'
这种方法更通用一些,在你不知道类似于字符串或bytes
的对象属于哪个类时,使用这种方法也更管用。一个通用规则是,不要做过于严格的假设。
编码和解码的最重要用途之一是,将文本存储到磁盘文件中。然而,Python提供的文件读写机制通常会替你完成这方面的工作!只要文件使用的是UTF-8编码,就无需操心编码和解码的问题。但如果原本正常的文本变成了乱码,就说明文件使用的可能是其他编码。在这种情况下,对导致这种问题的原因有所了解将大有裨益。
注意 源代码也将被编码,且默认使用的也是UTF-8编码。如果你想使用其他编码(例如,如果你使用的文本编辑器使用其他编码来存储源代码),可使用特殊的注释来指定。
# -*- coding: encoding name -*-
请将其中的encoding name
替换为你要使用的编码(大小写都行),如utf-8
或latin-1
。
最后,Python还提供了bytearray
,它是bytes
的可变版。从某种意义上说,它就像是可修改的字符串——常规字符串是不能修改的。然而,bytearray
其实是为在幕后使用而设计的,因此作为类字符串使用时对用户并不友好。例如,要替换其中的字符,必须将其指定为0~255的值。因此,要插入字符,必须使用ord
获取其序数值(ordinal value)。
>>> x = bytearray(b"Hello!") >>> x[1] = ord(b"u") >>> x bytearray(b'Hullo!')
Python官方网站-uincode:https://docs.python.org/3/howto/unicode.html
注:编写正则表达式时,原始字符串会很有用。