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

简介: 本文通过小杨爬虫遇乱码的实例,深入浅出讲解Python中str与bytes的本质区别:str是Unicode文本,bytes是字节序列;encode将str转为bytes,decode将bytes转为str。强调方向不可逆、编解码字符集必须一致,并详解requests、文件读写等常见场景的编码坑及解决方案。(239字)

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

import requests

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

终端输出:

b'\xe4\xb8\xad\xe6\x96\x87\xe6\xa0\x87\xe9\xa2\x98'

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

print(str(resp.content))

输出还是一样:

b'\xe4\xb8\xad\xe6\x96\x87\xe6\xa0\x87\xe9\xa2\x98'

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

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (6).png

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

小杨试了一下:

print(resp.content.decode("utf-8"))

输出:

中文标题

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

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

先看一个最直观的对比

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本身没有“字符”的概念,它只是一堆数字。

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。

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。

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

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

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

要么报错:

UnicodeDecodeError: 'gbk' codec can't decode byte 0xad in position 2: illegal multibyte sequence

要么得到乱码:

涓枃

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

坑一:搞反方向

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

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

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

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。方向不能反。

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

这是乱码的头号原因。

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

或者反过来:

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

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte

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

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

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

with open("data.txt", "w") as f:
   f.write("中文")

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

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

# 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

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

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就会得到乱码。

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

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

resp.encoding = "utf-8"
print(resp.text)          # 正常

或者直接用content自己解码:

print(resp.content.decode("utf-8"))

坑五:errors 参数

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

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

data = b'\xe4\xb8\xad\xff'
data.decode("utf-8")  # UnicodeDecodeError

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

data.decode("utf-8", errors="ignore")  # 跳过 0xff

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

data.decode("utf-8", errors="replace")  # '中�'

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

坑六:BOM 问题

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

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

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

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

text = data.decode("utf-8-sig")
print(text)  # 'hello'

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

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

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

判断标准很清晰:

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

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

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

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。

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

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

回到小杨的故事

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

import requests

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

或者更稳妥的方式:

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就不再是坑,而是两个方向明确的工具。

目录
相关文章
|
6天前
|
云安全 人工智能 运维
当攻击全面Agent化,你的安全防御能否跟上AI Speed?
多起客户侧AK泄露事件应急的真实观察
1303 6
|
5天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
|
10天前
|
存储 人工智能 安全
千问办公官网入口网页版登录:qwenwork.cn 注册送2000积分、登录送100积分
阿里云千问办公(QwenWork)是AI智能工作平台,支持一句话生成PPT、表格、网页、视频及数据分析。个人免费版送2000积分+每日登录赠100分,含1GB存储、5个发布页;企业版198元/席/月起。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
29天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9928 33
|
5天前
|
人工智能 开发者
Qoder CN 大使计划正式开启|无限创造,等你一起
全年免费积分、优先内测、活动支持与更多共创机会,邀请用 Qoder 把想法变成现实的你。
497 0
|
16天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
7天前
|
Web App开发 人工智能 JavaScript
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
REA(Reverse Engineer Anything)是近期爆火的开源AI逆向工具,4月发布,1个月内斩获15万+ Star。它让AI Agent无需源码即可分析二进制、APK、JS/Electron、HAR包等17类目标,自动反编译、生成可运行代码,并强制返回带证据与局限的结论。全本地执行,MIT协议,支持Hopper/Ghidra/IDA等引擎,一行命令即可上手。
729 2
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
|
14天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
1052 1