python类型转换(int、float、str、bytes、bytearray)、base64编码、rsa公钥加密

2,938 阅读21分钟

python的常用数据类型不多,但经常会有需要互相转换的场景。虽然各种转换在python中都有着各种原生方法,但如果不经常做相关的转换工作,难免有所不熟悉,需要拿着“python xx类型转xx”这种关键词去搜索对应的转化函数。假如此时需要将A类型转换成B类型,但又不了解A和B之间能不能直接通过原生方法转化时,一个通用的做法是:将A类型先转换成str类型,再由str类型转成B类型。大多数时候这种做法是可以解决相关问题的,因为在这些类型中大家总是对str与其他类型的转换更熟悉。但有时候比起任何转换都要经过str,直接由A类型转B类型效率会更高,代码也会更优美。所以本文的首要目的就是帮助厘清各种转化的边界,哪些转换需要通过str作为中介,哪些不需要,具体用什么转换方法,有什么转换要求。然后对编码和解码做解析,同时拓展base64相关的知识。

本文共分三部分。

  • 第一部分 int、float、str、bytes、bytearray五种类型的相互转换。
  • 第二部分 base64编码与各种字符串的要求。
  • 第三部分 部分特殊场景的转换。

查阅某种类型有哪些转换方法和具体代码请根据下文的转换图参照第一部分,了解base64/base32/base16如何编码以及字符串的组成规则请参照第二部分,特殊场景如不同格式的公私钥的处理请参照第三部分。

1. int、float、str、bytes、bytearray五种类型的相互转换

下图为本文涉及到的5种类型的转换示意图。针对这5种类型,列出的共有15种转换情况,每种情况在下文中均附有代码和输出结果。其中bytes->bytes和str->bytes这两种情况除了使用到python builtins类型的方法外,还使用了标准包base64、struct中的方法。 (strcut可以将int/float/str转为bytes,但本文不着重记录struct,仅在int->bytes和bytes->int中做了示例)

image.png

1.1 bytes->str

#定义bytes
mybytes = b'python'

#bytes->str
mybytes.decode("utf-8") #以utf-8格式转换,格式也可以是ascii,gbk等
#output-> "python"

1.2 str->bytes

"""导入特定包"""
import base64

"""定义字符串"""
mystr = "python"            #常规字符串
b64str = "cHl0aG9u"         #base64格式的字符串,要求详见2.3
b32str = "OB4XI2DPNYYTE===" #base32格式的字符串,要求详见2.3
b16str = "707974686F6E"     #base16格式的字符串,要求详见2.3
hexstr = "707974686f6e"     #16进制字符串,要求详见2.3

"""str->bytes"""
str.encode(mystr,"utf-8")   #将字符串作为一个utf-8格式字符串转换成bytes
#output-> b'python'
mystr.encode("utf-8")       #同上
#output-> b'python'
base64.b64decode(b64str)    #将base64格式的字符串转成bytes
#output-> b'python'
base64.b32decode(b32str)    #将base32格式的字符串转成bytes
#output-> b'python'
base64.b16decode(b16str)    #将base16格式的字符串转成bytes
#output-> b'python'
bytes.fromhex(hexstr)       #将16进制字符串转成bytes
#output-> b'python'

1.3 bytes->bytes

"""导入特定包"""
import base64

"""定义bytes"""
mybytes = b'python'

"""bytes->bytes"""
base64.b64encode(mybytes)   #将bytes转换成base64格式的bytes
#output-> b'cHl0aG9u'
base64.b32encode(mybytes)   #将bytes转换成base32格式的bytes
#output-> b'OB4XI2DPNY======'
base64.b16encode(mybytes)   #将bytes转换成base16格式的bytes
#output-> b'707974686F6E'

base64.b64encode函数将b'python'转换成b'cHl0aG9u'的转化过程

image.png

1.4 bytes->int

"""定义bytes"""
mybytes = b'python'
shortbytes = b'abcd'


"""bytes->int"""
int.from_bytes(mybytes,byteorder="big") #将bytes以大端模式转成int,byteorder还可选little
#output-> 123666946355054
struct.unpack('i', shortbytes)  #将4个字节的bytes转为int,输入必须为4个字节
#output-> (1684234849,)

1.5 int->bytes

"""定义int"""
myint = 123666946355054
mybuf = 256

"""int->bytes"""
int.to_bytes(myint,6,byteorder="big")   #将int以大端模式转成bytes
#output-> b'\x00\x00python'  ps:不满8个字节,填充了两个全0的字节
struct.pack('i', mybuf)
#output-> b'\x00\x01\x00\x00' 

1.6 int->str

"""定义int"""
myint = 123666946355054
chint = 123

"""int->str"""
bin(myint)         #将int转成二进制字符串
#output-> '0b11100000111100101110100011010000110111101101110'
oct(myint)         #将int转成八进制字符串
#output-> '0o3407456432067556'
hex(myint)         #将int转成十六进制字符串
#output-> '0x707974686f6e'
str(myint)         #将int直接转成字符串
#output-> '123666946355054'
chr(chint)         #将int转成对应的单个unicode字符,需要这个int对应到一个unicode字符才能转换
#output-> '{'

1.7 str->int

"""定义str"""
chstr = "{"
mystr = "123"
binstr = "0b11100000111100101110100011010000110111101101110" #二进制字符串,要求详见2.3
octstr = "0o3407456432067556" #八进制字符串,要求详见2.3
hexstr = "0x707974686f6e"     #十六进制字符串,要求详见2.3

"""str->int"""
ord(chstr)       #将单个unicode字符转成int
#output-> 123   
int(mystr)       #将字符串转成int,需要这个字符串为全部为数字的字符 
#output-> 123   
int(binstr,2)    #将二进制字符串转为int,需要这个字符串全部为0-1,0b前缀可选
#output-> 123666946355054
int(octstr,8)    #将八进制字符串转为int,需要这个字符串全部为0-7,0o前缀可选
#output-> 123666946355054
int(hexstr,16)   #将十六进制字符串转为int,需要这个字符串全部为0-9、a-e、A-E,0x前缀可选
#output-> 123666946355054

1.8 float->str

"""定义float"""
myfloat = 123.0

"""float->str"""
float.hex(myfloat)     #将float格式的数字转成十六进制字符串
#output-> "0x1.ec00000000000p+6"
myfloat.hex()          #将float格式的数字转成十六进制字符串
#output-> "0x1.ec00000000000p+6"
str(myfloat)           #将float格式的数字转成普通字符串
#output-> "123.0"
#output-> (3.1415,)

1.9 str->float

"""定义str"""
floatstr = "123.0"
hexstr = "0x1.ec00000000000p+6"

"""float->str"""
float.fromhex(hexstr)   #将十六进制字符串转成float
#output-> 123.0
float(floatstr)         #将十六进制字符串转成float
#output-> 123.0

1.10 int->float

"""定义int"""
myint = 123        

"""int->float"""
int(myint)
#output-> 123

1.11 float->int

"""定义float"""
myfloat = 123.0

"""float->int"""
int(myfloat)
#output-> 123

1.12 bytes->bytearray

"""定义bytes"""
mybytes = b'python'

"""bytes->bytearray"""
bytearray(mybytes)    #将不可变的bytes转成可变的bytearray,方便修改
#output-> bytearray(b'python')

1.13 bytearray->bytes

"""定义bytearray"""
mybytearray = bytearray(b'python')

"""bytearray->bytes"""
bytes(mybytearray)  #将可变的bytearray转成不可变的bytes,阻止修改
#output-> b'python'

1.14 bytearray->str

"""定义bytearray"""
mybytearray = bytearray(b'python')

"""bytearray->str"""
mybytearray.hex()   #将bytearray直接转换成十六进制字符串
#output-> "707974686f6e"

1.15 str->bytearray

"""定义bytearray"""
hexstr = "707974686f6e"

"""str->bytearray"""
bytearray.fromhex(hexstr)  #将十六进制字符串直接转换成bytearray
#output-> bytearray(b'python')

2. base64编码的讲解与各种字符串的说明

2.1 base64编码表

base64编码是将任意二进制序列编码成ASCII字符(可打印字符)序列的编码方案。

众所周知,在ASCII编码方案中,单个字符使用1个字节来表示,实际用到其中7bits,共有128个不同的字符。在这128个不同字符中,可以简单分为可打印字符不可打印字符两种,其中可打印字符为序号33-127(95个)的字符,不可打印字符为序号0-32(33个)的字符。而对于一些古老的纯文本协议,如SMTP协议,需要所有的字符都是可打印字符,这时候就不能直接使用ASCII的编码方式,因为里面还有33个不可打印字符。为了解决这种问题,base64方案从这95个可打印字符中选出了64个,作为重新编码的基础字符。而为什么是64个字符,主要是为了尽量利用每一位bit,6bits能表示的的就64个字符,7bits能表示128个字符但是可打印字符不够128个,如果使用7bits的话,会有(128-95)=33种情况无法使用可打印字符表示,所以只使用了其中64个字符。而按照这种想法,只取32个可打印字符和16个可打印字符也是可以的,所以也会有base32编码和base16编码。

ASCII中部分不可打印字符

image.png

ASCII中部分可打印字符

image.png

Base64从ASCII中选取了64个可打印字符,然后对这些字符重新做了映射,例如字符"0",在ASCII中的编号是48,在Base64中的编号是52;字符"a"在ASCII中的编号是97,在Base64中的编号是26;字符"A"在ASCII的编号是65,在Base64中的编号是0. 同理,Base32、Base16从ASCII中分别选取了32、16个可打印字符,也各自重新做了映射。

Base64的编码表

0x0  0x1  0x2  0x3  0x4  0x5  0x6  0x7  0x8  0x9  0xa  0xb  0xc  0xd  0xe  0xf  0x10  0x11  0x12  0x13  0x14  0x15  0x16  0x17  0x18  0x19  0x1a  0x1b  0x1c  0x1d  0x1e  0x1f  0x20  0x21  0x22  0x23  0x24  0x25  0x26  0x27  0x28  0x29  0x2a  0x2b  0x2c  0x2d  0x2e  0x2f  0x30  0x31  0x32  0x33  0x34  0x35  0x36  0x37  0x38  0x39  0x3a  0x3b  0x3c  0x3d  0x3e  0x3f
0    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    26    27    28    29    30    31    32    33    34    35    36    37    38    39    40    41    42    43    44    45    46    47    48    49    50    51    52    53    54    55    56    57    58    59    60    61    62    63
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     +     /

Base32的编码表

0x0  0x1  0x2  0x3  0x4  0x5  0x6  0x7  0x8  0x9  0xa  0xb  0xc  0xd  0xe  0xf  0x10  0x11  0x12  0x13  0x14  0x15  0x16  0x17  0x18  0x19  0x1a  0x1b  0x1c  0x1d  0x1e  0x1f  
0    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    26    27    28    29    30    31  
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     2     3     4     5     6     7 

Base16的编码表

0x0  0x1  0x2  0x3  0x4  0x5  0x6  0x7  0x8  0x9  0xa  0xb  0xc  0xd  0xe  0xf  
0    1    2    3    4    5    6    7    8    9    10   11   12   13   14   15   
0    1    2    3    4    5    6    7    8    9    A   B   C   D   E   F

Base64、Base32、Base16各自包含的可打印字符

b64str="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
b32str="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
b16str="0123456789ABCDEF"

2.2 base64编码实例

有了相关的编码表,我们便可以开始尝试对字符串、bytes或者说字节流进行编码。假定有这么一个字节流,

011100000111100101110100011010000110111101101110

如果我们按ASCII的编码方式去对这个字节流进行解码,一个字节一个字节取出,共有下面6个字节,

01110000|01111001|01110100|01101000|01101111|01101110

按照ASCII的方式对这6个字节分别解析,得到"p", "y", "t", "h", "o", "n"六个字符,打印出来就是"python".

01110000|01111001|01110100|01101000|01101111|01101110
   p    |   y    |   t    |   h    |   o    |   n

此时,这个字节流解析出来的六个字符都是可打印字符,能够完整地打印出一个"python",结果是正常的。但如果对这个字节流做一个简单的修改,比如说将最后的8bits改为00000111(响铃符,不可打印,此时在python中使用print,则会打印出b'pytho\x6e'),则这个字节流在纯文本协议就会解析错误,因为出现了无法打印的字符。

01110000|01111001|01110100|01101000|01101111|01101110
   p    |   y    |   t    |   h    |   o    |响铃符(引发读取错误)

为了解决这个问题,我们需要使用Base64做一个映射,生成一个新的字节流。这个新字节流,还是使用ASCII编码去读取,但不会读出任何一个不可打印字符。这个过程需要4步来完成。

第一步,字节流按6bits读取成int (base64为6bits,base32为5bits,base16为4bits)

011100|000111|100101|110100|011010|000110|111101|101110
    28|     7|    34|    52|    26|     6|    61|    20

第二步,将读取到的int按base64编码逐个转换成字符

    28|     7|    34|    52|    26|     6|    61|    20
     c|     H|     i|     0|     a|     G|     9|     U

第三步,将上一步的字符逐个按ascii编码转换成对应的int

     c|     H|     i|     0|     a|     G|     9|     U
    99|    72|   108|    48|    97|    71|    57|   117

第四步,将上一步的int逐个按ascii编码逐个转成字节流

       c|       H|       i|       0|       a|       G|       9|       U
01100011|01001000|01101100|00110000|01100001|01000111|00111001|01110101

下图概括了以上4个过程 image.png

由上面的转换实例可知,base64至少要有3个字节才能转换(3x8=4x6),如果输入的字符不满3个字节,该如何去做处理?答案是填位,填到字节数为3的倍数,然后再转化。具体如何填位,需要在上面的基础上增加1个步骤,修改1个步骤。

增加步骤,在原第一步之前,将原始字节流尾部填0,填至字节数为3的倍数,

"pytho"的原始字节流为  :0111000001111001011101000110100001101111
尾部全部填0,共填入共80011100000111100101110100011010000110111100000000

修改步骤,原第二步,将尾部剩下的0按6bits一个转化为"="处理(b32:5bit/"=",b16:无"=")

    28|     7|    34|    52|    26|     6|    60|     0
     c|     H|     i|     0|     a|     G|     8|     =

按照上面的步骤可以实现一个完整的base64编码函数,如下

b64str="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" #base64所有字符
b64dict = {i:b64str[i] for i in range(len(b64str))} #建立base64映射
def b64encode(old:bytes)->bytes:
    lgt = len(old)          #获取bytes长度
    gap = 3 -lgt%3          #获取需要填充的字节数
    if gap>0:               #填充字节
        for _ in range(gap):
            old = old + b"\x00"
    oldint = int.from_bytes(old,byteorder="big")    #将bytes转化成int
    bitnum = (lgt + gap)*8
    masks = [1<<(bitnum-i-1) for i in range(bitnum)]
    chars = [] #存放取出的字符
    for i in range(int(bitnum/6)):
        mask = masks[i*6:i*6+6]
        num = (oldint&sum(mask))>>(bitnum-6*(i+1)) #利用二进制运算,每次6位取出对应的数字
        char = b64dict[num]                        #将int数根据base64编码转成字符
        chars.append(char)
    for i in range(1,gap+1):                       #将剩下的0按6bits一个转化为"="处理
        chars[-i]="="
    return bytes("".join(chars),encoding="ascii")

2.3 各种字符串的要求

二进制字符串

前缀:"0x"(可选)
主体:必须由0-12种字符构成
示例:"0x00001","100001"

八进制字符串

前缀:"0o"(可选)
主体:必须由0-78种字符构成
示例:"0o1765412","12356117"

十六进制字符串

前缀:"0x"(可选)
主体:必须由0-9、a-f、A-F这16+6种字符构成
示例:"0xaef123e","ed32798a","EFCA321B"

BASE64字符串

前缀:无
主体:必须由A-Z、a-z、0-9、+、/、=这64+1种字符构成,并且字符个数要为4的倍数,"="只能出现在尾部且最多2个
示例:"cHl0aG9u"

BASE32字符串

前缀:无
主体:必须由A-Z、2-7、=这32+1种字符构成,并且字符个数要为8的倍数","="只能出现在尾部且最多6个
示例:"OB4XI2DPNYYTE==="

BASE16字符串

前缀:无
主体:必须由0-9、A-F这16种字符构成,字符数无要求,尾部不会包含"="
示例:"1DCA2F"
说明:base16和十六进制字符串很像,但和十六进制字符串不一样的是,base64只支持大写的A-F

3. 部分特殊场景的转换

3.1 公钥的解析使用

证书的解析常见于爬虫场景,当我们在对一个登录界面的网页进行抓包分析时,往往会发现,POST出去的登录请求中的username和password字段被加密成了长长的一串字符串。示例如下。

原始数据:
username:"liming"
password:"liming"

POST登录请求中的数据:
password: "231acaeaf5a1dffedf19e576ee9e0d16dbabd6ce6660594978c7391b2af500f8f36881e7b2dc0600ce358796d2bf67340b1bd0ee50cfbdd911a0522e7fcb8e4c9e4ed478f61e9c3447727e5dd13e60b0bf98aec4d8756b0824a946290980d4527e8625658264f06cdfce86ec47b85e9c392bbef1754d16ff86ba03b761991a60"
username: "011b3690b8896fffbf612ddb72a2d0f0234cbab021aa10ec18e014d2a1f1bf6a7570c4d313af5fffbd6d13075e47b47bbc443cd6f61af1b84d4a03ae9eb7da6c59243a4f21da434d396571e173da55091f9dcfa1ab0e5ac6e36c441afc18c04b7db9445add14007b60e909708d3a44fba3de6ecd213833ecffcc196e4d3619a3"

原始数据是如何加密成下面的加密字符串的?一般都在这个登录界面的JS文件中可以找到加密的动作。而如何去找到加密动作?一般可以通过“浏览器打开登录界面-右键登录按钮-检查-eventlistener-click”为入口找到加密动作的源代码,而通过分析调试源代码,我们一般能找到两种形式的加密公钥。

十六进制字符串格式的模数(modulus)和指数(exponent)

rsa_public_key_modules='BE0594D36A8DE700E12C018B998A7F754FE3908816A4157D220CB755BF51B02ECA27ADEE1199436CF9622F1DE991798229ACDD2EB3931611EE5F44547B6AA05E6C5002B6FDCA4D92244B57C7C9EEEA59022EA6CF7EF967F6331DF62EBAB3CBCE7CB7B76B83439F31E95F26116709552ACFD39FDBC5E674230453FF23ADF7519B'
rsa_public_key_exponent='10001'
#该情况下module一般有256个字符,转换成二进制后就是1024位
#如何判断是否为十六进制字符串格式请参考2.3

base64字符串格式的完整公钥

rsa_public_key='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC7kw8r6tq43pwApYvkJ5laljaN9BZb21TAIfT/vexbobzH7Q8SUdP5uDPXEBKzOjx2L28y7Xs1d9v3tdPfKI2LR7PAzWBmDMn8riHrDDNpUpJnlAGUqJG9ooPn8j7YNpcxCa1iybOlc2kEhmJn5uwoanQq+CA6agNkqly2H4j6wIDAQAB'
#如何判断是否为base64字符串格式请参考2.3

两种形式对应的转化过程是不一样的。而了解如何转化之前,还得先从复习一下RSA,因为是爬虫模拟请求的场景,此处我们主要关注加密过程,而生成密钥和解密这两个环节我们先略过。

rsa加密过程

encryptedmsg = (msg**rsa_public_key_exponent)%rsa_public_key_modules

#原始msg进行exponent次幂,最后取余modules,得到加密信息encryptedmsg
#msg、exponent、modules三个都需要转化成int数才能进行上面的计算

由上了解到rsa加密过程需要3个参数msg、exponent、modules参与计算,其中msg为变量,exponent、modules是rsa公钥的一部分,一般是固定的。这三个参数参与运算都需要为int数,所以上面两种形式的公钥,我们要提取出int格式的exponent、modules。

十六进制字符串格式的模数(modulus)和指数(exponent)提取int

exponent = int(rsa_public_key_exponent,16)
modules  = int(rsa_public_key_modules,16)

base64字符串格式的完整公钥提取int

import base64

b64str = base64.b64decode(rsa_public_key) #使用base64读取

def key2me(b_str):  #从公钥中提取出modules和exponet
    if len(b_str) < 162:
        return False
    hex_str = ''
    for x in b_str:
        h = hex(ord(x))[2:]
        h = h.rjust(2, '0')
        hex_str += h
    m_start = 29 * 2
    e_start = 159 * 2
    m_len = 128 * 2
    e_len = 3 * 2
    modulus = hex_str[m_start:m_start + m_len]
    exponent = hex_str[e_start:e_start + e_len]
    return int(modulus,16),int(exponent,16)
    
exponent,modules = key2me(b64str)

提取到modules、exponet后就可以用来对username,password进行加密,可用于爬虫时模拟真正的前端请求。为了确保加密的可靠性,一般还需要对比较短的信息进行填充,在尾部使用随机数填充至128字节(填充还有其他细节请阅读源代码了解更多),这导致每次rsa加密都会出现不同的结果。如果不想手动进行填充代码的编写,则可以使用标准包rsa中的方法进行加密。假如我们已经获取了int类型的modules、exponet,可以通过以下代码获取加密字符串。

import rsa
msg = msg.encode('gbk') #utf8,ascii也可以,主要是要先解析成bytes,bytes转int
modules,exponent = 47935720654916623590644293621677208850287724941640799046618503720773919010421437477927662123127430857864157476831539539474670567438621094891058144531889292350380640531578134734983414572211724737787098575818778665317308006029293341304990192158356601996178672751544841614751795862975445314871728728380589885241, 5271096
finbytes = rsa.encrypt(message=msg, pub_key=PBKey) #msg填充后转成int在此函数内部完成
msg = "".join(['%02x' % b for b in finbytes])
#msg即为加密后的字符串

3.2 暂无

4. 总结

python中的类型转化不难。按抓问题要抓主要矛盾的说法,需要着眼之处就是bytes和str这一对兄弟类型,这里面有着最多的转化,其他的类型转化都可以直接间接地以bytes、str为桥梁来完成。具体问题具体分析,结合实际情况,就能把某种类型转成合适的类型。