0%

Base64 编码

Base64 是网络上最常见的用于传输 8Bit 字节码的编码方式之一,Base64 就是一种基于64个可打印字符来表示二进制数据的方法。

Base64编码由以下64个字符构成: ‘A-Z’(26),‘a-z’(26),‘0-9’(10),’+’(1),’/’(1)

原理

Hello,World! 共12个字符为例,需要经过以下几个步骤完成编码过程:

  1. 将原文字符每3个字节一组进行划分
    划分为 Hel lo, Wor ld! 这四个部分

  2. 将每组3个字节转化为24位二进制

    1
    2
    3
    4
    Hel    010010000110010101101100
    lo, 011011000110111100101100
    Wor 010101110110111101110010
    ld! 011011000110010000100001
  3. 将24位二进制数按6位一组转化成4个十进制数
    之所以按6位一组,是因为6个二进制位正好表示0~63,对应 Base64 字符表的64个字符

    1
    2
    3
    4
    Hel    [18, 6, 21, 44]
    lo, [27, 6, 60, 44]
    Wor [21, 54, 61, 50]
    ld! [27, 6, 16, 33]
  4. 将十进制数按 Base64 字符表编号查找相应字符

    1
    2
    3
    4
    Hel    SGVs
    lo, bG8s
    Wor V29y
    ld! bGQh
  5. 将每组字符按原先字符组成顺序连接为原字符的 Base64 编码
    Hello,World! 的 Base64 编码为 SGVsbG8sV29ybGQh

总的操作过程是:将所有的字符转换为二进制,然后每6个二进制位一组,不足6位的低位补充0,每6个二进制位转换为对应的十进制数查ascii表得到对应的ascii。

代码实现

根据基本原理,使用 Python 代码实现基础功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def base(string:str)->str:
oldstr = ''
newstr = []
base = ''
base64_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P','Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '+', '/']
#把原始字符串转换为二进制,用bin转换后是0b开头的,所以把0b替换了,首位补0补齐8位
for i in string:
oldstr += '{:08}'.format(int(str(bin(ord(i))).replace('0b', '')))
#把转换好的二进制按照6位一组分好,最后一组不足6位的后面补0
for j in range(0, len(oldstr), 6):
newstr.append('{:<06}'.format(oldstr[j:j + 6]))
#在base_list中找到对应的字符,拼接
for l in range(len(newstr)):
base += base64_list[int(newstr[l], 2)]
#判断base字符结尾补几个‘=’
if len(string) % 3 == 1:
base += '=='
elif len(string) % 3 == 2:
base += '='
return base

代码解释

进制转换内置函数只能是数字作为对象,或者其他进制表示的数字

  • bin() 用于将各进制数转换为二进制表示,开头以0b表示,如 0b1001000
  • oct() 用于将各进制数转换为八进制表示,开头以0o表示,如 0o110
  • hex() 用于将各进制数转换为十六进制表示,开头以0x表示,如 0x48
  • int() 用于将各进制数转换为十进制表示,如 72

int(x, base=10) 可以接受各进制数,以及字符串形式的数字转换为对应的整型

1
2
3
将二进制'0b1001000'转换为对应的十进制表示
int(0b1001000) 输出 72
或者使用 int('1001000', 2) 注意,此时需要使用字符串形式,且必须指明该数字之前的类型,2表示之前是二进制,否则默认的base是10,为十进制,则输出的结果是 1001000

ord()用于获得字符对应的ASCII码,chr()相反,用于获得ASCII码对应的字符。
所以要获得字符的二进制码,需要使用先使用ord()函数得到字符对应的ascii码,然后使用bin()得到对应的二进制。

如果最后剩下两个输入数据,在编码结果后加1个“=”;如果最后剩下一个输入数据,编码结果后加2个“=”;如果没有剩下任何数据,就什么都不要加,这样才可以保证资料还原的正确性

注意:以上代码只是原理的简单实现,只适用于ASCII的码,不能用于汉字

Python内置模块

Python 内置的 base64模块 可以直接进行base64的编解码。

1
2
3
4
5
6
7
8
9
import base64

# 需要注意的是,b64encode()方法接收一个字节类对象,所以需要使用 encode() 或者 b'' 的形式
print(base64.b64encode("Hello,World!".encode('utf-8'))) # b'SGVsbG8sV29ybGQh'

# 解码 Base64 编码过的 bytes-like object 或 ASCII 字符串 并返回解码过的 bytes。
print(base64.b64decode(b'SGVsbG8sV29ybGQh')) # b'Hello,World!'
# 需要解码后返回str,可以使用 decode('utf-8')
print(base64.b64decode(b'SGVsbG8sV29ybGQh').decode('utf-8')) # Hello,World!

由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,所以又有一种"url safe"的base64编码,其实就是把字符+和/分别变成-和_

1
2
3
4
import base64

print(base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')) # b'abcd++//'
print(base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')) # b'abcd--__'

编码与转码

常用编码说明:
ASCII码:只有一个字节,且开始的时候连最高位都没有用,为0,只用了后7位,共127个来存储英文。
GB2312:为了能使用两个字节表示一个汉字,所以使用两个字节来存储一个字符。
Unicode:是编码标准,规定了使用两个字节来表示一个字符,足以涵盖世界上所有的语言,是计算机内数据存储的编码方式
UTF-8:诞生是因为Unicode编码标准规定所有的都是用两个字节,但是英文只要一个字节存储足矣,所以为了节省空间,提出了utf-8, 是一种针对Unicode的可变长度字符编码,它可以使用1~4个字节表示一个符号(用一个字节表示英文,三个字节表示汉字),根据不同的符号改变字节长度,当字符在ASCII码的范围时,就用一个字节表示,所以是兼容ASCII编码的。这样显著的好处是,虽然在我们内存中的数据都是unicode,但当数据要保存到磁盘或者用于网络传输时,直接使用unicode就远不如utf8省空间,这也是为什么utf8是我们的推荐编码方式。

Unicode与utf8的关系:
一言以蔽之:Unicode是内存编码表示方案(是规范),而UTF是如何保存和传输Unicode的方案(是实现)这也是UTF与Unicode的区别。
编码转换图
解释:
可见在计算机内部的编码是unicode标准存储的,即一个字符两个字节。
通过 encode 使用utf-8编码后,一个汉字占3个字节
通过 encode 使用gbk编码后,一个汉字占2个字节
想要获得unicode,需要使用对应的编码 decode 进行还原。
所以不同的编码规则之间是不可以直接进行转换的,否则会因为存储字节数的不同(有的即使字节数一样,使用的编码表也是不同的),导致乱码,即使不是乱码,也和原本要存储的不一致。所以若要将utf-8的数据要变成gbk类型的数据,需要先 decode(utf-8) 还原成unicode标准,然后再 encode(gbk) 变成gbk类型。

Python3 编码处理

Python3中字符串就是unicode编码,而计算机内存中处理的时候也是使用的unicode编码。
这里需要明确一点就是:计算机在内存中处理信息时使用的是unicode编码,但是存储的时候会转换成某种编码方式编码的bytes字节串进行存储,不同的系统、不同的软件使用的编码格式可能不同,

因为utf-8为可变长编码,占用的空间要少于unicode,所以在网络传输,硬盘存储的时候都是将unicode编码变成utf-8编码在去处理,主要是英文只占了一个字节,而英文是网络传输和存储中被大量使用的字符。

如果一个我们读取一个文件,改文件保存时的编码是比如gbk,则我们在读取的时候,就应该先 encoding = ‘gbk’ (只用encoding,此时的encoding和encode()不是同一个意思,encode指定的进行编码这个动作的格式,而encoding是指定文本的使用的编码方式) 或是 文本.decode(“gbk”) 解码为unicode在进行操作。
再如网络传输的时候,都要先使用str.encode(“utf-8”) 将unicode转变为utf-8编码进行传输,在接受到数据的时候再使用 数据.decode(“utf-8”) 将str解码为 unicode处理

Python3中str类型存unicode数据(两个字节),bytse类型存bytes数据(字节型)

通过在字符串之前使用 b’字符串’ 的方式,可以声明字节类型,但是这种方式后面的字符串只能是ASCII码,此外可以使用 bytes(‘字符串’,encoding = ‘编码方式’) 得到相应编码的二进制数据。
使用 str(‘二进制编码’, encoding = “解码方式”) 可以还原到原始的str类型数据(使用的unicode两个字节存储的)

1
2
3
4
5
6
7
name = '小明'
name = bytes(name, encoding='utf-8')
# name = name.encode('utf-8') # b'\xe5\xb0\x8f\xe6\x98\x8e'
print(name) # b'\xe5\xb0\x8f\xe6\x98\x8e'

name = str(b'\xe5\xb0\x8f\xe6\x98\x8e', encoding='utf-8')
print(name) # 小明

这是一对互逆操作。

Python3的解释器的默认解码方式是utf-8,而Python2的解释器使用的默认解码方式是ascii,所以要在开头声明 #coding:utf8,用于指定python2解释器读取python文件使用的解码方式(此时需要python文件的编码格式是utf8,否则需换成相应的格式,如 #coding:gbk, 但是目前使用的都是utf8)。
这里要注意区分,Python3的解释器的默认解码方式是utf-8,指的是以utf-8去解释Python文件,读取其中的内容,所以Python文件在创建的时候使用utf-8编码(一般应用创建的文件多数都采用的utf-8编码,磁盘存取时会自动完成utf-8和unicode的转换)。
而Python3中str类型存unicode数据,是指在内存的操作(通过 .encode('unicode_escape') 的编码方式,发现是使用的2个字节的unicode存储方式),两者不是一个层面的意思。

在Python3中定义str类型,既可以使用直接的表述(a =’你好’),也可以使用它对应的unicode编码,因为str在内存中以unicode表示。a = b'\xe4\xbd\xa0\xe5\xa5\xbd' 两者是等价的,因为本质上都是同一个二进制数据的不同的表现形式,一个是string类型,一个是它使用utf-8编码格式对应的字节类型。

所以说unicode表示的内容是不变的,然后通过 .encode(编码格式) 编码得到相应编码格式的二进制字节型数据,因为各种编码格式不同(占用的字节不同),所以编码后续使用原来的编码格式进行解码才能得到源数据,不能混用,会出现乱码。

所以说:当我们有需求说要将一个str装换为字节型数据的时候,实际就只要 encod("一种编码格式") ,由于使用的编码格式不同,我们转换的字节型数据就有多种,这是没有问题的,而我们之所以会一般选择utf-8来进行编码,因为占用的资源较少

1
2
3
4
5
6
7
8
9
10
11
12
13
'''
编码解码的过程中必须显示的指定编码格式
bytes()方法是一定要指定encoding参数的,但是str()在转换时候不一定
'''
be = b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(type(be)) # <class 'bytes'>
st = str(be)
print(st) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(type(st)) # <class 'str'>
st1 = str(be, encoding='utf-8')
print(type(st1)) # <class 'str'>
print(st1) # 你好
print(be.decode('utf-8')) # 你好

文本文件,都是使用unicode存在内存中的,当涉及到音频、视频、图像文件的时候,都是二进制流,所以open操作这些类型的文件的时候要加b。

乱码问题

创建一个urf-8编码的文件test.txt,内容是hello,王小明,使用下面的程序,分别在win和非win系统下运行:

1
2
with open('./test.txt') as f:
print(f.read())

在win系统下,打印的内容为:hello锛岀帇灏忔槑
在非win系统下,打印的内容为:hello,王小明, 可以正常打印。

这是因为你的win的操作系统安装时是默认的gbk编码,而linux操作系统默认的是utf8编码。当执行open函数时,调用的是操作系统打开文件,操作系统用默认的gbk编码去解码utf8的文件,自然乱码,此时只要添加encoding='utf-8'参数,指定使用的编码方式为utf8即可。如果你的文件保存的是gbk编码,在win 下就不用指定encoding了。另外,如果你的win上不需要指定给操作系统encoding=‘utf8’,那就是你安装时就是默认的utf8编码或者已经通过命令修改成了utf8编码。
所以,为了代码的兼容性,推荐还是需要设置 encoding 参数的。

编码的本质: 使用不同的字符表获得相应的字符,底层存数据的时候都是二进制,只是各种编码方式的字节不同,也就是断句不同,所以要以相应的编码处理。

总结

Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
除了使用标准的 Base64 字母表,还可以自己定义64个字符的排列顺序,这样就可以自定义 Base64 编码,不过,通常情况下完全没有必要。

Base64 是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
Base64 适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
Base64是一种任意二进制到文本字符串的编码方法,常用于在 URL、Cookie、网页中传输少量二进制数据。

参考:官方文档:https://docs.python.org/zh-cn/3/library/base64.html
廖雪峰:https://www.liaoxuefeng.com/wiki/897692888725344/949441536192576
python编码:https://www.cnblogs.com/yuanchenqi/articles/5956943.html