Python:用字符串拼接字节
我正在做一个Python项目,使用的是2.6版本,同时也在为将来支持Python 3做准备。具体来说,我在研究一种叫做digest-md5的算法。
在Python 2.6中,如果不运行以下导入:
from __future__ import unicode_literals
我可以写出这样的代码:
a1 = hashlib.md5("%s:%s:%s" % (self.username, self.domain, self.password)).digest()
a1 = "%s:%s:%s" %(a1, challenge["nonce"], cnonce )
没有任何问题,我的认证功能正常。但是,当我尝试在导入了unicode_literals的情况下使用同样的代码时,就出现了一个错误:
UnicodeDecodeError: 'utf8' codec can't decode byte 0xa8 in position 0: unexpected code byte
我对Python还比较陌生,所以在解决这个问题上有点卡住了。如果我把格式化字符串中的%s替换成%r,我可以拼接字符串,但认证就不管用了。我读过的digest-md5规范说,16个字节的二进制摘要必须附加到其他字符串后面。
有没有什么想法?
2 个回答
问题在于,当你导入了unicode_literals后,"%s:%s:%s"变成了一个unicode字符串。而哈希的输出是一个“普通”的字符串。Python试图把这个普通字符串解码成unicode字符串,但失败了(这也是预期中的结果,因为哈希输出看起来应该像杂音一样)。你可以把代码改成这样:
a1 = a1 + str(':') + str(challenge["nonce"]) + str(':') + str(cnonce)
我假设cnonce
和challenge["nonce"]
是普通字符串。为了更好地控制它们转换成字符串的方式(如果需要的话),可以使用:
a1 += str(':') + challenge["nonce"].encode('UTF-8') + str(':') + cnonce.encode('UTF-8')
你观察到的行为是因为 from __future__ import unicode_literals
改变了 Python 处理字符串的方式:
- 在 2.x 版本中,没有 u 前缀的字符串被当作字节序列处理,每个字节的值在 \x00 到 \xff 之间。带有 u 前缀的字符串是 ucs-2 编码的 Unicode 序列。
- 在 Python 3.x 版本中,以及在使用
unicode_literals
的情况下,没有 u 前缀的字符串是 Unicode 字符串,编码方式可以是 UCS-2 或 UCS-4(这取决于编译 Python 时使用的编译器选项)。带有 b 前缀的字符串是bytes
数据类型的字面量,和 3.x 之前的非 Unicode 字符串比较相似。
无论使用哪个版本的 Python,字节字符串和 Unicode 字符串都需要进行转换。默认的转换方式取决于你系统的默认字符集;在你的情况下是 UTF-8。如果不设置任何东西,默认应该是 ascii,这会拒绝所有值大于 \x7f 的字符。
由 hashlib.md5(...).digest() 返回的消息摘要是一个字节字符串,我想你希望整个操作的结果也是一个字节字符串。如果你想这样做,需要将 nonce 和 cnonce 字符串转换为字节字符串:
a1 = hashlib.md5("%s:%s:%s" % (self.username, self.domain, self.password)).digest()
# note that UTF-8 may not be the encoding required by your counterpart, please check
a1 = b"%s:%s:%s" %(a1, challenge["nonce"].encode("UTF-8"), cnonce.encode("UTF-8") )
另外,你也可以将来自 digest()
的字节字符串转换为 Unicode 字符串(不推荐)。因为 UCS-2 的低 8 位等同于 ISO-8859-1,这可能满足你的需求:
a1 = hashlib.md5("%s:%s:%s" % (self.username, self.domain, self.password)).digest()
a1 = "%s:%s:%s" %(a1.decode("ISO-8859-1"), challenge["nonce"], cnonce)