Python 中的 Base64 编码:完整指南
这是硬币的另一面。你手里有数据 - 一个文件、一组用户名密码、一团二进制、一段 Unicode - 而往下游的某处,它必须穿过一个只接受字母的通道。这就是 Base64 的全部工作:把数据每三个字节重写成四个字符,取自一个 64 字母的字符表,用 = 把最后那组补满,让一切以四个为一组收尾,然后把字母交出去。本站首页已经完整解释了这个格式,字母表也包括在内,所以我们只用一口气带过它,把其余时间花在 Python 实际上拿它做什么上。
开始之前,经济学方面值得一句诚实话,因为这个数字在每一个关于它的对话里都会出现:那份文本安全的代价是体积。Base64 会把你的数据膨胀大约三分之一,每三个输入字节换来四个字符,所以一兆字节的二进制会变成一兆字节加三分之一的字母。对一个令牌或一个配置值来说无关紧要;对一个视频文件来说,这就是你应该想想备选方案的原因。
好消息是:Python 对这一切的答案是一个 import 加一个函数。base64.b64encode 几十年来就在标准库里,不需要安装,底层跑的是 C 的速度。这份指南剩下的内容是那条让单行代码在真实世界里有用的长尾:那条拦下半数 TypeError bug 的只收字节规则、URL 安全字母表、MIME 折行工具,以及 Base64 正在其中悄悄干活的各个协议 - JWT、HTTP 头部、WebSocket 握手、邮件、PEM、data URL。
认识 b64encode
约定用四句话就能装下。一:输入是 bytes 类对象 - bytes、bytearray、memoryview - 普通字符串会被拒收。二:输出是一个 bytes 对象,绝不是 str。三:输出永远填充到四的倍数个字符,所以哪怕只有一个输入字节,也会产生 Zg==。四:输出只有一行,从不折行,无论输入多大。这篇文章里的其他一切,都是对这四句话的注脚:
import base64
encoded = base64.b64encode(b"foobar")
print(encoded)
# b'Zm9vYmFy'
print(len(encoded))
# 8
想要一个真正的字符串,为了 URL、头部或 JSON 字段,就把结果按 ASCII 解码。字母表保证里面不可能有其他东西,这让这一步又安全又便宜:
import base64
text = base64.b64encode(b"foobar").decode("ascii")
print(text)
# Zm9vYmFy
签名上还有一个参数,altchars,它把标准字母表里的 + 和 / 换成另一对字符。那正是 URL 安全变体背后的旋钮,所以先记住它 - 几节之后我们谈到令牌和查询字符串时就会见到它。
类型墙:str 不是 bytes
这篇文章里第一道 Python 专属的墙是类型系统,值得学会去感觉它。b64encode 拒绝字符串,用的还是这门语言里最不留情面的错误消息之一:
import base64
try:
base64.b64encode("hello")
except TypeError as caught:
print(caught)
# a bytes-like object is required, not 'str'
解法是整个指南里最重要的一个习惯:先把你的文本变成字节,然后刻意选择编码,而不是靠猜:
import base64
print(base64.b64encode("été".encode("utf-8")))
# b'w6l0w6k='
print(base64.b64encode("été".encode("utf-16")))
# b'//7pAHQA6QA='
同样的字符,两个不同的字节串,两个不同的 Base64 输出。编码选择是一个决定,不是一个细节。UTF-8 是任何要过网线、过数据库、过 API 的东西的默认选项。UTF-16 出现在你跟 Windows API 打交道的时候,它还在开头带来一个你可能不想编码进去的字节序标记,用 utf-16-le 就能甩掉它,或者用 lstrip("\ufeff") 把它剪掉。Latin-1 还藏在老旧的欧洲文件里,那里一个字符恰好一个字节,整个问题根本不会出现。要记住的心智模型是:编码器从来看不到你的文本,它看到的永远只是位。字节一旦越过这堵墙,字符集的问题就关闭了 - 这也正是解码那一侧日后必须去问:那个字符集归谁所有。
base64url:换两个字母,丢掉填充
标准字母表里藏着两个 URL 和文件系统都讨厌的字符。+ 号会被任何表单解码器悄悄读成空格,/ 号是路径分隔符,所以一个标准字母表的载荷躺在查询字符串或文件名里,就是一颗滴答作响的定时炸弹。RFC 4648 第 5 节定义了修法:一个变体,其中 + 变成 -,/ 变成 _,只要数据长度能从上下文得知就丢掉填充,并且 RFC 坚持叫它 base64url 而不是简称为"base64"。你会在 JSON Web Token、OAuth 令牌和 API 游标参数里遇到它,也就是说,在现代 Web 的大部分角落。
Python 既带了专用函数,也带了前一节那个 altchars 旋钮,两者产生的输出完全一致:
import base64
data = b"\xfb\xff\xfe"
print(base64.b64encode(data))
# b'+//+'
print(base64.urlsafe_b64encode(data))
# b'-__-'
print(base64.b64encode(data, altchars=b"-_"))
# b'-__-'
在令牌和查询字符串里,填充通常也一并丢掉,因为末尾的 = 需要百分号编码,而且一些中间设备反正会把它弄坏:
import base64
padded = base64.urlsafe_b64encode(b"fooba")
print(padded)
# b'Zm9vYmE='
print(padded.rstrip(b"="))
# b'Zm9vYmE'
剪掉,发出,接收方用取模技巧 "=" * (-len(s) % 4) 把填充补回来,它产生的填充数量恰好等于长度要求的数量。经验法则:如果数据要待进 URL、文件名或 JWT,就用 urlsafe 变体并丢掉填充;如果它要待进邮件正文或文本文件,带填充的标准字母表才是常态。
当读取方想要多行:MIME 与 76 字符规则
b64encode 的那一行无尽长串,对 JSON 字段、头部和 URL 来说是完美的,但邮件有自己的主张。RFC 2045,也就是 MIME 标准,要求 Base64 输出被拆成至多 76 字符的行,而 Python 的遗留工具正是为了产出那个样子而建造的。encodebytes 在 Python 3.1 加入,替 bytes 对象完成折行:
import base64
wrapped = base64.encodebytes(b"x" * 100)
for line in wrapped.splitlines():
print(len(line), line[:12])
# 76 eHh4eHh4eHh4
# 60 eHh4eHh4eHh4
机制有点巧妙。这个模块按 57 字节的块编码,就是常量 MAXBINSIZE,因为 57 字节恰好变成 76 个字符,而且在现代 CPython 里,每个折行都以一个普通的换行符结尾。RFC 2045 要求 CRLF,但 Python 的 LF 输出被生态里的每一个解码器接受,包括 Python 自己的。遗留的文件到文件函数 encode 把同样的折行从一个文件句柄直接做到另一个,这让它成为处理大文件的干净工具,免得你在内存里把文件留两份。
哪个工具用在什么时候,简短地说:b64encode 用于一切要进 JSON 字段、URL、头部或数据库列的东西;encodebytes 用于邮件正文和 PEM 式护甲;遗留的 encode 用于你在流式处理大文件、想白拿折行的时候。选错是经典 bug,因为 JSON 字段里哪怕混进一个多余的换行,都足以让对端的严格解码器抛出异常。
扩展家族
base64 模块其实是 base-N 模块,它带着整个 RFC 4648 家族,外加几个来自计算机世界其他角落的表亲。其中大多数是同一份字节进字节出契约的单行替代品:
| 函数 | 字母表 | 你在哪里会遇上它 |
|---|---|---|
b16encode / b16decode |
0-9A-F |
"Base16" 就是十六进制;模块里往返最快的,适合哈希和 UUID |
b32encode / b32decode |
A-Z2-7 |
许可密钥和激活码;没有 0、O、1 和 I,所以念出声也认得出 |
b32hexencode / b32hexdecode |
0-9A-V |
带十六进制字母表的 Base32,Python 3.10 加入;让编码数据保持字典序可排序 |
a85encode / a85decode |
85 个可打印字符 | 来自 PostScript 和 PDF 的 ASCII85,Unix btoa 工具的后代;Python 3.4 起在模块里 |
b85encode / b85decode |
85 个可打印字符 | git 和 Mercurial 二进制 diff 使用的 Base85 格式;同样从 Python 3.4 起 |
z85encode / z85decode |
85 个可打印字符 | ZeroMQ 的 Z85,Python 3.13 加入;按每四字节一组给数据分帧 |
它们没有改变你已经学过的规则:字节进,字节出,一个可选的字母表,另一端还有一个对应的解码函数在等。实践中,只要需要人眼能读这个值,就伸手去拿 b16;值要被手工键入或念出时,拿 b32;85 字符的表亲只在规范指定时才用。其余一切,文章开头那对 Base64 函数才是正确的工具,其他所有部分也都建立在它之上。
页面里的图片:data URL
网络上最显眼的 Base64 就是 data: URI:媒体直接嵌进 HTML 或 CSS,让浏览器不必再发第二个请求。格式是 data:、媒体类型、单词 base64、一个逗号,然后是编码后的字节。从磁盘上的文件构建一个,只需三行:
import base64
with open("logo.png", "rb") as handle:
encoded = base64.b64encode(handle.read()).decode("ascii")
uri = "data:image/png;base64," + encoded
print(uri[:40])
# data:image/png;base64,iVBORw0KGgoAAAAN...
两条提醒,遵守起来都不费事。第一,浏览器会欣然渲染 data URI,也乐意在文档里装下它们好几个兆字节:超过几 KB 的东西,一个带着合适缓存头的普通图片请求在每一项要紧的指标上都赢。第二,冒号后面的媒体类型是一份承诺。如果字节是 JPEG,URI 就说 image/jpeg,因为有些工具会校验这对组合,有些渲染器干脆拒绝猜。.decode("ascii") 这一步也不是装饰;没有它,你就是在把一个 bytes 对象拼到字符串上,然后收下一个 TypeError,类型墙又来巡街了。
可以发出去的令牌:JWT
一个 JSON Web Token 是三段用点号连起来的 base64url:头部、载荷和签名。如果你要签发真令牌,别亲手拼这些部分。装上 PyJWT(pip install pyjwt),让它在一次调用里构建 base64url 部分、填充和签名:
import jwt
# 短于 32 字节的密钥会招来 PyJWT 的 InsecureKeyLengthWarning,对演示密钥来说算是一句合理的抱怨。
token = jwt.encode(
{"sub": "1234567890", "name": "John Doe"},
"super-secret-key",
algorithm="HS256"
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...
print(type(token))
# <class 'str'>
在底层,PyJWT 做的正是这篇文章描述的事:序列化成 JSON,跑过 urlsafe 编码器,再剪掉填充,一切按 RFC 7515 里的 JWS 定义。如果哪天你需要手工拼一个部分,为了测试夹具或调试会话,配方是到处相同的算术:
import base64
import json
payload = json.dumps({"sub": "1234567890"}).encode("ascii")
part = base64.urlsafe_b64encode(payload).rstrip(b"=")
print(part)
# eyJzdWIiOiAiMTIzNDU2Nzg5MCJ9
关于信任方向,说明一句:构建令牌是容易的那一半。接收方必须先验证签名,才能信任任何一条声明,而 PyJWT 2.x 在没有显式 algorithms 列表时不会解码令牌,这是个特性,因为"随便什么算法都行"的错误,是有史以来最昂贵的认证代码行之一。
HTTP:Basic 认证与 WebSocket 握手
HTTP 里有两个时刻的生死都系于 Base64。第一个是协议里最老的认证方案:Basic 认证(RFC 7617),客户端把 user:pass 做 Base64 编码,放在单词 Basic 后面发送:
import base64
credentials = base64.b64encode(b"jane:pa:ss").decode("ascii")
header = "Basic " + credentials
print(header)
# Basic amFuZTpwYTpzcw==
如果 requests 已经在你技术栈里,它可以用 auth=("jane", "pa:ss") 替你构建这个头部,值得用,因为它把编码细节挡在你的代码之外。顺手把正在发生的事说诚实:RFC 7617 直言不讳,这个方案"不是安全的用户认证方法,也不以任何方式保护那个以明文传输的实体"。任何看到流量的人一行代码就能还原凭据,所以这是 TLS 保护连接上的便利,不是安全边界。
第二个时刻是 WebSocket 握手(RFC 6455),服务器要证明自己读到了客户端的随机密钥,就用密钥粘上一个魔术 GUID 之后做 SHA-1 哈希、再取 Base64 来应答:
import base64
import hashlib
key = "dGhlIHNhbXBsZSBub25jZQ=="
magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
accept = base64.b64encode(
hashlib.sha1((key + magic).encode("ascii")).digest()
).decode("ascii")
print(accept)
# s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
这个输出就是 RFC 里那个算例的精确值,用它来检验从零写起的实现,是个可爱的办法。在生产环境里,websockets 库在两端都替你做了这一步;只有当你正在写那个证明你理解的小测试服务器时,才需要亲手拼。
邮件:最初的客户
Base64 在 1993 年被标准化,就为了一个活儿,而那个活儿就是邮件:按 RFC 2045 的 Content-Transfer-Encoding: base64,让二进制在只讲文本的 SMTP 世界里活下去。Python 的 email 包构建消息、挑选编码、按标准行宽折行正文,你一行 Base64 都不用亲自写:
import email.mime.multipart
import email.mime.application
msg = email.mime.multipart.MIMEMultipart()
msg["Subject"] = "binary payload"
part = email.mime.application.MIMEApplication(
b"\x00\x01\x02", _subtype="octet-stream"
)
msg.attach(part)
text = msg.as_string()
print(text)
# ...
# Content-Transfer-Encoding: base64
#
# AAEC
# --...
MIMEApplication 部件是有意思的那个:它把字节折成正确的 76 字符行,并盖上传输编码头,这正是之前那个 encodebytes 的行为由框架来应用。如果你拼的不是完整消息而是一段裸片段,email.encoders.encode_base64(obj) 会直接对一个消息对象做一次编码加折行。而当消息到达另一端时,故事解码这一侧,从 get_payload(decode=True) 到头部解码,都在那篇相关的解码文章里。
密钥与证书的 PEM 护甲
PEM 文件 - 那些以 -----BEGIN ...----- 开头的证书、私钥和 CRL - 不过是 Base64 外面的一层护甲:一行标签、折行后的 Base64、一个收尾标签。护甲一戳就穿,因为正文就是你已经见过的那种折行输出:
import base64
der = b"\x30\x03\x02\x01\x05" # 一个很小的 DER 数据块,用于演示
body = base64.encodebytes(der).decode("ascii")
armor = ("-----BEGIN CERTIFICATE-----\n"
+ body
+ "-----END CERTIFICATE-----")
print(armor)
# -----BEGIN CERTIFICATE-----
# MAMCAQU=
# -----END CERTIFICATE-----
去掉两行标签,把剩下的接起来,b64decode 就把 DER 字节还给你。在生产环境里你几乎不会亲手做这件事:cryptography 包(pip install cryptography)用 public_bytes 生成护甲,用 load_pem_x509_certificate 和伙伴们解析,底层的 Base64 一步都替你做了。手动路径发挥价值的特定时刻,是原始 DER 字节已经在手里的时候 - 一个数据库列、一个配置文件、协议里的缓冲区 - 而你面前的规范说:"请用 PEM"。
运送文件:上传、下载与 .b64 习惯
互联网上最老的用例,是一个必须穿过只运文本的通道二进制文件:一个会弄坏换行符的 FTP、一个拒绝上传的表单、一个吞掉二进制的聊天窗口。配方是读、编码、发出,让对端解码,而有趣的半边是中间两步:
import base64
with open("photo.png", "rb") as src:
data = src.read()
wrapped = base64.encodebytes(data)
with open("photo.b64", "wb") as dst:
dst.write(wrapped)
print(len(wrapped), "bytes on disk for", len(data), "in the photo")
# 比原始文件大约大三分之一
两条备注。.b64 扩展名是社区约定,不是标准,所以接收方也得知道这个约定 - 这就是为什么 JSON API 通常把载荷包进一个命名字段,比如 "image_base64",并在文档里说明。而 encodebytes 折出的 76 字符行,是磁盘文件的首选格式,因为它们能干净利落地穿过人手里的每一种文本工具复制粘贴,从邮件客户端到 PDF 阅读器。反方向,把这样的文件读回字节,是相关解码文章里的一次调用;在这里你只是发送方,而发送方的工作就是保持一致。
存储问题:配置文件、环境变量与数据库
开发者喜欢把 Base64 放进只接受文本的地方:一个 .env 文件、一个 .ini 设置、一个 TEXT 列。编码这一步微不足道,最常见的形状是 Base64 里的 JSON:
import base64
import json
config = {"api_user": "svc-bot", "api_pass": "hunter2-not-really"}
packed = base64.b64encode(
json.dumps(config).encode("utf-8")
).decode("ascii")
print(packed)
# eyJhcGlfdXNlciI6ICJzdmMtYm90IiwgImFwaV9wYXNzIjogImh1bnRlcjItbm90LXJlYWxseSJ9
然后是警告,因为整篇文章里最昂贵的误解就住在这里。Base64 不是站得住脚的混淆,也不是加密。RFC 4648 第 12 节说得像 RFC 所能达到的那样直白:base 编码"在视觉上隐藏了本容易辨认的信息,例如密码,但不提供任何计算意义上的保密性"。一个装着 Base64 机密的 .env 文件,保护你免受瞟一眼它的人,保护不了读它的人,一条 base64 -d 命令之后,那个"机密"就以明文坐在对方的终端里。如果数据真的敏感,先加密 - cryptography 包自带 Fernet,正是为此而生 - 然后只有当你的存储要求文本时,才对密文做 Base64。
一百万字节之后:大数据与分块
b64encode 是一个 C 速度的函数 - 在一台普通笔记本上,它处理一兆字节大约只要一毫秒 - 但它不是流式函数。标准库里到处都找不到 update 加 finish 的一对函数,所以编码大于你想留在内存里的数据,意味着自己动手做边界算术。三个输入字节产生四个输出字符,所以任何分块边界都必须落在三字节接缝上:
import base64
def encode_chunks(chunks):
out = []
leftover = b""
for chunk in chunks:
buffer = leftover + chunk
whole = len(buffer) // 3 * 3
if whole:
out.append(base64.b64encode(buffer[:whole]))
leftover = buffer[whole:]
if leftover:
out.append(base64.b64encode(leftover))
return b"".join(out)
with open("video.mp4", "rb") as handle:
encoded = encode_chunks(iter(lambda: handle.read(65536), b""))
输出与一次性编码整个文件逐字节相同,因为三字节接缝是分组唯一可能断开的位置。填充恰好出现一次,就在最后一个块上,而对端的严格解码器期待的正是这个。iter(lambda: handle.read(65536), b"") 这一行是按固定大小分片读取文件的标准惯用法,而 leftover 变量就是整个算法。解码那一侧持的是四字符接缝而不是三字节接缝,所以两篇文章把这份额术分着做,而不是重复一遍。
编码方翻车的地方
编码这一侧的坑比解码一侧少,因为你自己生产字母时,出错的地方更少。尽管如此,这些每个星期都会冒出来,而只要你认得早,每一个都有两分钟的修法:
- 把字符串喂给编码器。类型墙那一节的
TypeError。在源头用.encode("utf-8")修掉它,并且在你敲下它之前先想想,你到底想要哪个字符集。 - 忘了输出是字节。
b64encode返回的是 bytes;进 URL 或 JSON 字段的是str,所以.decode("ascii")这一步是配方的一部分,不是事后补丁。 - 手搓 URL 安全替换。
str.replace("+", "-").replace("/", "_")能用,但这是两个字母的维护债,而urlsafe_b64encode是一次调用。更糟的是,一次做了一半的替换,加号换了斜杠忘了,产出的字母表跟任何规范都对不上。 - 把填充留在 URL 里。查询字符串里末尾的
=,会被一个工具百分号编码,被另一个工具剥掉,接收方的填充算术就会以最让人困惑的方式崩掉。剥掉它们;长度已经告诉了解码器它需要的一切。 - 在不想要折行的地方折行。
encodebytes的换行对邮件和 PEM 是正确的,对 JSON 字段或 URL 则是毒药。一个多余的换行就足以让对端的严格解码器抛出一个关于你数据(而不是你格式)的异常。 - 重复编码。数据在上游已经是 Base64 了 - 一个从别的 API 预编码到达的字段,一个被
.b64处理了两次的文件 - 第二遍产生一个解码回去就是第一遍编码的字符串。往返一次,检查魔术字节,然后收手。 - 把机密交给 Base64 保管。存储那一节的警告,再重复一次,因为它花的是真金白银:如果威胁模型里包括任何会读这个文件的人,你需要的是密码学,不是字母表。
一份你真的读得懂的更新日志
这个模块的年纪,体现在安静而带有日期的改进上,而不是革命上。简短版,按各部件落地的顺序,从编码方的视角:
| 版本 | 发生了什么 |
|---|---|
| Python 2.4(2004) | Barry Warsaw 的完整 RFC 3548 支持发货:b16、b32 和 b64 家族,加上今天还在用的 standard_* 和 urlsafe_* 变体 |
| Python 3.1(2009) | encodebytes 到来,encodestring 被弃用,这次重命名至今还让旧教程绊跟头 |
| Python 3.4(2014) | 每个编码器都接受任何 bytes 类对象,a85encode 和 b85encode 加入模块 |
| Python 3.6(2016) | binascii.b2a_base64 学会了一个 newline 开关,正是它让 b64encode 得以保持一行无尽长串 |
| Python 3.9(2020) | 遗留的 encodestring 和 decodestring 名字终于被移除 |
| Python 3.10(2021) | b32hexencode 和 b32hexdecode,可排序的十六进制字母表表亲 |
| Python 3.13(2024) | z85encode 和 z85decode,ZeroMQ 的字母表,加入家族 |
| Python 3.14(2025) | 整个标准库的导入都更快了,base64 在内,b16decode 快了最多六倍,因为它的校验现在跑在 bytes.translate 上而不是正则表达式 |
如果你想要一条主线:模块在 1995 年被重写,把工作委托给 C 层的 binascii 模块,而这种委托到今天依然成立。bytes 时代的第一次改动,是 Python 3 开发期间 2007 年的一次提交,它让一切处处使用 bytes,这篇文章里的类型墙就出自那里,也正是它让现代编码器收 bytes、还 bytes,其他一切都只是围绕这一份契约的包装。
模块没告诉你的事
正经活儿干完了,来看编码侧账本上的那些小乐趣:
- 文档自己的例子做了十多年同一个演示:
b'data to be encoded'进去,b'ZGF0YSB0byBiZSBlbmNvZGVk'出来。你早就见过这对组合了,不管你认不认得。 b64encode底下的 C 函数添加结尾换行时,附带的注释写着"追加一个客气的换行"。一行源码,就是一整部文化。- 单词
password编码成cGFzc3dvcmQ=,这就是为什么日志文件里的 Base64 在扫描器眼里像个机密,而对读者来说离成为机密只差一条命令。 b64encode从不折行。从来没有。一 GB 的输入产出一整条 1.3 GB 的行,函数连眼睛都不眨一下。你想要多行,就得开口要encodebytes。- 模块的 docstring 至今还写着 RFC 3548,也就是 2003 年版的规范。RFC 4648 在 2006 年接了班,docstring 只是压根没注意到。
- Python 2 根本没有类型墙:
b64encode欣然接受一个str,还回来的也是一个。2007 年的 bytes 大改造结束了这一切,而大多数"为什么我的编码崩了"帖子至今指向的还是那些旧 Python 2 教程。 z85encode,家族里最新的一员(Python 3.13),也是最挑剔的:ZeroMQ 按每四字节一组给数据分帧,所以规范要求编码输出是五个字符的倍数 - 而文档把填充甩给了你:输入必须按 4 字节的倍数到达(编码器不会替你填充;3 字节的输入产出一个 4 字符的帧,没有任何 ZeroMQ 对端会接受)。
所以,编码方的哲学浓缩成三条规则。先定字节,再定编码,因为类型墙是大多数 Python Base64 bug 的出生地。为通道挑字母表,别为数据挑:邮件和文件用带填充的标准字母表,URL 和令牌用不带填充的 base64url,永远不要在键盘上即兴创造第三种变体。再把输出保持在读取方期待的形状:JSON 和头部是一行,MIME 和 PEM 是 76 字符的行,因为对端的解码器会拿它来量你。
当那些字母到达另一端时,真正的乐趣才开始:缺失的填充、静默的丢弃、并不完全是 Base64 的载荷,还有一个要应对两种脾气的解码器。这些在本页底部那篇相关的 Base64 解码文章里都有详细展开,两篇指南凑成一对读起来正合适。祝编码愉快。
最后更新: 2026-09-08
相关文章: Python 中的 Base64 解码:完整指南