bytes是什么
由上篇文章几种字符编码, 我们已经知道了ASCII Unicode UTF-8的关系。而且,计算机只能识别0和1,那显然,文件存储在计算机中也只能是以二进制的形式存储,字符编码在计算机中的工作机制是怎样的呢?
在计算机内存中(你打开电脑上的一个文件是要从硬盘读取到内存中的),统一使用Unicode编码。在需要保存到硬盘或需要传输时,就转化为UTF-8编码(由上篇文章可知,这样可以节省空间,提高传输速度)。
如,在记事本编辑时,从文件读取的UTF-8字符被转化为Unicode字符到内存里,编辑完成,保存时在将内存中的Unicode字符转化为UTF-8保存到文件:
浏览网页时,服务器会把动态生成的Unicode字符转化为UTF-8字符再传输到浏览器:
所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />
的信息,表示该网页正在使用UTF-8编码。
在python中,字符串是以Unicode编码的,而python的字符串类型是str
,内存中以Unicode表示。要在网络上进行传输或保存到磁盘中,就需要将str
转化为以字节为单位的bytes
。
要获取字符的bytes
表示,可以使用encode()
方法,如
>>> 'ABC'.encode('ascii')
b'ABC'
>>>'ABC'.encode('utf-8')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
纯英文的str
,可以用ascii编码为bytes
,其内容与utf-8相同。含中文的str
不能用ascii编码为bytes
,其超过了ascii编码的范围,会报错。
bytes
中,无法显示为ASCII字符的字节,会以b\x##
的形式显示。
使用type可以查看b'abc'或b'\xe4\xb8\xad\xe6\x96\x87'的数据类型,是一个bytes类
>>> type(b'\xe4\xb8\xad\xe6\x96\x87')
<class 'bytes'>
相反,从网络上或磁盘中读取到了字节流,读到的就是bytes
,需要用decode()方法解码为str
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'
字节字符串与文本字符串
b'ABC' 与 'ABC'是不同的,前者是bytes
,也叫字节字符串;后者是str
,也称为文本字符串。前者一个字符占一个字节(中文一个汉字占三个字节),str
类型在内存中以Unicode表示,一个字符占若干字节。
>>> len('下'.encode('utf-8'))
3
在读取二进制数据的时候,字节字符串和文本字符串可能会引起错误。特别需要注意的是,索引和迭代动作返回的是字节的值而不是字节字符串。
>>> # Text string
>>> t = 'Hello world'
>>> for x in t:
... print(x)
...
H
e
l
l
o
w
o
r
l
d
>>> # Byte string
>>> b = b'Hello world'
>>> for x in b:
... print(x)
...
72
101
108
108
111
32
119
111
114
108
100
Base64:显示与打印二进制数据
Base64是一种用64个字符表示任意二进制数据的方法。
当我们用记事本打开bmp
, exe
, jpg
文件时,会出现一大堆乱码:
这是因为它们不是文本文件,是二进制文件,而二进制文件包含很多无法显示和打印的字符,所以,如果想让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符的转换方法,Base64是一种最常见的二进制编码方法。
注意:通常我们说编码都是将字符编码成二进制,将二进制解码为字符。而现在我们说的是将二进制编码为字符文本,将字符文本解码为二进制。不要弄混,笔者在最开始学的时候全程懵逼,完全搞不明白到底是在解码还是编码。
方法:
准备一个包含64个字符的数组:
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
对二进制数据进行处理,每三个字节一组,共3 * 8 = 24
bit,划为4组,每组6个bit:
我们得到四个数字作为索引,查表,得到对应的4个字符,就是编码后的字符串。
所以,我们是将3个字节的二进制数据编码为4个字节的文本数据,长度增加33%,好处是编码后的文本可以在邮件正文、网页中正常显示。
python
内置的base64
模块可以提供base64的编解码功能:
>>> import base64
>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd++//'
>>> base64.b64decode(b'abcd++//')
b'i\xb7\x1d\xfb\xef\xff'
当要编码的二进制数据不是3的倍数,最后会剩下1或2个字符时怎么办?Base64会自动在末尾用b\x00
补足后在进行解码,再在编码的结尾加上1或2个=
,以表示在二进制数据末尾加了几个b\x00
。
但是,在很多Base64编码中会把=
去掉,因为它会在URL,Cookies中造成歧义:
# 标准Base64:
'abcd' -> 'YWJjZA=='
# 自动去掉=:
'abcd' -> 'YWJjZA'
去掉=
怎么解码呢,因为Base64编码后的长度永远是4的整数倍,所以将不是4的整数倍的Base编码自动添加相应数量=
后使其变为4的整数倍后再解码即可
你可能会想,上面我们已经说了可以用decode解码二进制数据,为什么现在还需要对二进制数据编码再显示呢?
原因是:上文针对的是文本文件中的字符,而像jpg
bmp
mp3
等二进制格式文件,其中的二进制数据不能正常解析为字符:
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> b'\xe4\xb8\xad\xe6\x96\x56\x87'.decode('utf-8')
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 3-4: invalid continuation byte
可很多时候我们会在一些文件头加一些与文件属性有关的数据,如在jpg文件头加数据表示该图片的大小、分辨率、色彩等信息,这时我们就需要通过对二进制进行编码读取这些信息了。
struct bytes
与其他数据类型的转换
Bytes之间可以进行加法(无减法操作)组成一个新的bytes
:
>>> m = b'hello '
>>> b = b'world'
>>> m+b
b'hello world'
>>> m+b'world'
b'hello world'
根据前文已知,将字符转换成二进制可以使用encode()
方法,那如果是非字符型数据如整数、浮点数怎么转换成二进制数据呢?
Python提供了一个struct模块来解决bytes
与其他数据类型之间的转换:
>>> struct.pack('>I', 10240099)
b'\x00\x9c@c'
pack
第一个参数是处理指令,'>I'
的意思是:
>
表示字节顺序是big-endian,也就是网络序,I
表示4字节无符号整数,unsigned int
。
后面参数个数要与处理指令一致,大小也要在指定的参数范围内:
>>> struct.pack('>2H', 10245599)
Traceback (most recent call last):
File "<input>", line 1, in <module>
struct.error: pack expected 2 items for packing (got 1)
>>> struct.pack('>2H', 102456565599)
Traceback (most recent call last):
File "<input>", line 1, in <module>
struct.error: pack expected 2 items for packing (got 1)
H
表示整数,两字节无符号整数,usigned short
。
struct
模块定义的数据类型可以参考python的官方文档
相反,unpack
指令就用来将字节流bytes
按给定参数转化为我们想要的格式:
>>> struct.unpack('>I', b'\x00\x9c@c')
(10240099,)
该句指令的意思是:将给定的字节流转换成unsigned int
类型的4字节无符号整数。unpack
同样也可以将字节流转换为字符数据,更换参数即可。
unpack
返回的是tuple类型
应用场景:
有时需要用python处理二进制数据,比如存取文件,socket操作时。这时可以用python的struct
模块来完成,比如可以用struct
处理c语言中的结构体。
比如有一个结构体:
struct Header
{
unsigned short id;
char[4] tag;
unsigned int version;
unsigned int count;
}
通过socket.recv接收到了上面的结构体数据,存在字符串s中,bytes
格式,现在把它解析出来,可以使用unpack
函数:
import struct
id, tag, version, count = struct.unpack('!H4s2I', s)
!
表示网络字节顺序,因为数据是从网络上接收到的,再网络上传送时他是网络字节顺序的。后面的H4s2I
表示1个unsigned int
,4s
表示4字节的字符串,2个unsigned short
。
通过一个unpack
就将id, tag, version, count数据解析好了。
同样,也可以使用pack
再将本地数据pack成struct
格式
ss = struct.pack('>I4s2I', id, tag, version, count)
pack
函数按照指定格式转换成了结构体Header,ss现在是一个字节流,可以通过socket将这个字节流发送出去