Python的编码解码把我坑惨了,原来encode和decode的区别这么大

小杨那天写了个爬虫,抓取一个中文网站的文章标题。他用requests拿到响应后,想看看原始内容长什么样,于是打印了response.content:

dart 复制代码
import requests

resp = requests.get("https://example.com/article")
print(resp.content)

终端输出:

bash 复制代码
b'\xe4\xb8\xad\xe6\x96\x87\xe6\xa0\x87\xe9\xa2\x98'

小杨盯着这串\x开头的十六进制数字,完全不知道这是什么。他试着用str()转换:

python 复制代码
print(str(resp.content))

输出还是一样:

bash 复制代码
b'\xe4\xb8\xad\xe6\x96\x87\xe6\xa0\x87\xe9\xa2\x98'

他纳闷了:str()不是能把任何东西转成字符串吗?为什么转完还是这堆乱码?

旁边的同事看了一眼,说:"response.content返回的是bytes,你要用decode()把它解码成字符串。或者直接用response.text,requests已经帮你解码好了。"

小杨试了一下:

bash 复制代码
print(resp.content.decode("utf-8"))

输出:

复制代码
中文标题

他终于看到了能读懂的字符。但他又冒出一个新问题:decode和encode到底谁是谁?什么时候该用哪个?为什么有时候用反了也不报错,只是得到一堆乱码?

这些问题的答案,都藏在"str和bytes是两种不同的东西"这个核心认知里。

先看一个最直观的对比

ini 复制代码
text = "中文"
data = text.encode("utf-8")
print(data)   # b'\xe4\xb8\xad\xe6\x96\x87'

back = data.decode("utf-8")
print(back)   # 中文

encode把字符串(str)变成字节串(bytes)。decode把字节串(bytes)变回字符串(str)。

方向是相反的,而且必须成对使用。用UTF-8编码,就得用UTF-8解码。用GBK编码,就得用GBK解码。编码和解码的方式不一致,就会得到乱码或者报错。

用一个生活场景来理解。你有一封中文信,要寄给一个只懂英文的快递员。

encode的做法是:你把中文信翻译成英文,交给快递员。翻译的过程就是编码。

decode的做法是:收件人拿到英文信,再翻译回中文。翻译回来的过程就是解码。

翻译成英文(encode),再翻译回中文(decode),前提是两次翻译用的是同一套词典。如果你用英汉词典翻译过去,对方用法汉词典翻译回来,那就对不上了。这就是编码不一致导致乱码的原因。

str 和 bytes 是两种东西

要理解encode和decode,必须先理解str和bytes的区别。

str是"文本" 。它是一串Unicode字符,是人类可读的。你写的"中文"、"hello"、"123"都是str。str不关心字符在计算机里怎么存储,它只关心字符本身是什么。

bytes是"字节序列" 。它是一串0到255之间的整数,是计算机存储和传输的实际数据。文件里存的、网络上传的,都是bytes。bytes本身没有"字符"的概念,它只是一堆数字。

python 复制代码
text = "中文"
print(type(text))        # <class 'str'>
print(len(text))         # 2,两个字符

data = text.encode("utf-8")
print(type(data))        # <class 'bytes'>
print(len(data))         # 6,UTF-8 下一个中文字符占 3 个字节

"中文"是两个字符,但UTF-8编码后是6个字节。字符数和字节数不一样,因为一个字符可能需要多个字节来表示。

这就是为什么打印bytes会看到\xe4\xb8\xad这样的东西------它不是字符,是字节的十六进制表示。\xe4是一个字节,值是228。三个字节\xe4\xb8\xad组合起来,才代表"中"这个字符。

encode:把文本变成字节

encode是str的方法。它接受一个编码名称(默认是UTF-8),返回编码后的bytes。

ini 复制代码
text = "hello"
data = text.encode("utf-8")
print(data)  # b'hello'

text = "中文"
data = text.encode("utf-8")
print(data)  # b'\xe4\xb8\xad\xe6\x96\x87'

data_gbk = text.encode("gbk")
print(data_gbk)  # b'\xd6\xd0\xce\xc4'

同一个字符串,用不同的编码方式,得到的字节序列不同。UTF-8下"中文"是6个字节,GBK下是4个字节。

encode的适用场景:你要把文本写入文件、发送到网络、存入数据库的二进制字段,或者传给只接受bytes的API。

decode:把字节变回文本

decode是bytes的方法。它接受一个编码名称(默认是UTF-8),返回解码后的str。

ini 复制代码
data = b'\xe4\xb8\xad\xe6\x96\x87'
text = data.decode("utf-8")
print(text)  # 中文

如果编码和解码的方式不一致,就会出问题:

ini 复制代码
data = "中文".encode("utf-8")       # 用 UTF-8 编码
text = data.decode("gbk")           # 用 GBK 解码

要么报错:

arduino 复制代码
UnicodeDecodeError: 'gbk' codec can't decode byte 0xad in position 2: illegal multibyte sequence

要么得到乱码:

复制代码
涓枃

这就是"编码不一致"的典型表现。

坑一:搞反方向

最常见的错误,是对str调用decode,对bytes调用encode。

arduino 复制代码
text = "中文"
text.decode("utf-8")  # AttributeError: 'str' object has no attribute 'decode'

str没有decode方法,因为str已经是"解码后"的文本了,不需要再解码。

kotlin 复制代码
data = b'\xe4\xb8\xad\xe6\x96\x87'
data.encode("utf-8")  # AttributeError: 'bytes' object has no attribute 'encode'

bytes没有encode方法,因为bytes已经是"编码后"的字节了,不需要再编码。

记住:str用encode,bytes用decode。方向不能反。

坑二:编码和解码用了不同的字符集

这是乱码的头号原因。

ini 复制代码
data = "中文".encode("utf-8")     # 用 UTF-8 编码
text = data.decode("gbk")          # 用 GBK 解码
print(text)                        # 乱码:涓枃

或者反过来:

ini 复制代码
data = "中文".encode("gbk")       # 用 GBK 编码
text = data.decode("utf-8")        # 用 UTF-8 解码
arduino 复制代码
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte

编码和解码必须使用同一个字符集。用UTF-8编码,就必须用UTF-8解码。

坑三:文件读写的默认编码

open()函数在文本模式下会自动编码和解码。但默认编码取决于操作系统。

python 复制代码
with open("data.txt", "w") as f:
    f.write("中文")

在Linux和macOS上,默认编码是UTF-8,没问题。在Windows上,默认编码是GBK(中文系统),也没问题,因为读写用的是同一个编码。

但如果你在Windows上写了一个文件,拿到Linux上读,就可能出问题:

python 复制代码
# Windows 上写入(默认 GBK)
with open("data.txt", "w") as f:
    f.write("中文")

# Linux 上读取(默认 UTF-8)
with open("data.txt", "r") as f:
    content = f.read()  # 可能报 UnicodeDecodeError

解决方案是显式指定编码:

python 复制代码
with open("data.txt", "w", encoding="utf-8") as f:
    f.write("中文")

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

永远显式指定编码,不要依赖系统默认值。这是跨平台代码的基本要求。

坑四:requests 的 text 和 content

requests库有两个常用的属性:response.text和response.content。

response.content返回原始字节(bytes)。response.text返回解码后的字符串(str)。

response.text的编码是requests根据HTTP响应头里的Content-Type推断的。如果响应头没有指定编码,requests会用chardet或charset_normalizer去猜测。

猜测不一定准确。有时候响应头写的是ISO-8859-1,但实际内容是UTF-8,response.text就会得到乱码。

scss 复制代码
resp = requests.get(url)
print(resp.encoding)      # 可能是 ISO-8859-1
print(resp.text)          # 乱码

解决方案是手动指定编码:

ini 复制代码
resp.encoding = "utf-8"
print(resp.text)          # 正常

或者直接用content自己解码:

bash 复制代码
print(resp.content.decode("utf-8"))

坑五:errors 参数

decode有一个errors参数,控制解码失败时的行为。

默认是strict,遇到无法解码的字节就抛UnicodeDecodeError。

ini 复制代码
data = b'\xe4\xb8\xad\xff'
data.decode("utf-8")  # UnicodeDecodeError

errors="ignore"会跳过无法解码的字节:

ini 复制代码
data.decode("utf-8", errors="ignore")  # 跳过 0xff

errors="replace"会用替换字符(通常是�)代替无法解码的字节:

ini 复制代码
data.decode("utf-8", errors="replace")  # '中�'

errors="ignore"和errors="replace"会让程序不崩,但会丢失信息。除非你明确知道自己在做什么,否则不要随便用。数据损坏了却静默通过,比直接报错更危险。

坑六:BOM 问题

UTF-8有一个变体叫"UTF-8 with BOM"。BOM是文件开头的一个特殊字节序列\xef\xbb\xbf,用来标识文件是UTF-8编码。

ini 复制代码
data = b'\xef\xbb\xbfhello'
text = data.decode("utf-8")
print(text)  # '\ufeffhello',开头多了一个不可见字符

\ufeff是BOM对应的Unicode字符,叫"零宽不换行空格"。它看不见,但会导致字符串比较失败、JSON解析报错等问题。

解决方案是用utf-8-sig编码,它会自动处理BOM:

arduino 复制代码
text = data.decode("utf-8-sig")
print(text)  # 'hello'

读文件时也可以用encoding="utf-8-sig":

csharp 复制代码
with open("data.txt", "r", encoding="utf-8-sig") as f:
    content = f.read()

什么时候用 encode,什么时候用 decode?

判断标准很清晰:

你手里是str,要变成bytes :用encode。比如写文件、发网络请求、存数据库二进制字段。

你手里是bytes,要变成str :用decode。比如读文件、接收网络响应、从数据库读二进制字段。

不确定手里是什么 :用type()看一眼。

kotlin 复制代码
print(type(data))  # <class 'str'> 还是 <class 'bytes'>

文件读写 :显式指定encoding参数,不要依赖默认值。

网络请求 :优先用response.content.decode("utf-8"),或者手动设置response.encoding再用response.text。

一个容易忽略的点:默认编码是UTF-8

从Python 3开始,encode()和decode()的默认编码是UTF-8。

go 复制代码
"中文".encode()          # 等价于 "中文".encode("utf-8")
b'\xe4\xb8\xad\xe6\x96\x87'.decode()  # 等价于 .decode("utf-8")

但open()的默认编码不是UTF-8,而是取决于系统。所以open()一定要显式指定encoding="utf-8"。

回到小杨的故事

小杨最终的爬虫代码是这样的:

ini 复制代码
import requests

resp = requests.get("https://example.com/article")
resp.encoding = "utf-8"  # 显式指定编码
title = resp.text
print(title)  # 中文标题

或者更稳妥的方式:

ini 复制代码
title = resp.content.decode("utf-8")
print(title)

他后来在团队文档里写了一条:"str是文本,bytes是字节。encode把str变bytes,decode把bytes变str。方向不能反,编码和解码的字符集必须一致。文件读写显式指定encoding='utf-8',网络请求优先用content.decode('utf-8')。"

这个区分看起来简单,但真到写代码的时候,很多人会对着b'\xe4\xb8\xad'发呆,不知道这串东西怎么变成中文。或者用了response.text,结果得到一堆乱码,不知道为什么。

记住那个寄信的比喻:encode是翻译成对方能看懂的语言,decode是翻译回自己能看懂的语言。翻译过去和翻译回来必须用同一套词典,否则就会鸡同鸭讲。

编码解码本身不难,难的是想清楚"我手里的是文本还是字节,我要把它变成什么"。想清楚了,encode和decode就不再是坑,而是两个方向明确的工具。

相关推荐
明月_清风2 小时前
拆解 DeepSeek 最新开源推理框架 DeepInfer:万亿级大模型的高效落地实战
人工智能·后端·deepseek
Thneonl2 小时前
选举之后,数据去哪了:MongoDB rollback 目录里的失踪写入
后端·架构
Thneonl2 小时前
一个 user_id 打爆 Prometheus
后端·架构
弦儿2 小时前
从零认识 SpringBoot Web 后端:HTTP 协议 + 三层架构 + IOC/DI 知识点全梳理(附全套代码)
后端
海豚调度2 小时前
Apache DolphinScheduler 2026 年 9 月社区贡献总结报告
后端
全栈弄潮儿3 小时前
Python实战第10期:异常处理
后端·python·编程语言
专业程序开发源3 小时前
springboot校企合作双选会一站式预约与面试系统16010-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·面试·php·课程设计