为什么我需要使用'b'来对字符串进行Base64编码?

345

按照这个Python示例的方法,我使用以下代码将字符串编码为Base64:

>>> import base64
>>> encoded = base64.b64encode(b'data to be encoded')
>>> encoded
b'ZGF0YSB0byBiZSBlbmNvZGVk'

但是,如果我省略掉前面的b

>>> encoded = base64.b64encode('data to be encoded')

我遇到了如下错误:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python32\lib\base64.py", line 56, in b64encode
   raise TypeError("expected bytes, not %s" % s.__class__.__name__)
   TypeError: expected bytes, not str

这是为什么呢?


52
所有返回“TypeError: expected bytes, not str”的问题都有相同的答案。 - Lennart Regebro
1
b 简单地意味着你输入的是字节或字节数组,而不是字符串。 - Atul6.Singh
5个回答

348

Base64编码将8位二进制字节数据编码成仅使用字符A-Z, a-z, 0-9, +, /* 的字符串,因此可以在无法保留所有8位数据的通道上传输,例如电子邮件。

因此,需要一个8位字节的字符串。在Python 3中,您可以使用b''语法创建这些字节。

如果去掉了b,它就变成了字符串。字符串是Unicode字符序列。对于Unicode数据,base64不知道该怎么处理它,因为它不是8位。实际上,它不是任何比特。 :-)

在您的第二个示例中:

>>> encoded = base64.b64encode('data to be encoded')

所有字符都可以完美地适应ASCII字符集,因此使用base64编码实际上有点无意义。您可以将其转换为ASCII格式,方法如下:

>>> encoded = 'data to be encoded'.encode('ascii')
或者更简单:
>>> encoded = b'data to be encoded'

在这种情况下,这也将是同样的事情。


* 大多数Base64变体可能在末尾包含一个=作为填充。此外,一些Base64变体可能使用除+/之外的字符。请参见Wikipedia上的变体总结表以获取概述。


它需要一个8位字节的字符串。计算机中的一个字节由8个比特组成,所有编程语言中的大多数数据类型(包括Python str)都是由字节组成的,因此我不明白你的意思。也许你的意思是“它需要一个8位字符的字符串”,就像ASCII字符串一样? - Alan Evangelista
1
从概念上讲,Python字符串是Unicode字符序列。它不需要任何特定的底层二进制表示。另一方面,bytesbytearray对象实际上代表了字节/八位组的序列。(虽然它也不需要任何特定的底层二进制表示。) - user2846495
1
@AlanEvangelista 并非每台计算机都有8位字节。现在很难找到一个字节不是8位的设备,除了DSP之外,但在旧时代,每字节使用6或7位的架构并不罕见。古代的诅咒魔法书中保留着32位甚至亵渎的48位字节的禁忌知识。 - jetpack_guy

232

简短回答

您需要将一个“类字节”对象(bytesbytearray等)推送到base64.b64encode()方法中。以下是两种方法:

>>> import base64
>>> data = base64.b64encode(b'data to be encoded')
>>> print(data)
b'ZGF0YSB0byBiZSBlbmNvZGVk'

或使用变量:

>>> import base64
>>> string = 'data to be encoded'
>>> data = base64.b64encode(string.encode())
>>> print(data)
b'ZGF0YSB0byBiZSBlbmNvZGVk'

为什么?

在Python 3中,str对象不是C风格的字符数组(因此它们不是字节数组),而是没有任何固有编码的数据结构。您可以用多种方式对该字符串进行编码(或解释)。最常见的(也是Python 3中的默认值)是utf-8,特别是因为它向后兼容ASCII(尽管大多数广泛使用的编码都是如此)。当您获取一个string并在其上调用.encode()方法时,就会发生这种情况:Python正在以utf-8(默认编码)解释字符串,并为您提供相应的字节数组。

Python 3中的Base-64编码

最初,问题标题询问Base-64编码。请继续阅读Base-64内容。

"base64"编码将6位二进制块使用字符A-Z、a-z、0-9、'+'、'/'和'='(某些编码在'+'和'/'的位置使用不同的字符)进行编码。这是一种基于数学构造的基数64或base-64数字系统的字符编码,但它们非常不同。在数学中,Base-64是像二进制或十进制一样的数字系统,你可以对整个数字进行基数转换,或者(如果你要从的基数是小于64的2的幂),从右到左分块进行转换。
在"base64"编码中,翻译是从左到右进行的;那前64个字符就是为什么它被称为"base64"编码。第65个'='符号用于填充,因为编码提取6位块,但通常要编码的数据是8位字节,所以最后一个块中有时只有两个或四个位。
例子:
>>> data = b'test'
>>> for byte in data:
...     print(format(byte, '08b'), end=" ")
...
01110100 01100101 01110011 01110100
>>>

如果您将二进制数据解释为一个整数,那么您可以将其转换为十进制和Base-64(Base-64表)。
base-2:  01 110100 011001 010111 001101 110100 (base-64 grouping shown)
base-10:                            1952805748
base-64:  B      0      Z      X      N      0

base64编码,会将数据重新分组如下:

base-2:  011101  000110  010101 110011 011101 00(0000) <- pad w/zeros to make a clean 6-bit chunk
base-10:     29       6      21     51     29      0
base-64:      d       G       V      z      d      A

因此,从数学上讲,'B0ZXN0' 是我们的二进制代码的 base-64 版本。然而,base64编码需要相反的方向进行编码(因此原始数据被转换为'dGVzdA'),并且还有一个规则来告诉其他应用程序在结尾处留下多少空格。这是通过使用 '=' 符号填充末尾来完成的。因此,这些数据的 base64 编码是 'dGVzdA==',其中有两个 '=' 符号表示需要从末尾删除两对比特以使其匹配原始数据。

让我们测试一下,看看我是否在撒谎:

>>> encoded = base64.b64encode(data)
>>> print(encoded)
b'dGVzdA=='

为什么要使用base64编码?

假设我需要通过电子邮件向某人发送一些数据,就像这样的数据:

>>> data = b'\x04\x6d\x73\x67\x08\x08\x08\x20\x20\x20'
>>> print(data.decode())
   
>>> print(data)
b'\x04msg\x08\x08\x08   '
>>>

我种下了两个问题:

  1. 如果我尝试在Unix中发送该电子邮件,则只要读取到\x04字符,电子邮件就会立即发送,因为这是ASCII的END-OF-TRANSMISSION(Ctrl-D),因此剩余数据将被遗漏。
  2. 而且,虽然Python足够聪明以直接打印数据时转义所有恶意控制字符,但是当该字符串解码为ASCII时,您可以看到'msg'不存在。这是因为我使用了三个BACKSPACE字符和三个SPACE字符来擦除'msg'。因此,即使我没有EOF字符,最终用户也无法从屏幕上的文本转换为真正的原始数据。

这只是一个演示,向您展示简单发送原始数据有多困难。将数据编码为base64格式可以给您提供完全相同的数据,但以确保其安全发送到电子媒体(例如电子邮件)的格式。


18
当你想进行字符串到字符串的转换时,base64.b64encode(s.encode()).decode() 不是很符合 Python 风格。在 Python3 中, base64.encode(s) 应该足够了。感谢非常好的关于 Python 中字符串和字节的解释。 - MortenB
3
是的,这很奇怪,但好处是只要工程师了解字节数组和字符串之间的区别,就非常清楚发生了什么。因为它们之间没有单一的映射(编码),而其他语言则假定存在这种映射。 - Greg Schmit
5
顺便提一下,在Python3中base64.encode(s)无法使用,你是说应该有类似的替代方法吗?可能会让人感到困惑的原因是,根据编码和字符串内容的不同,s可能没有1个唯一的字节数组表示。 - Greg Schmit
2
@MortenB 但是 b64 不仅适用于文本,任何二进制内容都可以进行 b64 编码(音频、图像等)。按照您的建议实现这个功能会在我看来隐藏文本和字节数组之间的差异,使调试变得更加困难。它只是将困难转移到其他地方。 - Michael Ekoka
1
@MortenB base64.encode 需要两个类文件对象。"在 Python3 中至少应该使用 base64.encode(s)" 是不正确的。 - Mattwmaster58
显示剩余3条评论

39

如果要编码的数据包含“奇特”的字符,我认为你需要以“UTF-8”编码。

encoded = base64.b64encode (bytes('data to be encoded', "utf-8"))

30

如果字符串是Unicode编码,最简单的方法是:

import base64                                                        

a = base64.b64encode(bytes(u'complex string: ñáéíóúÑ', "utf-8"))

# a: b'Y29tcGxleCBzdHJpbmc6IMOxw6HDqcOtw7PDusOR'

b = base64.b64decode(a).decode("utf-8", "ignore")                    

print(b)
# b :complex string: ñáéíóúÑ

虽然不是最简单的方法,但这是其中最清晰的方法之一。当传输字符串的编码方式很重要时,这是数据通过base64传输的“协议”之一部分。 - xuiqzy

13

谢谢,我正在使用3.x版本。为什么Python需要显式地将其转换为二进制?在Ruby中相同的操作需要引入"base64"库,然后使用Base64.encode64('要编码的数据')函数。 - dublintech
2
@dublintech 因为(Unicode)文本与原始数据不同。如果您想将文本字符串编码为Base64,则首先需要确定字符编码(例如UTF-8),然后您会得到字节而不是字符,可以使用文本ASCII安全形式进行编码。 - fortran
2
这并没有回答问题。他知道它可以使用字节对象,但不能使用字符串对象。问题是为什么。 - Lennart Regebro
@fortran 默认的 Python3 字符串编码是 UTF,不知道为什么还要显式设置。 - xmedeko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接