Python3里的unicode和byte string

Python3里的unicode和byte string

原文: Python 3 Unicode and Byte Strings

Python2和Python3的一个显著区别:字符数据分别是用unicode和bytes存储的。在迁移老代码和新写代码时,你可能都不不知道这个区别,因为大多数字符串算法同时支持这两种表示;但你应该知道它们的区别。

如果你在使用web service的一些库,比如urllib(以前叫urllib2)和requests、网络socket、二进制文件、基于pySerial的串口I/O等,会发现它们现在都是用byte string存数据的。

在比较字符串常量时,你很可能会发现问题。拿unicode string和byte string比较会失败:有序比较(<, <=, >, >=)时会触发TypeError,判断是否相等时则始终会返回False。

字符集简史

如果你对unicode很熟悉,知道过字符集映射,可以跳过本小节直接看 Python和unicode 一节。

计算机(底层)只能处理数字,因此,为了处理文本,每个字符被赋予了一个独立的数字,叫做字符码(character code)。你肯定对ASCII很熟悉,它给128个字符定义了标准码,包括95个可打印字符和33个不可打印字符(例如空格、LF,tab,Esc,CR)。它的定义追溯至1963年,最初是用来给电报和串口通信使用的:它们用一个字节(8bit)中的7个bit来存储字符,而最高位则当做纠错码使用。

ASCII只定义了英文字母以及美国的常用符号。尽管包含美元符号$(代码是24(16进制)或36(10进制)),但并不包含其他货币符号,例如英镑£,日元¥,欧元€。

当数据被存储到磁盘或通过网络纠错协议传输时,单个字节的最高位(第8位)被空闲出来,可以用于映射额外的128个字符。然而这一位要怎样使用,映射到哪些字符,有多种方式而且并不统一。尽管Extended ASCII(8bit)包含了英镑符号£、日元符号¥,但是那会儿欧元还没确立,也就不可能包含欧元符号了。

Extended ASCII之后,产生了多个版本的8-bit编码集,其中英国最多使用的是Latin-1(ISO-8859-1)和Microsoft CP1252。这两个编码集都映射了英镑、日元以及其他西欧重音字符,但它们不是100%兼容对方,并且都不包含英镑符号。

英镑符号最后实在ISO-8859-15中定义的,它在8859-1的基础上做了少量修改,而许多网站则用8859-1指代8859-15。另一方面,CP1252的第三版增加了欧元符号的定义。

这些提到的内容,无疑增加了字符集映射的混乱;而wikipedia上则列出了至少50种不同的字符编码标准。

Unicode

到20世纪80年代末,人们试图用两个字节来定义一个通用字符集(unicode),他可以唯一地定义65000多个不同的字符,包括西欧字符、希腊字母、西里尔文、阿拉伯文、科普特和其他东欧字符,以及亚洲字符,像在日文和中文中的汉子都被包含在内。

unicode的前256个字符和ISO-8859-1(Latin-1)字符是完全重合的。欧元符号被正式定义为代码#20AC。

unicode的目的是用2个字节对现代语言中广泛使用的字符进行编码。1996年,unicode 2定义了扩展平面,允许4字节字符代码支持古代文字和特殊字符集的映射。

unicode 2将字符代码D800-DBFF定义为高代理代码点,后面必须跟第二个双字节代码(低代理点)。尽管需要4字节,但这两个字节组合在一起形成了一个3字节的代码,用于10000到10FFF的字符(在当前规范中)。Emojis是需要4字节扩展点的代码的一个很好的例子:笑脸 🙂 是字符代码1F642(编码为D83DDE42)。

截至本博客发文,有超过137000个unicode字符。还有一些非官方的unicode映射,包括为pIqad Klingon等语言构建的脚本。

unicode的缺点

unicode的明显缺点是需要为每个字符使用两个字节。这增加了内存、磁盘、I/O时间的开销,同时降低了数据传输速率。

为了支持更有效地处理西方字符集的方法,定义了UTF-8编码方案,该方案最多使用4个字节来存储任何Unicode字符。ASCII码只需要一个字节,7FF以下的代码需要两个字节(这包括大多数欧洲、中东和西里尔字符)。FFFF以下的字符代码需要3个字节,其余的需要4个字节。这意味着欧元符号(20AC)需要三个字节,而emoji符号比如笑脸(1F642)需要四个字节。

(疑问:UTF-8和unicode兼容吗??)

Python和unicode

第一次遭遇Python unicode问题,可能是读取文本文件时发生编码报错,也可能是字符无法正确显示在屏幕上。

python3在读取文本文件时创建一个TextIO对象,它使用默认编码将文件中的字节映射为Unicode字符。在Linux和OSX下,默认编码是UTF-8,而Windows采用CP1252。

如果文本文件不使用Python假定的默认编码,则需要在打开该文件时指定编码。要读取使用Latin-1(ISO-8859-1)编码的文件,请使用'latin-1':

with open('example.txt', mode='r', encoding='latin_1'):
    pass

支持的编码的完整列表在 python api编解码器页面 上。

Python3的所有字符串常量都是unicode。使用小写转义字符\u开头、紧跟4个数字的转义序列(\uxxxx)来定义FFFF之前(包括FFFF)的unicode字符。欧元符号定义为:

euro = '\u20AC'

高于FFFF的unicode字符,使用大写转义字符\U开头、紧跟8个数字(\Uxxxxxxxx)。笑脸的emoji符号定义为:

smile = '\U0001F642'

如果打印此值,则仅当输出设备支持emojis时才会显示高兴的表情(实测,Win10 powershell支持emoji,cmd/wsl则不支持)。

如果你在python2中使用过Unicode文本,那么你肯定知道在字符串文本定义前增加'u'来表示Unicode字符串文本(u'Hello world!)。 Python3仍然支持这种表示法,只是不再需要了,因为Python3的字符串就是unicode。

Python和byte string

Unicode string,类型为str
Byte string, 类型为bytes

如果你使用底层数据连接,例如串口或网络套接字(包括web连接和蓝牙),你会发现python3以字节字符串的形式传输数据:数据类型为bytes。类似地,如果你以二进制模式打开一个文件,你将使用字节字符串(byte string)。

Python3中的字节字符串(也就是打印出来为bytes的类型),支持Unicode字符串(数据类型str)提供的大多数方法。如果代码使用字符串方法、下标和切片(slice),通常来说代码不用改,可以继续使用。

但也有例外的情况,例如startswith方法,虽然它支持

  • some_unicode_string.startswith(another_unicode_string),和
  • some_byte_string.startswith(another_byte_string)
    但它不支持:
  • some_unicode_string.startswith(some_byte_string),以及
  • some_byte_string.startswith(some_unicode_string)

在用字符串常量来定义字符串时,在字面量前面增加b表示定义一个byte string,例如:

byte_hello = b'Hello world!'

byte string支持常见的反斜杠转义字符,并且可以包含十六进制代码,例如英镑符号用\xA3表示(假设是latin-1字符集)。可以在原始byte string前添加前缀brrb来避免反斜杠转义识别。

注意,byte string和unicode string这两种string表示方式,永远是不兼容的,因此如下的表达式比较结果永远是False:

'Hello world' == b'Hello world!'

unicode string支持字符串format方法,而byte string并不支持format。如果你用format string来打印一个byte string,你看到的肯定是byte string的表示方式。例如:

message = b'world'
print('Hello {}!'.format(message))

对应的输出:

Hello b'world'!

字符串的编码和解码 (Encoding and Decoding Strings)

            decode()
byte     ------------>     unicode
string   <------------     string
            encode()

要把byte string转为unicode,用str.decode()方法,它接受一个编码参数,所有平台的默认编码都是UTF-8。因此前一个例子的改正写法是:

print('Hello {}!'.format(message.decode()))

如果你在用Windows CP1252字符集,并且是从二进制文件获取了文本(data是byte string),则可以用如下方式处理:

print('Hello {}!'.format(data.decode('cp1252'))

反过来,要把unicode string转为byte string,使用bytes.encode()方法,它也接受一个可选的编码参数,默认值也是UTF-8。要把我们的“hello world”内容写到Windows CP1252编码的文本文件,我们用:

message = 'world'
with open('hello.txt', 'wb') as fp:
    fp.write('Hello {}!'.format(message).encode('cp1252'))

实际上,对于本例,我们可能只需要单独编写byte string,或者使用字符串拼接:

b'Hello '+message.encode('cp1252')+b'!'

格式串(Format Strings)

作为Python2程序员,尤其是有C背景的程序员,你可能已经使用%运算符格式化输出。python3仍然支持unicode string和byte string。但是format string和任何字符串参数必须是同一类型(和前面提到过的startswith()类似)。要使用byte string的printf样式格式,请使用:

print(b'Hello %s!' % (b'world'))

但是如果你还没有治安想格式化字符串(从Python2.7开始有的),你真的应该用用它。因为format string比printf格式强大得多。推荐的格式化输出方法是使用str.format()方法。在最简单的行驶中,format string使用大括号来表示具有与printf相似的数据类型和字段宽度约定的可替换参数:

print('Hello {:s}!'.format('world'))

Python3.6引入了一个新的格式化字符串文本特性,它在unicode字符串上使用f作为前缀,一允许大括号内的任何Python表达式。通常缩写为f-strings,这样就避免了对简单格式化情况调用format()的需要,例如hello world实例中的以下变量:

message = 'world'
print(f'Hello {message}!')

事实上,任何有效的Python表达式都可以与大括号一起使用,因此可以调用函数并执行运算,例如:

import random
print(f'Random value {random.random()**2:.2f}')

但这在实践中可能不是一个好主意,因为很难识别表达式(random.random()**2)和字符串文本中的格式(:.2f)。如第一个例子所示,我们应该坚持使用简单的变量名。

如果使用格式化字符串文本,那么解析f字符串的IDE是必不可少的,因为它会突出语法错误和不正确的变量名。

在撰写本文时,PyCharm强调了语法错误和不正确的变量名。在其他流行的pythonide(Atom、Eclipse/pydev、Spyder和VSCode)中,对语法检查的支持有限(并不总是正确的),但对变量名没有语义检查。当你读到这篇文章的时候,这一点可能已经改变了,所以要确保你的IDE是最新的,这样你就可以得到f-string的支持了。

总结

Python3的string class(str)存储Unicode字符串,新的byte string(bytes)类支持单字节字符串。这两种类型不同,因此字符串表达式必须使用一种形式或另一种形式。以小写字母b开头的是byte string,不以b开头的则是unicode string。

尤其是在web页面上,通常需要将基本的UTF编码设置为8字符编码。将byte string转换为使用Unicode,使用bytes.decode()方法,而要转换Unicode为byte string,则使用str.encode()。如果需要UTF-8以外的字符集,这两种方法都允许将字符集编码指定为可选参数。

新的格式化字符串文本(也叫做f-string)允许在格式字符串的大括号内计算表达式,可以替代使用str.format()。

延伸

  • 应当用UTF-8格式存储.py文件,如果是Python2则应该用# coding: utf-8开头,避免中文乱码
  • 如果.py文件内容只包含ASCII的可打印字符,则存储为ANSI或UTF-8时,每个字符都是占1个字节,文件大小的轻微差距在于UTF-8格式文件本身的一些flags。
  • "UTF-8是Unicode的一种实现":Unicode仅仅是规定了每个字符对应的唯一数字(标量值)。至于这个数字在存储时用几个字节(字节序列),这是unicode编码形式(encoding form)。UTF-8则是建立unicode标量值和字节序列的一个真子集之间的双射。
  • "一个汉字算两个英文字符"的说法,不适用于Unicode。那是GB2312/GBK/GB18030编码系统下的便于记忆的规则,但如今应换用UTF-8。
  • Unicode 和 UTF-8 有什么区别? - 盛世唐朝的回答 - 知乎
  • 字符编码笔记:ASCII,Unicode 和 UTF-8 - 阮一峰的网络日志
  • Windows10的cmd.exe仍然不是UTF-8的,而Linux则默认使用UTF-8编码,如果你不想处理编码之间的相互转换,可以直接用Linux/MacOSX

打印chr(n)返回的结果,当n的取值范围不同时,开头的转义字符不一样:

\x开头:
ASCII范围内的数字(准确说是[0,160]区间内的数字),

chr(0)
'\x00'

chr(160)
'\xa0'

\u开头:unicode在FFFF之前,\u紧跟4个(16进制)数字:

In [57]: chr(57344)
Out[57]: '\ue000'

\U开头:unicode在FFFF之后,\U紧跟8个(16进制)数字:

In [58]: chr(1114111)
Out[58]: '\U0010ffff'

实际上,上述说法,在Python3.7.3下实测,并不严谨:

  • \x80也可以用\u0080\U00000080表示
  • ord('\x80')ord('\u0080')以及ord('\U00000080')打印,结果都是128
  • \ue0000chr(57344)的最短表示,但不能用\xhh表示。因为\xFF\x开头的能表示的最大值(255)

移植Python2的代码到Python3,发现原来定义为hoho='\x01\xF4'的变量,现在需要改成hoho=b'\x01\xF4'才能使用。这是能手动改代码的情况。假如没法修改hoho的值,并且也不知道hoho的具体取值,该怎么修改代码呢?尝试了hoho = hoho.encode(),虽然能运行,但是效果不对啊?

原因:此时应当用.encode('latin1')而不是.encode(),因为.encode()等同于.encode('utf-8')

In [36]: hoho = '\x01\xF4'

In [37]: hoho.encode('latin1')
Out[37]: b'\x01\xf4'

In [38]: hoho.encode()
Out[38]: b'\x01\xc3\xb4'

In [39]: hoho.encode('utf-8')
Out[39]: b'\x01\xc3\xb4'

举例

贴几个可以复现问题、带解决方案的 Python3 编码问题。

例子1

"""
Python编码问题,例子1

来源:https://www.cnblogs.com/WangAoBo/p/7108278.html
"""

import binascii
import struct

#\x49\x48\x44\x52\x00\x00\x01\xF4\x00\x00\x01\xA4\x08\x06\x00\x00\x00

crc32key = 0xCBD6DF8A
for i in range(0, 65535):
    height = struct.pack('>i', i)
    #CRC: CBD6DF8A
    
    # 直接用这句,报错
    #data = '\x49\x48\x44\x52\x00\x00\x01\xF4' + height + '\x08\x06\x00\x00\x00'

    # 方法1:换成这句即可
    data = b'\x49\x48\x44\x52\x00\x00\x01\xF4' + height + b'\x08\x06\x00\x00\x00'

    # 方法2:换成如下6句即可
    part1 = '\x49\x48\x44\x52\x00\x00\x01\xF4'
    part2 = height
    part3 = '\x08\x06\x00\x00\x00'
    part1 = part1.encode('latin1')
    part3 = part3.encode('latin1')
    data = part1 + part2 + part3

    crc32result = binascii.crc32(data) & 0xffffffff
    if crc32result == crc32key:
        print('height is', i)
        print(''.join(map(lambda c: "%02X" % c, height)))

例子2


"""
Python编码问题,例子2

报错信息:TypeError: a bytes-like object is required, not 'str'

来源:https://blog.csdn.net/weixin_40283816/article/details/83591582
"""

import codecs
import urllib.request
target_url = ('https://archive.ics.uci.edu/ml/machine-learning-'
              'databases/undocumented/connectionist-bench/sonar/sonar.all-data')
data = urllib.request.urlopen(target_url)
xList = []
labels = []
for line in data:
    # row = line.strip().split(',') # 报错
    # 如下的每一种修改方式,都可以
    # row = line.strip().split(',')
    # row = line.strip().split(b',')
    # row = line.strip().split(','.encode())
    # row = line.strip().split(','.encode('utf-8'))
    # row = line.strip().split(','.encode('latin1'))
    # row = line.decode().strip().split(',')
    # row = bytes.decode(line).strip().split(',')
    row = codecs.decode(line).strip().split(',')
    xList.append(row)

print('Number of Rows of Data = %d' % len(xList))
print('Number of Columns of Data = %d' % len(xList[1]))

例子3

"""
Python编码问题,例子3

报错:TypeError: a bytes-like object is required, not 'str'

来源:https://justcode.ikeepstudying.com/2019/01/python-3-5-a-bytes-like-object-is-requirednot-str-%E6%8A%A5%E9%94%99/
"""

import base64

a = 'hello'

# 这句会报错
#out = base64.b64encode(a)

# 改成如下即可
out = base64.b64encode(a.encode())

print(out)

例子4

"""
Python编码问题,例子4

报错:

来源:https://www.zhihu.com/question/60231684
"""

a = '中文'.encode('utf-8')
print(a)

test_str = '\xe4\xb8\xad\xe6\x96\x87'
print(test_str)
print(test_str.encode())

test_str2 = b'\xe4\xb8\xad\xe6\x96\x87'
print(test_str2.decode())



a = '中文'.encode('utf-8')
print(a)
test_str = '\xe4\xb8\xad\xe6\x96\x87'
print(test_str) # 输出乱码

print(test_str)
print(test_str.encode('latin1').decode())

例子5

"""
Python编码问题,例子5

报错: argument for 's' must be a bytes object

来源:https://segmentfault.com/a/1190000022812087
"""

a = 'hello {:s}'.format('Chris')
print(type(a))

F = open('data.bin', 'wb')
import struct

# 这句,py3下会报错
#data = struct.pack('>i4sh', 7, b'spam', 8)

# 改成这句即可(增加了'b')
data = struct.pack('>i4sh', 7, b'spam', 8)
data

例子6

"""
来自zheng tianqi的提问
"""

a = '\x00'

# 如何把a变成int类型?
# 实际上,此时的a相当于C语言中的 char a = 0,即ASCII的第一个字符
# 用ord打印即可
out = ord(a)
print(out)
posted @ 2020-09-13 14:12  ChrisZZ  阅读(3565)  评论(0编辑  收藏  举报