学习笔记:用python实现手工编写base64编码和解码,完整代码(支持中文编码)
1、base64简介
base64最初产生的原因:我们知道在计算机中的一个字符(一个字节)共有256种组合,对应就是ascii码,而ascii码的128~255之间的值是不可见字符。当在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以一些牛人就想到一个方法,先把数据(字符串)做一个Base64编码,统统变成可见字符再传输,这样出错的可能性就大大降低了。
2、尝试base64编码
网上有太多base64的原理文章,我这里不再墨迹,咱们在这里直接尝试写一个泉中流版的base64编码和解码器,因水平有限,肯定难免有错,欢迎大家批评指正,说好了不墨迹,好吧开干:
首先我们举个base64编码的例子,简单点,就对仨尖“AAA”编码:
编码过程如下:
1、AAA # 把预编码的字符串AAA先写成下面的二进制编码
2、01000001 01000001 01000001 # 每三个字节一组,字符个数不够三个就在最后补上一个或两个全“0”的字节
3、010000 010100 000101 000001 # 将24位拆分为6位一组,组成四组!
4、00010000 00010100 00010100 00000001 # 每组前面都补上两个"0",组成新的四个字节!
5、16 20 20 1 # 把四个字节分别转换为10进制的数字
6、Q U F B # 最后到下面的标准base64编码表中找到每个“数字”对应序号的字符,不够四个字符则用“=”补齐!
标准base64编码表如下:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
与上面对应的序号为:
012345 …… …… …… 61 62 63
好,既然这样,我们就开始写代码:
myStr="hello base64!"
codeBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
modNum=len(myStr)%3 # 对字符串进行三个一组分组,多余的个数为modNum
base64Str=""
numList=[ord(x) for x in myStr] # 对每个字符求ascii码值
for i in range(0,int(len(numList)),3):
if int(len(numList))>i+2:
str1=codeBase64[(numList[i]>>2)]
str2=codeBase64[(((numList[i]&0b11)<<4)+((numList[i+1]&0b11110000)>>4))]
str3=codeBase64[(((numList[i+1]&0b1111)<<2)+((numList[i+2]&0b11110000)>>6))]
str4=codeBase64[(numList[i+2]&0b00111111)]
base64Str+=str1+str2+str3+str4
elif modNum==1: # 三个一组,剩下1个落单的字符,需要补两个等号
str1=codeBase64[(numList[i]>>2)]
str2=codeBase64[((numList[i]&0b11)<<4)]
str3="="
str4="="
base64Str+=str1+str2+str3+str4
elif modNum==2: # 三个一组,剩下2个落单的字符,需要补一个等号
str1=codeBase64[(numList[i]>>2)]
str2=codeBase64[(((numList[i]&0b11)<<4)+((numList[i+1]&0b11110000)>>4))]
str3=codeBase64[(((numList[i+1]&0b1111)<<2))]
str4="="
base64Str+=str1+str2+str3+str4
print("字符串:'"+myStr+"'")
print("base64编码得:'"+base64Str+"'\n")
运行结果如下:
字符串:‘hello base64!’
base64编码得:‘aGVsbG8gYmFzZTY0IQ==’
上面将三个字节拆分组合为四个字节的方法解释如下:
1、先把字符串分组为三个一组,然后每组第一个字符取值并右移两位str1=codeBase64[(numList[i]>>2)],相当于把这个字符的前六位二进制取出,前面加上两个零作为一个数值,就重新生成了第一个字节。
2、再把第一个字符的二进制数值与0b11做与运算去掉前6位,然后左移四位与第二个字符的数值去掉前四位并右移四位进行相加, str2=codeBase64[(((numList[i]&0b11)<<4)+((numList[i+1]&0b11110000)>>4))],再到标准base64编码表中找对应数值序号的字符。
例如:第一个字符ord(“A”)再转二进制0b01000001,与运算0b01000001&0b11=0b01去掉前6位得到第一个字符的后两位,移位0b01<<4=0b010000得“0b010000”。第二个字符的二进制为0b01000001,与运算0b01000001&0b11110000=0b01000000去掉前4位得到第二个字符的后四位,右移四位0b01000000>>4=0b00000100得到“0b00000100”,最后把前面得到的“0b010000”与“0b00000100”相加,就得到了第一个字符的后两位与第二个字符的前四位的“组合二进制”,就生成了第二个字节。
3、第三个字节与第二个字节的生成方法类似,只是取第二个字符对应二进制数值的后四位与第三个字符二进制的前两位组成新的字节。
4、第四个字节很简单,只需要用与操作去掉第三个字符二进制中的前两位,留下后六位即可!
四个字节的数值产生后,直接到base64标准编码表中按数值序号找到对应字符即可。
最后补充一下,因为字符总个数不可能总是那么凑巧为三的倍数,那么会出现两种情况,就是剩下一个落单的字符或者剩下两个落单的字符,这时,我们就用等号补全即可!
3、中文等字符无法编码问题的解决
程序逻辑貌似很成功,运行结果也正常,但是我发现一旦对中文等字符进行编码则出错如下:
IndexError: string index out of range
这是因为中文等字符在用ord转换为数值后,会大于255,那么在第4步中对这个字符进行求值得到的数字就肯定大于255,之后在第5步中右移后仍然会大于63,所以超出了base64的编码的最大取值范围!比如ord(“泉”)=27849 右移两位仍然为6962,大于63,当然会出现越界错误。看来这种方法不能用于非ascii编码。
好吧,赶紧去学习了一下utf-8编码,发现我可以将中文等字符先转换为utf-8编码,而这些编码都小于256,这样我再对每个字节的utf-8字节进行base64编码即可,按照这个思路我对上面的程序进行了如下修补。
1、首先我写了个中文字符转为utf-8的函数,代码如下:
# 对非ascii码值进行utf-8编码:
# 遍历列表,码值大于0x80则是非ascii码
# 根据0x80,0x800,0x10000对非ascii码值进行2字节,3字节,4字节utf-8编码,具体如下:
# 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
# 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
# 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
def uncodeUTF8(mylist): # 码值列表
lenMyList=len(mylist)
i=0
myUniStr=[] # 编码后列表
while i<lenMyList: # 数据没有了,跳出
if ord(mylist[i])>=0x10000: # 4字节 0b11110xxx
myUniStr.append(0b11110000+(ord(mylist[i])>>18))
myUniStr.append(0b10000000+((ord(mylist[i])>>12)&0b111111))
myUniStr.append(0b10000000+((ord(mylist[i])>>6)&0b111111))
myUniStr.append(0b10000000+(ord(mylist[i])&0b111111))
elif ord(mylist[i])>=0x800: # 3字节 0b1110xxxx
myUniStr.append(0b11100000+(ord(mylist[i])>>12))
myUniStr.append(0b10000000+((ord(mylist[i])>>6)&0b111111))
myUniStr.append(0b10000000+(ord(mylist[i])&0b111111))
elif ord(mylist[i])>=0x80: # 2字节 0b110xxxxx
myUniStr.append(0b11000000+(ord(mylist[i])>>6))
myUniStr.append(0b10000000+(ord(mylist[i])&0b111111))
else: # ascii码 0b0xxxxxxx,小于等于127,直接用chr()函数返回字符即可!
myUniStr.append(ord(mylist[i]))
i+=1
return myUniStr
2、然后,将最上面代码中的numList=[ord(x) for x in myStr] 这一句代码直接替换为:
numList=uncodeUTF8(myStr)即可!
修补后对中文等编码的运行结果如下:
字符串:‘hello 泉中流!’
base64编码得:‘aGVsbG8g5rOJ5Lit5rWBIQ==’
这样就成功的实现了泉中流版的base64编码。
4、base64解码
下面的问题就是base64解码了,解码过程是一个反过程,我只给出代码,因为里面的注解足够详细了(当然,解码过程也涉及到utf-8的解码问题,所以我又写了个utf-8解码函数)。
# ****** 解码过程如下:(例如对QUFB进行base64解码)*********
# Q U F B # 四个字符一组,到标准base64编码表中找到以上字符对应的“序号”,出现“=”则是编码结尾!
# 00010000 00010100 00000101 00000001 # 去掉前面的两个"0",组成24位
# 010000 010100 000101 000001 # 将24位拆分为8位一组,组成三个字节!
# 01000001 01000001 01000001 # 得到utf-8码值列表
# AAA # 送入decodeUnicode函数进行utf-8解码得到结果!
# ========== decodeUTF8 方法 ==============
# 功能: 将一个UTF-8码值列表(int)解析为可显示的中英文等字符串
# 实现方法如下(根据utf8编码方式进行解码):
# 1、从头遍历得到一个码值,先根据码值大小判断是ascii码还是中文等其它编码
# 2、码值小于等于127则是ascii码,直接chr()即可,大于192则是中文等编码!
# 3、中文等编码则根据大小判断此编码需要几个字节(2~4)。(注意5和6个字节是ucs-4,不属于标准unicode编码范围,此处不考虑)
# 4、最后取出编码中的数据位,组成unicode码,chr()即可!
# 例如:0b11100110 0b10110011 0b10001001 因0b11100110大于等于192,判断是utf-8编码,立即根据下面的对应位置来解析:
# 1110 此位置有三个1,一个0,所以判断此编码需要3字节
# 110 110011 001001 由这些对应上面的位置得到数据位,连接一起得到unicode码,最后chr()得到中文等字符!
def decodeUTF8(mylist): # 码值列表
lenMyList=len(mylist)
i=0
myUniStr="" # 解码后字符串
while i<lenMyList: # 数据没有了,跳出
if mylist[i]>=240: # 4字节 0b11110xxx
mynum=((mylist[i]&0b111)<<18)+((mylist[i+1]&0b111111)<<12)+((mylist[i+2]&0b111111)<<6)+(mylist[i+3]&0b111111)
myUniStr+=chr(mynum)
i+=4
elif mylist[i]>=224: # 3字节 0b1110xxxx
mynum=((mylist[i]&0b1111)<<12)+((mylist[i+1]&0b111111)<<6)+(mylist[i+2]&0b111111)
myUniStr+=chr(mynum)
i+=3
elif mylist[i]>=192: # 2字节 0b110xxxxx
mynum=((mylist[i]&0b11111)<<6)+(mylist[i+1]&0b111111)
myUniStr+=chr(mynum)
i+=2
else: # ascii码 0b0xxxxxxx,小于等于127,直接用chr()函数返回字符即可!
myUniStr+=chr(mylist[i])
i+=1
return myUniStr # 返回解码后的字符串
base64Str="aGVsbG8g5rOJ5Lit5rWBIQ==" # "hello 泉中流!"
codeBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
equalNum=base64Str.count("=") # 等号的个数
orgStr="" # 结果字符串
orgNum=[] # 编码值列表
for i in range(0,int(len(base64Str)),4):
if (base64Str[i:i+4].find("="))<0: # 分组字符串中没有“=”,说明是三个码值
index1=codeBase64.find(base64Str[i])
index2=codeBase64.find(base64Str[i+1])
index3=codeBase64.find(base64Str[i+2])
index4=codeBase64.find(base64Str[i+3])
num1=(index1<<2)+(index2>>4)
num2=((index2&0b1111)<<4)+(index3>>2)
num3=((index3&0b11)<<6)+index4
orgNum+=[num1,num2,num3]
elif equalNum==1: # 分组字符串结尾有1个“=”,说明是两个码值
index1=codeBase64.find(base64Str[i])
index2=codeBase64.find(base64Str[i+1])
index3=codeBase64.find(base64Str[i+2])
num1=(index1<<2)+(index2>>4)
num2=((index2&0b1111)<<4)+(index3>>2)
orgNum+=[num1,num2]
elif equalNum==2: # 分组字符串结尾有2个“=”,说明是一个码值
index1=codeBase64.find(base64Str[i])
index2=codeBase64.find(base64Str[i+1])
num1=(index1<<2)+(index2>>4)
orgNum+=[num1]
orgStr=decodeUTF8(orgNum) # utf-8解码
print("字符串:'"+base64Str+"'")
print("base64解码得:'"+orgStr+"'\n")
运行结果如下:
字符串:‘aGVsbG8g5rOJ5Lit5rWBIQ==’
base64解码得:‘hello 泉中流!’
5、总结
base64的编码和解码原理比较简单,利用二进制的与操作和移位操作基本就可以完成了,但是如果想要编码和解码相对完善,需要考虑中文等非ascii字符的处理问题。通过本文我把自己学习base64编码和解码的过程记录下来,因为我不善于写文章,所以有些地方可能叙述的不够清晰,或者有错误。仅希望本文能给大家带来帮助,也给我留下曾经学习过的记录,更欢迎大佬门批评指正。
2021年3月21日 18:06 by lzq2000 泉中流