我们知道在计算机中任何数据都是按字节存储的,有的复杂的数据(比如汉字)可能由多个字节表示,简单(比如单个英文字符)的由 1 个字节表示,字节是内存中基本的存储单位。
我们知道一个字节可表示的范围是 0 ~ 255(十六进制:0x00 ~ 0xFF),其中 ascii 值的范围为 0 ~ 127(十六进制:0x00 ~ 0x7F);而超越 ascii 范围的 128~255(十六进制:0x80 ~ 0xFF)之间的值是不可见字符。当然也并不是所有的 ascii 都是可见的,ascii 中只有 95 个可见字符(范围为 32 ~ 126),其余的 ascii 为不可见字符,本节后面我们会列出所有可见的 ascii 和 不可见的 ascii。
当不可见字节流在网络上传输时,比如说从 A 计算机传到 B 计算机,往往要经过多个路由设备,由于不同的设备(特指老的路由设备)对字节流的处理方式有一些不同,这样那些不可见字节就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个 base64 编码,统统变成可见字节,也就是 ascii 码可表示的可见字符,确保数据可靠传输。base64 的内容是有 0 ~9,a ~z,A ~Z,+,/组成,正好 64 个字符,这些字符是在 ascii 可表示的范围内,属于 95 个可见字符的一部分。
对于现在的路由设备,只要是文本字符(无论是否是可见字符)都可以转为字节流(字节类型数据)在网络上传输。而字节类型的数据(图片,音频,视频,语音等二进制数据), 本身就是字节流,所以可以直接在网络上传输。但是,在企业开发中,我们为了一致性约定,往往对要发送的数据先进行序列化,然后再编码(encoding)为字节流在网络上发送,而对方收到数据后,要先把字节流解码(decoding)为字符串,然后再反序列化还原为原始数据。
我们往往使用 json 对要发送的数据进行序列化,因为 json 有多语言支持,而 pickle 只有 Python 语言本身支持,我们知道通信的另一方不一定也是使用 Python 语言,所以,我们对网络传输的数据一般不使用 pickle 进行序列化。然而,json 无法序列化字节类型的数据,那我们如果想发送字节类型的数据(图片,音频,视频,语音等二进制数据),应该怎么办呢?
有些同学可能会想,我们可以把字节类型数据转为字符串类型数据,然后再使用 json 序列化,然而不幸的是,对于字节类型的图片,音频,视频,语音等二进制数据里面含有很多不可见字节,而这些不可见字节无法转为字符串。但是,我们可以先用 base64 把这些不可见字节数据编码为可见字节数据,然后再转为字符串类型,然后在通过 json 进行序列化,然后再编码为字节流通过网络发送出去。而对方收到数据后,要先把字节流解码为字符串,然后再反序列化,然后再转为字节类型数据,然后再使用 base64 解码还原成原始数据。
最后,大家可能要问,字节类型的数据(图片,音频,视频,语音等二进制数据)本身就可以直接通过网络传输,为什么还要序列化?在此,我再告诉大家,你项目中要传输的数据不只有这些二进制吧,应该还需要包容其它类型的数据(int,list,dict等等),而这些结构化类型数据,必须要序列化后,才能通过网络传输。通信双方约定的就是发送的东西都是序列化后数据,这样方便对通信双方的数据类型进行还原。你没必要只对其它类型的数据序列化,对字节类型数据不序列化,这样反而增加了业务逻辑。
base64 是 Python 内建模块,我们只需要 import 导入模块就可以使用,我们可以把超越 ascii 范围的字符用 base64 编码为可见字符,注意 base64只能编码字节类型数据。
import base64 mydata = b"\x80" # \x80 换成十进制为 128,超越了 ascii 范围,为不可见字节 datab64 = base64.b64encode(mydata) # 编码为 base64 的可见字节 print(datab64) mydata = base64.b64decode(datab64) print(mydata) # 解码为不可见字节 \x80
base64 自己有一套算法可以把我们的字节编码成 ascii 范围内的字节,当然它不关心我们给的字节是不是可见字节,如果是可见字节同样要编码,如果是不可见字节则会编码成可见字节。
import base64 dataone = "p".encode("utf8") # 可见字节 datatwo = "鸟".encode("utf8") # 不可见字节 print(base64.b64encode(dataone)) # 编码可见字节为可见字节 print(base64.b64encode(datatwo)) # 编码不可见字节为可见字节
base64 编码会把 3 个字节的字节类型数据编码为 4 个可见字节的字节类型数据,如果要编码的字节类型数据不是 3 的倍数,最后会剩下 1 个或 2 个字节,base64 会在编码的末尾加上 1 个或 2 个 = 号,表示补了多少字节,解码的时候,会自动去掉。
import base64 strone = "p".encode("utf8") # 1个字节 strtwo = "py".encode("utf8") # 2 个字节 strthree = "pyt".encode("utf8") # 3 个字节 strfour = "pyth".encode("utf8") # 4 个字节 strfive = "pytho".encode("utf8") # 5 个字节 strsix = "python".encode("utf8") # 6 个字节 strseven = "老鸟".encode("utf8") # utf-8 编码,每个汉字一般占 3 个字节 streight = "老鸟python".encode("utf8") # utf-8 编码,每个汉字一般占 3 个字节,英文占 1 个字节 print(base64.b64encode(strone)) # 按 3 个字节(缺 2 个,补齐 2 个 =)编码成 4 个字节 cA== print(base64.b64encode(strtwo)) # 按 3 个字节(缺 1 个,补齐 1 个 =)编码成 4 个字节 cHk= print(base64.b64encode(strthree)) # 按 3 个字节编码成 4 个字节 cHl0 print(base64.b64encode(strfour)) # 按 6 个字节(缺 2 个,补齐 2 个 =)编码成 8 个字节 cHl0aA== print(base64.b64encode(strfive)) # 按 6 个字节(缺 1 个,补齐 1 个 =)编码成 8 个字节 cHl0aG8= print(base64.b64encode(strsix)) # 按 6 个字节编码成 8 个字节 cHl0aG9u print(base64.b64encode(strseven)) # 按 6 个字节编码成 8 个字节 6ICB6bif print(base64.b64encode(streight)) # 按 12 个字节编码成 16 个字节 6ICB6bifcHl0aG9u
我们可以对用 base64 编码后的字节进行解码。
import base64 dataone = "python".encode("utf8") # 可见字节 datatwo = "老鸟".encode("utf8") # 不可见字节 dataone = base64.b64encode(dataone) # 编码可见字节 "python" datatwo = base64.b64encode(datatwo) # 编码不可见字节 "老鸟" 为可见字节 print(base64.b64decode(dataone)) # 解码 base64 编码后的字节 print(base64.b64decode(datatwo)) # 解码 base64 编码后的字节
由于 base64 编码后可能出现字符 + 和 /,在网页上传输数据,我们用 get 方式传输字符串时,字符 + 和 / 在 URL 中有特殊用途,就不能直接作为参数,所以又有一种 "url safe" 的 base64 编码,其实就是把字符 + 和 / 分别变成 - 和 _。
import base64 mydata = b"\xfb\xef\xff" print(base64.b64encode(mydata)) # 编码为 ++//,但是 + 和 / 在 url 中有特殊用途,不能作为参数 print(base64.urlsafe_b64encode(mydata)) # 编码为 --__,很好。
在互联网上传输图片,音乐,视频,语音等等这些二进制数据是常用的需求, 本节开头部分,我们说过,最好要对所有发送的数据进行序列化后再进行网络传输。所以,正规的流程是:先用 base64 把这些不可见字节数据编码为可见字节数据,然后再转为字符串类型,然后在通过 json 进行序列化,然后再编码为字节流通过网络发送出去。而对方收到数据后,要先把字节流解码为字符串,然后再反序列化,然后再转为字节类型数据,然后再使用 base64 解码还原成原始数据。
import base64 import json f = open("d:/test.png", "rb") # 确保你计算机中存在 d:/test.png imgdata = f.read() # 二进制图片数据 f.close() imgdatab64 = base64.b64encode(imgdata) # 编码为可见字节 imgdatab64str = imgdatab64.decode("utf8") # 转为字符串 jsondata = json.dumps(imgdatab64str) # json 序列化 bytesdata = jsondata.encode("utf8") # 编码为字节类型在网络上传输 ''' 通过网络传输......, 对方机器收到后,先反序列化,最后用 base64 进行解码 ''' bytesdata_recv = bytesdata # 收到网络上发来的字节流 jsondata_recv = bytesdata_recv.decode("utf8") # 解码为字符串 strdata_recv = json.loads(jsondata_recv) # json 反序列化 bytesdata_recv = strdata_recv.encode("utf8") # 转为字节类型数据 imgdata = base64.b64decode(bytesdata_recv) # 解码为原始数据 # 把从网络上收到的图片数据写入磁盘,文件名字为 copy.png f = open("d:/copy.png", "wb") imgdata = f.write(imgdatab64) f.close()
注意:base64 仅仅是把字节类型数据按照一定的算法转化为属于 ascii 范围内的可见字节类型数据,但不要作为加密行为。
我们知道一个字节的大小超越 ascii 编码的都是不可见字符,但 ascii 编码也不是所有字符都是可见的,在 128 个 ascii 中,只有 95 个可见字符,下表为 ascii 中可见字符表。
|
|
|
下表为 ascii 不可见字符表。
二进制 | 十进制 | 十六进制 | 缩写 | 可以显示的表示法 | 名称/意义 |
---|---|---|---|---|---|
0000 0000 | 0 | 00 | NUL | ␀ | 空字符(Null) |
0000 0001 | 1 | 01 | SOH | ␁ | 标题开始 |
0000 0010 | 2 | 02 | STX | ␂ | 本文开始 |
0000 0011 | 3 | 03 | ETX | ␃ | 本文结束 |
0000 0100 | 4 | 04 | EOT | ␄ | 传输结束 |
0000 0101 | 5 | 05 | ENQ | ␅ | 请求 |
0000 0110 | 6 | 06 | ACK | ␆ | 确认回应 |
0000 0111 | 7 | 07 | BEL | ␇ | 响铃 |
0000 1000 | 8 | 08 | BS | ␈ | 退格 |
0000 1001 | 9 | 09 | HT | ␉ | 水平定位符号 |
0000 1010 | 10 | 0A | LF | ␊ | 换行键 |
0000 1011 | 11 | 0B | VT | ␋ | 垂直定位符号 |
0000 1100 | 12 | 0C | FF | ␌ | 换页键 |
0000 1101 | 13 | 0D | CR | ␍ | 归位键 |
0000 1110 | 14 | 0E | SO | ␎ | 取消变换(Shift out) |
0000 1111 | 15 | 0F | SI | ␏ | 启用变换(Shift in) |
0001 0000 | 16 | 10 | DLE | ␐ | 跳出数据通讯 |
0001 0001 | 17 | 11 | DC1 | ␑ | 设备控制一(XON 启用软件速度控制) |
0001 0010 | 18 | 12 | DC2 | ␒ | 设备控制二 |
0001 0011 | 19 | 13 | DC3 | ␓ | 设备控制三(XOFF 停用软件速度控制) |
0001 0100 | 20 | 14 | DC4 | ␔ | 设备控制四 |
0001 0101 | 21 | 15 | NAK | ␕ | 确认失败回应 |
0001 0110 | 22 | 16 | SYN | ␖ | 同步用暂停 |
0001 0111 | 23 | 17 | ETB | ␗ | 区块传输结束 |
0001 1000 | 24 | 18 | CAN | ␘ | 取消 |
0001 1001 | 25 | 19 | EM | ␙ | 连接介质中断 |
0001 1010 | 26 | 1A | SUB | ␚ | 替换 |
0001 1011 | 27 | 1B | ESC | ␛ | 跳出 |
0001 1100 | 28 | 1C | FS | ␜ | 文件分割符 |
0001 1101 | 29 | 1D | GS | ␝ | 组群分隔符 |
0001 1110 | 30 | 1E | RS | ␞ | 记录分隔符 |
0001 1111 | 31 | 1F | US | ␟ | 单元分隔符 |
0111 1111 | 127 | 7F | DEL | ␡ | 删除 |
会使用 base64 进行编解码。
清楚 base64 使用的场景。
我们在 IO 编程中知道,因为图片数据有大量不可见字节,如果要把图片数据保存到磁盘上,需要用 "wb" 的方式写文件。如果我们把图片数据用 base64 编码成可见字节,然后在转为字符串,最后用 "w" 的形式存储到磁盘上。读数据的时候,先把读出的字符串转为字节类型数据,然后用 bases64 解码还原,这样也是可以的,请编码完成这个试验。
一个字节的内容如果不能用 ascii 编码表示的话,那就是不可见字符