编码和解码字符串
Python 的 Base64 函数处理的是字节,所以文本要先编码为 UTF-8,最后再解码回来。漏掉其中任何一步,都是最常见的错误。
import base64
s = "héllo wörld"
encoded = base64.b64encode(s.encode("utf-8")).decode("ascii")
print(encoded) # aMOpbGxvIHfDtnJsZA==
decoded = base64.b64decode(encoded).decode("utf-8")
print(decoded) # héllo wörld
b64decode 既接受 str 也接受 bytes,但 b64encode 只接受类字节对象。传入字符串会抛出 TypeError: a bytes-like object is required, not 'str'。
想手动检查结果,可以把它粘贴到 Base64 解码器 中,它在浏览器本地运行。
带填充和不带填充的 URL 安全 Base64
URL 安全字母表把 + 换成 -,把 / 换成 _(见 Base64 与 Base64URL 的区别)。Python 会保留 = 填充;如果目标格式(JWT、许多 API)不需要填充,就自己去掉。
data = b"<<???>>"
print(base64.b64encode(data)) # b'PDw/Pz8+Pg=='
print(base64.urlsafe_b64encode(data)) # b'PDw_Pz8-Pg=='
print(base64.urlsafe_b64encode(data).rstrip(b"=")) # b'PDw_Pz8-Pg'
解码不带填充的输入时,需要把填充补回去,因为 Python 坚持要求长度是 4 的倍数:
def b64url_decode(s: str) -> bytes:
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
print(b64url_decode("PDw_Pz8-Pg")) # b'<<???>>'
b64encode 和 b64decode 还接受一个 altchars 参数,即用来替换 +/ 的两字节字符串。altchars=b"-_" 等价于 URL 安全的函数。
print(base64.b64decode("PDw_Pz8-Pg==", altchars="-_")) # b'<<???>>'
编码和解码文件
对于能放进内存的文件,读取字节后一次调用即可完成编码:
from pathlib import Path
b64 = base64.b64encode(Path("hello.txt").read_bytes()).decode("ascii")
print(b64) # aGVsbG8K
Path("hello-copy.txt").write_bytes(base64.b64decode(b64))
临时转换一次、不想写代码时,文件转 Base64 工具可以在浏览器中完成同样的事。
大文件与流式处理
Base64 把每 3 个输入字节映射为 4 个输出字符,所以只要除最后一块外每块都是 3 字节的倍数,就可以分块编码数据流。这样中间不会出现填充,拼接后的输出与一次调用编码的结果相同。
CHUNK = 3 * 1024 * 256 # 768 KiB, a multiple of 3
with open("big.bin", "rb") as src, open("big.b64", "wb") as dst:
while chunk := src.read(CHUNK):
dst.write(base64.b64encode(chunk))
我们在一个 100 KB 的随机文件上与 base64 -w 0 做了对比:逐字节相同。分块解码也是同样的道理,块大小取 4 个字符的倍数即可,前提是输入中没有换行符。
MIME 换行
电子邮件和一些遗留系统要求每行最多 76 个字符(RFC 2045)。encodebytes 会这样做,base64.encode / base64.decode 则在文件对象之间完成同样的处理:
print(base64.encodebytes(b"x" * 60))
# b'eHh4eHh4...eHh4\neHh4\n' (76 chars, newline, rest, newline)
with open("big.bin", "rb") as src, open("big.mime", "wb") as dst:
base64.encode(src, dst)
with open("big.mime", "rb") as src, open("big.out", "wb") as dst:
base64.decode(src, dst)
注意结尾的换行符:encodebytes 总会加一个。在线版本见 MIME Base64 编码器。
校验输入
默认情况下 b64decode 是宽松的:它在解码前会扔掉字母表之外的任何字符。这会掩盖 bug。传入 validate=True 让它变得严格。
import binascii
print(base64.b64decode("aGk=!!")) # b'hi' (junk ignored)
try:
base64.b64decode("aGk=!!", validate=True)
except binascii.Error as e:
print(e) # Only base64 data is allowed
使用 validate=True 时,换行符也算无效字符,所以要先去掉 MIME 换行。你会遇到的错误都属于 binascii.Error 类型(ValueError 的子类):
| 输入 | 错误信息 |
|---|---|
aGk | Incorrect padding |
a | Invalid base64-encoded string: number of data characters (1) cannot be 1 more than a multiple of 4 |
aGk==,validate=True | Excess padding not allowed |
aGk=!!,validate=True | Only base64 data is allowed |
Base64 错误指南 解释了每种错误的成因,Base64 校验工具 会显示字符串在哪里出错。
常见陷阱
把 URL 安全的输入交给 b64decode。不加 validate=True 时,- 和 _ 会被悄悄丢掉,你得到的是乱码而不是报错:
print(base64.b64decode("PDw_Pz8-Pg==")) # b'<<\x0f\xcf\xc3\xe0'
print(base64.urlsafe_b64decode("PDw_Pz8-Pg==")) # b'<<???>>'
urlsafe_b64decode 同样接受标准输入,所以当你不确定收到的是哪种字母表时,它是更安全的默认选择。
把二进制数据当文本解码。如果载荷是图片或 ZIP,.decode("utf-8") 会以 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte 失败。请保持字节形式,并写入文件。
输出中的 b'...'。打印 b64encode 的返回值会显示一个字节字面量。如果这个 b' 跑进了 JSON 或请求头里,说明你忘了 .decode("ascii")。
把 Base64 当作保护手段。它只是一种编码,任何人都能还原。见 编码与加密的区别。
版本说明
base64.b64encode、b64decode、urlsafe_*和altchars在所有 Python 3 版本中都存在。- 从 Python 3.3 起,
b64decode接受 ASCIIstr输入(不仅限于字节)。 encodestring和decodestring在 3.9 中被移除;请使用encodebytes和decodebytes。binascii.b2a_base64(data, newline=False)(3.6+)是b64encode背后的底层函数,对b"hi"返回b'aGk='。- 同一个模块还提供
b32encode、b16encode、b85encode和a85encode,用于其他字母表。
常见问题
为什么 b64encode 返回的是 b'...' 而不是字符串?
Python 的 Base64 函数接受字节,也返回字节。对结果调用 .decode("ascii") 才能得到 str;对文本编码之前,要先调用 .encode("utf-8")。
在 Python 中如何解码没有填充的 Base64?
解码前把缺失的 = 补回去:s + "=" * (-len(s) % 4)。对于不带填充的输入,Python 会以 binascii.Error: Incorrect padding 拒绝。
b64decode 和 urlsafe_b64decode 有什么区别?
urlsafe_b64decode 会在解码前把 - 和 _ 转换为 + 和 /。普通的 b64decode 除非传入 validate=True,否则会把 - 和 _ 当作垃圾字符悄悄丢掉,结果得到错误的字节而不是报错。
base64.encodestring 还能用吗?
不能。encodestring 和 decodestring 已在 Python 3.9 中移除。需要带换行的 MIME 风格输出,请用 encodebytes 和 decodebytes。