Base64 编码算法 - Python实现
算法描述
Base64编码的目标:将任意字节转为可读字符的编码。
Base64 不是为了安全,而是为了显示。因为计算机世界最开始的时候,只支持 26 个字母和一些符号,所以 1个 字节就足够编码了,但后来,计算机世界的不止多了中文、日文等文字,更多了视频、图片、程序等一样以字节为单位的数据,这些字节大多数不仅仅是 2^7 以内的可显示的文字字符编码,还有可能是大于127(有符号数小于0)的字节,这些字节没办法用字符显示出来,Base64就是将他们显示出来的算法。
编码规则
Base64编码的思想: 采用64个基本的ASCII码字符对数据进行重新编码。
步骤如下
- 首先,我们得准备一个密文表,一般选这个
- ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
- 一共有 64 的字符,这些字符称为 Base 字符,基本的 ASCII 码字符,这也是 Base64 名称的由来
- 64 个也就是 2^6 个字符,也就是说我们需要 6 位二进制来显示
- 接下来将需要编码的数据拆分成字节数组,以三个字节为一组,每个字节
8
位,共24
位。当所要编码的数据的字节数不是3
的整数倍,也就是说在分组时最后一组不足3个字节。这时在最后一组填充1
到2
个0
字节。并在最后编码完成后在结尾添加1
到2
个‘=’
(可回忆一下之前碰到的 base64 编码,确实是不少在最后有=
) - 按顺序排列这
24
位数据 - 把这
24
位数据分成四组,即每组6
位 - 在每组的最高位前补两个
0
凑足一个字节。这样就把一个3
字节为一组的数据重新编码成了4
个字节 - 并且每个字节的值都是 0-63 以内,对应我们的 密文表 的字符位置,编码成功
示例:Hello
Hello
包含 5 个字节,取出对应的 ASCII 编码- 16进制 H (0x48) e (0x64) l (0x6c) l (0x6c) o(0x6f)
- 二进制 H (0100 1000) e (0110 0101) l (0110 1100) l(0110 1100) o(0110 1111)
- 三个字节为 1 组,加一个 0 补为两组:
Hel
和lo0
,这里补了一个0
,所以,编完码后需要在编码后面加一个=
- 分为两组后,按顺序排列这两组的编码
- 第1组:01001000 01100101 01101100
- 第2组:01101100 01101111 00000000
- 将这两组的 24 位数据各自按 6 位一组,分为 4 组
- 第1组:010010 000110 010101 101100
- 第2组:011011 000110 111100 000000
- 给每组的最高位补两个 0,就完成了 3 个字节重新编码为 4 个字节
- 第1组:00010010 00000110 00010101 00101100
- 第2组:00011011 00000110 00111100 00000000
- 找出每组的二进制对应的密文表的字符
- 第1组:00010010 -> 18 -> S, 00000110 -> 6 -> G, 00010101 -> 21 -> V, 00101100 -> 44 -> s
- SGVs
- 第2组:00011011 -> 27 -> b, 00000110 -> 6 -> G, 00111100 -> 60 -> 8, 00000000 -> 0 -> A
- bG8A
- 第1组:00010010 -> 18 -> S, 00000110 -> 6 -> G, 00010101 -> 21 -> V, 00101100 -> 44 -> s
- 最终编码为:
SGVsbG8A=
解码过程
以上就是编码过程,解码过程,就是倒过来,先去掉末尾的 =
,将剩余的字符按照 4 个字符一组进行分组,然后每组的每个字符去掉最高位的 2 个 0
,重新分为 3
个字符一组,在根据 =
的个数,去掉最后一组的 0-2
个 0
字符,完成解码(详细过程就不赘述了)。
可以使用系统指令base64
进行验证,命令如下:
# encode
printf "Hello" | base64 -i -
# decode
printf "SGVsbG8A=" | base64 -D -
此处不能使用echo
,因为echo
会在末尾添加换行符。
Python实现
接下来,我们使用 python 实现两个函数,encode
和 decode
,完成 base64 的编解码。
CHARSET = "ASCII"
BASE_CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
CHAR_PRE_GROUP = 3
def encode(msg):
# 转为二进制
b_msg = bytes(msg, CHARSET)
# print("字节数:", len(b_msg))
# 计算需要添加的 0 的个数
zero_cnt = CHAR_PRE_GROUP - len(b_msg) % CHAR_PRE_GROUP
if zero_cnt == CHAR_PRE_GROUP:
zero_cnt = 0
# print("需要添加的 0 的个数:", zero_cnt)
msg += str(chr(0))*zero_cnt
# print("Msg with zero:", msg)
# 再一次转化为二进制
b_msg = bytes(msg, CHARSET)
# 三个一组,分组进行处理
encoded = ""
for i in range(0, len(b_msg), 3):
i_msg =[int(i) for i in b_msg[i:i+3]]
# print([bin(i) for i in i_msg])
# 按照算法,将 3 字节变成 4 字节,位运算会比较绕
new_msg = [None] * 4
new_msg[0] = i_msg[0] >> 2
new_msg[1] = (((i_msg[0] & 0b00000011) << 6) | (i_msg[1] >> 2)) >> 2
new_msg[2] = (((i_msg[1] & 0b00001111) << 4) | (i_msg[2] >> 4)) >> 2
new_msg[3] = i_msg[2] & 0b00111111
encoded += "".join([BASE_CHAR[i] for i in new_msg])
return encoded + '=' * zero_cnt
def decode(msg):
zero_cnt = msg.count('=')
# print("zero cnt:", zero_cnt)
# remove =
msg_without_zero = msg.replace('=', '')
# 将字符转化为对应的索引值
i_msg = [BASE_CHAR.index(i) for i in msg_without_zero]
# 转化为 二进制
decoded = ""
for i in range(0, len(i_msg), 4):
i_group_msg =[int(i) for i in i_msg[i:i+4]]
# print([bin(i) for i in i_group_msg])
# 将四字节转化为三字节
original_msg = [None] * 3
original_msg[0] = (i_group_msg[0] << 2) | ((i_group_msg[1] & 0b00110000) >> 4)
original_msg[1]= ((i_group_msg[1] & 0b00001111) << 4) | ((i_group_msg[2] & 0b00111100) >> 2)
original_msg[2]= ((i_group_msg[2] & 0b00000011) << 6) | i_group_msg[3]
# print(original_msg)
decoded += "".join([chr(i) for i in original_msg if i])
# print(len(decoded))
# print(decoded)
return decoded
if __name__ == '__main__':
def test(msg):
emsg = encode(msg)
print("Encoded", emsg)
dmesg = decode(emsg)
print("Decoded", dmesg)
test("Hello")
test("Hello1")
test("Hello World")
以上就是一个简单的 Base64 编解码算法。
中文如何进行 Base64 编码
需要提醒的是,用上述代码,是无法对中文进行编码和解码的。
如果要对中文进行编解码,我们首先要对原始编码做一定的了解。
中文有多种编码方式:GBK、UTF-8等等,每一种编码的Base64对应值都不一样。比如 UTF-8 用三个字节对中文编码,所以代码里要对此进行优化,所以一个汉字就是一组了,最终会用 4个 Base 字符对其进行展示。
如:
printf "中文" | base64 -i -
结果为:5Lit5paH