跳转至

Python中的字节类型

Python中的字节类型包括字节流与字符串。

字符串是由字符组成的序列。字符是组成字符串的基本单位,对字符串的切片等操作以字符为单位进行。一个字符由两个部分进行定义:

  • 码位,即字符的字节数值。
  • 编码方式,即字节数值与字符的对应关系。

通过编码与解码操作,可以实现字符串与字节序列之间的转换:

>>> "abc".encode() 
b'abc'
>>> b"abc".decode()
'abc'
>>>

Python提供了两种字节对象,即bytesbytearray,两者都是由无符号字节(取值范围为0~255)为单位组成的序列。bytes是不可变序列。

>>> b"\xff\xff"[0]
255
>>> bytes(3)[0]=1   
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
>>>

New in version 3.5

bytes对象与bytearray对象添加了hex方法,返回字节的十六进制表示形式。该函数与bytes对象的fromhex方法相反。

>>> import random
>>> seq = random.randbytes(8) # New in Python 3.9
>>> seq.hex() # doctest: +SKIP
'f04be4376519e9ce'
>>>

Changed in version 3.8

hex方法新增了可选的sep参数与bytes_per_sep参数。

  • sep参数指定区段间的连接字符串;
  • bytes_per_sep参数用于划分连续的字节段,字节段从右往左进行划分
# Following the previous example
>>> seq.hex('-') # doctest: +SKIP
'f0-4b-e4-37-65-19-e9-ce'
>>> seq.hex('-', 2) # doctest: +SKIP
'f04b-e437-6519-e9ce'
>>> seq.hex('-', 3) # doctest: +SKIP
'f04b-e43765-19e9ce'
>>>

字节序列中的字节有三种表示方式:

  • ASCII中规定的可打印字符,使用该字符本身
  • 制表符、换行符、回车符与反斜杠使用对应的转义序列表示,即\t, \n, \r, \\
  • 所有字节都可以使用十六进制转义序列表示,如\x00
>>> b"\t" == b"\x09"
True
>>>

字符串与字节序列的区别在于:字符串的索引与切片操作返回的对象都是字符串类型;而字节序列的索引操作返回int类型,切片操作返回一个字节序列。

构造

字符串的构造非常简单,此处不做讨论。在字符串前加r可以取消字符串内部的转义,如:

>>> print(r"ab\n") 
ab\n
>>>

在字符串前加b可以构造一个字节序列。字符串中只能包含ASCII可打印字符。

除此之外,字节序列还有如下构造方式:

  • 指定一个字符串和对应的编码方式,将该字符串编码为字节序列
  • 一个仅包含0~255内数值的可迭代对象
  • 一个实现缓冲协议的对象,将该对象中的字节序列复制到新的字节序列中(可能涉及类型转换)
  • 一个整数,创建对应长度的空字节对象

如,从array.array对象创建字节序列:

>>> import array  
>>> import random
>>> a = array.array("H", [51417, 45016, 65120, 9976])
>>> b = bytes(a)
>>> b
b'\xd9\xc8\xd8\xaf`\xfe\xf8&'
>>>

结构体

struct模块提供了将字节序列转换为不同类型字段的元组,类似于C语言结构体的功能。

结构定义

结构定义包含两个部分,即字节顺序与字段。struct模块允许多种字节顺序

字符 字节顺序 大小 对齐方式
@ native native native
= native standard none
< little-endian standard none
> big-endian standard none
! network (= big-endian) standard none

默认的字节顺序为@

struct模块的字段定义如下,所有的字段在C语言中都有对应的类型:

字符 C 类型 Python 类型 字宽
x (填充字节) N/A
c char 长度为1的字节 1
b signed char int 1
B unsigned char int 1
? _Bool bool 1
h short int 2
H unsigned short int 2
i int int 4
I unsigned int int 4
l long int 4
L unsigned long int 4
q long long int 8
Q unsigned long long int 8
n ssize_t int (仅适用于默认或@字节顺序)
N size_t int (仅适用于默认或@字节顺序)
e (半精度) float 2
f float float 4
d double float 8
s char[] bytes 与字符串长度有关
p char[] bytes 与字符串长度有关
P void * int

当试图将非整数对象打包为整数类型时,会调用对象的__index__方法。

一个结构体的定义是一个字符串,按照如下结构组织:

  1. 第一个字符表示字节顺序
  2. 此后的字符串表示结构体中的字段类型
  3. sp以外,字母前的数字表明该字段重复出现的次数
  4. sp前的数字表明字符串的长度

结构体字符串可以创建一个struct.Struct对象,两者实现相同的功能。

结构操作

对于一个定义的结构体,可以将数据按照结构体进行打包,或将结构体中的数据解包,也可以显示结构体的大小。

>>> import struct   
>>> struct.pack('<hhf', 1, 2, 3)
b'\x01\x00\x02\x00\x00\x00@@'
>>> struct.unpack('<hhf', b'\x01\x00\x02\x00\x00\x00@@')
(1, 2, 3.0)
>>>

New in version 3.4

struct对象新增了iter_unpack对象,不同于unpack函数,iter_unpack函数返回一个迭代器。

struct模块不会对字节顺序进行检测,因此对于同一个字节序列,不同的结构体定义在解包后会有不同的结果:

>>> struct.unpack('>hhf', b'\x01\x00\x02\x00\x00\x00@@') 
(256, 512, 2.304855714121459e-41)
>>>

每个字段都有范围限制,当传入的参数超过字段所允许的范围,则会抛出异常:

>>> struct.pack('<hhf', 32768, -32769, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
struct.error: short format requires (-32768) <= number <= 32767
>>>

当解包的字节长度与结构体的长度不对应时,也会抛出异常:

>>> struct.unpack('>hhf', b'\x01\x00\x02\x00\x00\x00@')  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
struct.error: unpack requires a buffer of 8 bytes
>>>

对于任何结构体,struct模块提供了calcsize方法用于检查结构体长度。

内存视图

内存视图提供了在不同对象间共享内存的方式。

有关内存视图的内容,请参见序列类型的内存视图部分。

字符串

此处着重讨论字符串的相关问题

编码与解码

如前所述,str.encode方法提供了从字符串到字节序列的转换方式,encoding参数指明了所使用的编码器。

>>> "测试".encode("utf-8") 
b'\xe6\xb5\x8b\xe8\xaf\x95'
>>> "测试".encode("utf-16")
b'\xff\xfeKm\xd5\x8b'
>>> "测试".encode("gb2312")  
b'\xb2\xe2\xca\xd4'
>>>

当编码过程出现错误,如编码器无法识别字符串中的字符时,会抛出UnicodeEncodeError异常。

>>> "测试".encode("latin-1")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 0-1: ordinal not in range(256)
>>>

出现错误时,有以下解决方式,可以通过errors参数指定:

  • ignore:跳过无法编码的字符
  • replace:将无法编码的字符替换为?
  • xmlcharrefreplace:将无法编码的字符替换为xml实体(即XML中所使用的字符转换方式)
>>> "测试".encode("latin-1", errors="ignore")
b''
>>> "测试".encode("latin-1", errors="replace")
b'??'
>>> "测试".encode("latin-1", errors="xmlcharrefreplace")
b'&#27979;&#35797;'
>>>

对应地,解码器无法识别字节序列的字节时会产生UnicodeDecodeError异常。但不抛出异常不代表解码成功,解码得到的数据可能是无用数据。errors参数指定了解码器在出错时的行为,replace将无法编码的字符替换为

chardet是一个基于Python的字符编码检测工具,可以通过二进制序列对原始字符串的编码方式进行推断。不过推断仅适用于较长的字符串,因为任何字符串会有多个编码方式适用于同一个字符串的情况,所以无法完全确定字符串的编码方式。

BOM

BOM是字节序标记,对应的Unicode字符为U+FEFF(不存在U+FFFE字符,因此该字符可以用于推断字节顺序)

在UTF-16编码的字节序列开头会写入BOM,如果开头是b'\xff\xfe'两个字节,指明编码时使用little endian字节编码顺序。如果是b'\xfe\xff'两个字节,说明编码时使用的是big endian字节顺序。如果指明UTF-16所使用的字节顺序,如UTF-16LE或UTF-16BE,则不会生成BOM。

BOM仅用于推断字节顺序而不会出现在最终解码的字符串。

文本文件

使用open函数以文本模式打开一个文件时,最好指定文件的编码方式。

不要使用二进制方式打开文本文件。

Unicode规范化

考虑如下两个字符串:

>>> a = 'café'
>>> b = 'cafe\u0301'             
>>> print(a, b)
café café
>>> a == b
False
>>>

相同的打印结果,却对应不同的字符串,原因在于字符串b使用了U+0301字符作为重音标记(组合字符),多使用了一个字节。对于Python而言,这一段字符串的字节序列并不相同,因此认为a != b

>>> a.encode("utf-8")     
b'caf\xc3\xa9'
>>> b.encode("utf-8") 
b'cafe\xcc\x81'
>>>

unicodedata模块中的normalize函数提供了Unicode规范化的功能,该函数接收如下参数用于确定转换标准:

  • 'NFC':使用最少码位构成等价的字符串
  • 'NFD':将组合字符分解为基字符与单独的组合字符(如U+0301),可以用于去除字符串中的变音符号
  • NFKCNFKD会额外将兼容字符分解为一个或多个兼容分解,会导致数据损失,如下所示:
>>> from unicodedata import normalize
>>> normalize('NFKC', '㍿') 
'株式会社'
>>>

NKFCNFKD规范化可能会导致字符串的原意变化,但可以用于搜索引擎。

str.casefold函数提供了另一种规范化方式,即将字符串中的所有大写字母转为小写。与str.lower不同,部分字符会被替换成新的字符。

字符串排序

字符串的排序与其他数据的排序方式相同,都是按照码位升序排序。对于非ASCII字符可能会导致一些问题,如:

>>> sorted(['café', 'cafu'])       
['cafu', 'café']
>>>

模块locale提供了strxfrm函数,用于按照区域设置对字符串进行排序。如果操作系统支持,区域设置可以在setlocale函数中全局指定。

>>> import locale
>>> locale.setlocale(locale.LC_COLLATE, "en_US.UTF-8")
'en_US.UTF-8'
>>> sorted(['café', 'cafu'], key=locale.strxfrm)
['café', 'cafu']
>>>

API

部分涉及到字符串的一些函数可以输入字符串或字节序列。

re模块

不同于使用字符串构造的正则表达式,使用字节序列构造的正则表达式,\d\w只能匹配ASCII字符。

os模块

os模块中的所有字符串参数都可以使用字节序列替换。对于字符串,函数会使用sys.getfilesystemencoding()函数取得合适的编码器。此外,os模块提供fsencodefsdecode函数用于手动进行编码与解码操作。

评论