先说个真实场景:你写了三行Python,print("中文"),然后终端给你甩出来一堆形如鈥斺€?或者䏿–‡的乱码。这个画面在Python圈子里出现频率有多高?毫不夸张地说,几乎每个写Python的人都被它折磨过,而且很多人只是抄了一个# -*- coding: utf-8 -*-往上堆,换台机器照样翻车。这篇文章我用自己踩过的坑、排查过的案例,把“Python打印中文乱码”这个问题从原理到实战彻底拆一遍,保证你读完能自己定位乱码的根源,而不是靠瞎试。
很多教程喜欢一上来就让你改sys.setdefaultencoding,或者让你在文件头加coding: utf-8,这些做法部分有效,但都是治标不治本。乱码的本质只有一句话:文本从一种编码规则编码,又用另一种编码规则去解码,两边对不上,自然就显示出一堆“外星文字”。所以真正要做的,是把你的Python程序在“字符串内部的存储方式 → 输出到控制台/文件/网络的方式 → 目标端显示时依赖的方式”这三条链路里,每一环都统一到同一个编码规则上。
这篇文章适合正在被乱码折磨的Python新人,也适合写了不少代码但始终没搞明白编码细节的中级开发者。我会从编码模型讲起,然后分场景拆解Windows控制台、文件读写、网络请求、数据库这几大高频乱码现场,最后给出一套我自己常用的排查心法和避坑清单。
1. 乱码根源:编码与解码是两把钥匙
1.1 Python 2 与 Python 3 的字符串模型差异
很多老项目还在跑Python 2,Python 2里字符串默认是字节串(byte string),而Python 3里字符串默认是Unicode字符串(str),字节串是单独的bytes类型。这一变化是“中文乱码问题”从Python 2时代的高发问题到Python 3时代依然存在但表现形式不同的大前提。
Python 2时代,你在源码里写print "中文",如果文件声明了# -*- coding: utf-8 -*-,解释器会按UTF-8去读这个源文件,但把这个字符串当成字节串存在内存里。你把它打印到终端,终端按自己的默认编码(Windows上是GBK)去解析这些字节,如果源文件是UTF-8编码、终端是GBK解码,自然乱码。Python 3默认源码就是UTF-8,字符串是Unicode,乱码问题从“源码读取”转移到了“输出环节”——你打印一个Unicode字符串,终端显示不出来,通常是因为控制台编码不是UTF-8,或者终端字体不支持对应字符。
1.2 编码、解码与字符集的关系
可以把“字符”理解为一个抽象概念,比如汉字“中”。计算机只能存二进制字节,所以需要用一套规则把“中”变成一个或多个字节,这个过程叫编码。反向从字节恢复成“中”的过程叫解码。字符集就是“字符 → 编号”的对照表,UTF-8和GBK都是给一个字符编号并映射成字节的规则,但映射规则完全不同。
举一个最直观的例子:汉字“中”的Unicode码点是U+4E2D,UTF-8编码后是三个字节E4 B8 AD,GBK编码后是两个字节D6 D0。如果你用UTF-8把“中”变成了E4 B8 AD,又用GBK把这三个字节解码,GBK会认为这是“涓”和另一个字符的一部分,最终显示出来就是一个完全无关的汉字。这就是乱码最典型的成因:编码和解码用的不是同一套规则。
1.3 为什么偏偏是中文容易乱码
英文字母和数字在几乎所有常见编码里都是单字节且兼容ASCII,无论UTF-8还是GBK,A都是0x41,所以英文几乎永远不会乱码。而中文字符在不同编码下占用字节数不同,映射规则差异很大,一旦编码解码不对齐就立刻暴露出来。Windows简体中文系统的默认编码是GBK(代码页936),Python 3内部字符串是Unicode,输出到Windows控制台时,控制台默认用GBK去编码解码,你如果用UTF-8输出,两边就打架了。macOS和Linux则默认UTF-8,所以“同样的代码在Mac上没问题,在Windows上乱码”的现象非常普遍。
2. 场景化诊断:先搞清楚你到底在哪一环乱码
很多人一乱码就到处搜“Python中文乱码解决方案”,然后看到一个命令就复制粘贴,运气好解决了,运气不好反而更乱。我的建议是:先定位,再解决。乱码场景不同,处理方式完全不同,用错方案等于白费。
2.1 Windows控制台直接print中文出现乱码
这是最常见的场景:你在Windows的CMD或者PowerShell里跑一个Python脚本,脚本里print("中文"),结果终端显示䏿–‡或者鈥斺€?之类。
先看诊断方法。打开一个交互式Python,输入下面两行:
import sys print(sys.stdout.encoding)如果输出的是utf-8,通常不会乱码;如果输出的是gbk或者cp936,说明标准输出用的是GBK,你print的字符串如果是纯Unicode,Python会自动按GBK编码输出,一般能显示中文。真正的问题通常出在:你的脚本源代码是UTF-8,但Python解释器在读取源文件时用的默认编码和源文件不一致(Python 2才有这个问题,Python 3默认UTF-8),或者你操作的是从外部读进来的UTF-8字节流,直接塞给了print。
诊断逻辑:
- 如果
print("中文")直接写在源码里并乱码,检查源码文件保存编码是什么,代码里有没有声明文件编码(Python 2需要,Python 3默认UTF-8但你的编辑器可能保存成GBK了)。 - 如果
print("中文")不乱码,但从某个接口拿到的字符串print出来乱码,说明是外部字节流解码错了。
2.2 读文件和写文件时中文乱码
写文件的场景也很高频:你用文本编辑器或脚本写了一个文件,Python读出来是乱码;或者你用Python写中文内容到文件,用记事本打开是乱码。
这类问题的根源通常是open()函数没有指定encoding参数。Python 3的open()不指定编码时,会使用locale.getpreferredencoding(),在Windows中文系统上这个值多半是cp936(GBK)。如果你用一个UTF-8编码的文件,却用GBK去读,自然乱码。反之,你写文件时默认按GBK写入,但你的代码编辑器或工具按UTF-8打开,也会乱码。
诊断方法:先确认文件本身的真实编码。用记事本打开文件看是否正常,或用专门的编码检测工具/命令查看文件字节。最简单的方式是用Python的chardet库或file命令(Linux/macOS)。
2.3 网络请求返回的中文乱码
爬虫方向的朋友碰到最多的问题是:用requests请求一个网页,返回的response.text打印出来中文乱码。这个问题的根源在于HTTP响应头里的charset声明和网页实际编码不一致,或者网页没有声明,requests猜测错了。
requests库在response.text时会根据HTTP头里的Content-Type字段里的charset去解码,如果charset没有或者错误,它会用apparent_encoding去猜测。猜测机制用了响应头里的meta标签和chardet,但不保证100%正确。
诊断方法:
import requests resp = requests.get("https://example.com") # 查看响应头编码声明 print(resp.encoding) # 查看原始字节 print(resp.content[:100]) # 手动指定编码重新解码 text = resp.content.decode("utf-8") print(text)2.4 数据库读写中文乱码
数据库方向的乱码隐蔽很多,因为中间隔了一层网络协议和驱动。表现通常是:程序写进数据库的中文在表里看到是正常的,查询打印出来却是问号???或者????,或者反过来,程序写进去的时候就是乱码,数据库工具里看也乱码。
这个链路很长:Python字符串 → 驱动编码 → 数据库连接协议编码 → 表字段字符集 → 存储引擎 → 查询时按某字符集返回 → 驱动解码 → Python显示。任何一环不对齐都会乱码。最常见的原因是:建立数据库连接时没有声明charset,比如MySQL连接串没加?charset=utf8mb4,驱动默认用客户端本地编码(Windows上是GBK)去和服务器通信,服务器按UTF8MB4存,两边对不上。
2.5 Python 2 遗留代码的经典乱码
Python 2 的问题比Python 3复杂得多,因为它有两个字符串类型str和unicode,还有一个极度容易踩坑的机制:str和unicode拼接时会按ASCII默认编码自动转换,一旦内容是非ASCII字符就报UnicodeDecodeError。很多Python 2老项目里写u"中文" + name,如果name是UTF-8字节串,直接抛异常。
此外,Python 2源码文件如果不加# -*- coding: utf-8 -*-,解释器默认按ASCII读取源码,只要源码里出现中文,直接SyntaxError。这也是为什么网上搜Python中文乱码,十有八九让你加这一行——其实是Python 2时代的解法,被无差别搬运到了Python 3的教程里。
3. 系统性解决方案:从源头到输出的四层加固
3.1 源码与字符串层:坚持UTF-8 everywhere
编码问题是全局性的,所以我推荐的第一个原则是全链路统一UTF-8:源码文件保存成UTF-8,文件读写用UTF-8,网络交互用UTF-8,数据库连接用UTF-8,终端也尽量设成UTF-8。这个方案能规避掉90%以上的乱码问题。
源码层面,Python 3默认源文件编码是UTF-8,只要你确保编辑器或IDE保存文件时选的是“UTF-8 without BOM”,源码里的中文字符串字面量就不会有问题。Python 2则需要在文件头加注释声明编码:
# -*- coding: utf-8 -*-注意:BOM(字节顺序标记)在Windows下常见,utf-8-sig编码会写入BOM头,如果你的代码文件被保存成了带BOM的UTF-8,Python 3读取源码默认UTF-8是可以容忍的,但有些工具会出问题。建议统一使用“UTF-8 without BOM”,如果处理的是需要Windows记事本打开的文件,写入时才选择utf-8-sig编码。
字符串操作层面,记住一个口诀:内部尽量用str,外部字节流按明确编码解码成str,输出时把str按目标端编码编码成字节流。也就是说,不要在程序中手动拼接中文和bytes类型,所有从文件、网络、数据库等外部来源拿到的数据,第一时间把它decode成str,然后再去处理;向外部输出时,再encode成目标端需要的字节编码。
3.2 文件读写:open()必须显式指定编码
文件读写是最容易修也最容易忽略的部分。正确姿势:
# 读文件 with open("test.txt", "r", encoding="utf-8") as f: content = f.read() # 写文件 with open("output.txt", "w", encoding="utf-8", newline="") as f: f.write("中文内容")不指定encoding参数,Python会按locale.getpreferredencoding()来,Windows中文版大概率是GBK,这就是为什么你在Windows上读UTF-8文件全是乱码的原因。macOS和Linux默认UTF-8,所以代码在Mac上没事、在Windows上乱码,多半就是open()没指定编码。
如果你不确定一个文件是什么编码,可以用chardet库自动检测:
import chardet with open("test.txt", "rb") as f: data = f.read() result = chardet.detect(data) print(result["encoding"]) # 例如 utf-8但要注意chardet是概率检测,不是100%准确,尤其是短文本。最可靠的还是知道文件的来源和格式约定,比如“我在某网站下载的数据清单,网站文档里写了UTF-8编码”,那就直接用UTF-8读。
读文件时如果内容包含GBK和UTF-8混排的特殊情况(极少数),可以考虑用errors="ignore"或者errors="replace"先不报错,但强烈不建议用errors="ignore"掩盖问题,因为会丢掉数据。正确的做法是确定统一编码后重新处理。
3.3 Windows控制台输出:三个开关一次搞定
Windows下Python打印中文乱码,本质是控制台代码页和Python输出编码不一致。最实用的解决方案按优先级排序:
方案一:设置环境变量(推荐,最稳定)
在系统环境变量里添加:
PYTHONUTF8=1:让Python 3.7+启用UTF-8模式,源码、stdout、stderr、文件I/O全部默认UTF-8。PYTHONIOENCODING=utf-8:强制标准输入输出(stdin/stdout/stderr)的编码为UTF-8。
设置方式:Win+R打开“系统属性” → 高级 → 环境变量 → 新建用户变量。或者命令行一次性生效:
set PYTHONUTF8=1 set PYTHONIOENCODING=utf-8 python your_script.py从Python 3.7开始,UTF-8模式是官方推荐的做法,它把默认编码统一为UTF-8,能一次性解决一大半Windows下的编码问题。具体可以参考官方文档中关于UTF-8 mode的说明。
方案二:改控制台代码页
在CMD里执行:
chcp 65001把控制台代码页切换为UTF-8,然后重新运行Python脚本。这个方法见效快,但只对当前控制台会话有效,而且有时候chcp 65001之后终端对中文字体的渲染支持不好,可能出现字体方块或对齐错乱。PowerShell用户可以在当前会话执行:
$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8方案三:在代码里强制指定标准输出编码
import sys if sys.stdout.encoding != "utf-8": sys.stdout.reconfigure(encoding="utf-8")reconfigure是Python 3.7+提供的方法,可以在运行中改变标准输出的编码,不用改环境变量,适合临时调试。注意这个方法改变的是当前进程的stdout编码,不影响其它程序。
我个人在Windows上长期用的是“环境变量方案”,因为一次配置,所有脚本受益,而且不会像chcp 65001那样偶尔触发终端渲染问题。如果你是临时跑一个脚本,用代码里reconfigure最省事。
3.4 网络请求:response.encoding要手动校准
requests处理中文乱码,我的习惯是拿到响应后先手动设置编码再取text。因为我遇到的所有“requests解码错误”案例里,几乎都是因为响应头里的charset不对。
举个例子:某网页HTTP头写的是Content-Type: text/html; charset=ISO-8859-1,但页面内容实际是UTF-8编码。requests按ISO-8859-1解码,中文全变成乱码。这时候手动指定:
import requests resp = requests.get("https://example.com") resp.encoding = "utf-8" # 覆盖错误声明 text = resp.text如果是POST接口返回的JSON,同样存在这个问题。优先用resp.json(),它内部会按UTF-8解码,前提是接口返回标准JSON格式且HTTP头声明了application/json; charset=utf-8,如果没声明,先用resp.encoding校准,或者直接用json.loads(resp.content.decode("utf-8"))。
还有一个经验:从网页抓取的HTML最好用BeautifulSoup配合from_encoding参数,比如BeautifulSoup(resp.content, "html.parser", from_encoding="utf-8"),这样可以绕过requests的猜测,直接从原始字节解码。
3.5 数据库连接:charset参数是命门
数据库方向的乱码,解决思路是“连接时显式声明编码,表结构确认字符集”。以MySQL为例,连接串里必须加charset=utf8mb4:
import pymysql conn = pymysql.connect( host="localhost", user="root", password="password", database="dbname", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, )PostgreSQL使用psycopg2时,连接串里也可以加client_encoding='UTF8'。SQLite则比较简单,数据库本身编码通常由传入的字符串决定,Python的sqlite3模块默认接受Unicode字符串,乱码问题相对少见。
如果连接串已经加了charset=utf8mb4仍然乱码,检查数据库表字段的字符集:
SHOW CREATE TABLE your_table;确认字段不是latin1或者gbk,utf8mb4是MySQL里支持完整Unicode(包括emoji)的字符集,推荐优先使用。
连接后查询如果有乱码,先用一行代码验证驱动是否正常:
cur.execute("SELECT '中文'") rows = cur.fetchall() print(rows) # 如果这里正常,说明链路没问题,问题在数据源如果这行也乱码,说明连接串或环境变量的问题;如果正常但某个表查询乱码,那就是表字段本身存储的字节编码不对,属于历史脏数据。
4. 实操记录:一次Windows下Python运行乱码的完整排查
写到这里,我会拿一个我曾经处理过的模拟项目来复盘。某天我收到一个“脚本在Windows上打出中文乱码”的现象,脚本的内容是从一个.txt文件读入若干中文用户名,处理后打印到控制台。
第一版代码大致长这样:
with open("users.txt", "r") as f: lines = f.readlines() for line in lines: print("处理用户:" + line.strip())当时在Windows的CMD里跑,结果处理用户:这几个字显示正常,但line部分显示成乱码。这个现象很快定位了问题:print("处理用户:" + line.strip())这一步,前半个字符串是源码里的中文字面量,在Python 3里是str;后半个line是从GBK文件读取并解码出来的字符串,但读取时没有指定encoding,open()默认用GBK解码,而users.txt是UTF-8编码的文件,UTF-8字节流被GBK解码后得到了一堆乱七八糟的字符。
修复方案:
with open("users.txt", "r", encoding="utf-8") as f: lines = f.readlines()改动后,读取时严格按UTF-8解码成正确的str,问题立即消失。
这个场景的核心教训是:源码里的中文字符串和外部读入的中文字符串,不一定是同一种编码来的。源码里的字面量在Python 3里永远按Unicode存放,外部读入的则取决于你解码时用的编码。那两个“中文”看起来一样,实际来源不同,处理方式不同,所以排查时要分开看。
5. 常见问题速查表与踩坑清单
为了让你快速定位,我把高频问题整理成一个速查表,建议收藏:
| 现象 | 可能原因 | 推荐操作 |
|---|---|---|
CMD里print中文变成䏿–‡ | stdout编码为GBK,字符串按UTF-8编码输出 | 设置PYTHONUTF8=1或chcp 65001 |
CMD里print中文变成鈥斺€? | stdout编码为GBK,字节流被错误解码 | 检查字符串来源,统一decode后str再print |
| Python 3读文件中文乱码 | open()未指定encoding,默认GBK读UTF-8文件 | 加encoding="utf-8"或按文件实际编码读 |
| 写文件后记事本打开乱码 | 写入编码与记事本默认编码不一致 | 写入utf-8-sig编码或统一UTF-8 |
requests抓网页中文乱码 | HTTP头charset错误或缺失,猜测失败 | 手动指定resp.encoding="utf-8" |
| 数据库查询中文乱码 | 连接串未声明charset | 加charset="utf8mb4" |
| Python 2源码中文报错 | 文件头没加编码声明 | 加# -*- coding: utf-8 -*- |
报错UnicodeDecodeError | str和bytes混用触发隐式转换 | 提前decode成str再拼接 |
再分享几条实操中的独家经验:
不要随便使用
sys.setdefaultencoding。Python 2时代有人用reload(sys); sys.setdefaultencoding('utf-8')来改默认编码,Python 3里sys.setdefaultencoding已经不存在了。这条方案在Python 2下也只是掩耳盗铃,强烈不建议模仿。终端字体也会“看起来像乱码”。如果你用
chcp 65001后中文显示成方块或问号,很可能不是编码问题,而是控制台字体不支持中文字形,换成“Consolas”或“Microsoft YaHei Mono”这类字体即可。print到日志文件乱码是另一个坑。如果你用print(..., file=open("log.txt", "w")),这里的open()不指定编码照样按GBK,建议用模块级别的日志(比如标准库logging)并显式配置FileHandler的编码为UTF-8:
import logging handler = logging.FileHandler("log.txt", encoding="utf-8") logging.basicConfig(handlers=[handler], level=logging.INFO) logging.info("中文日志")同一个程序在纯代码环境和交互式解释器里表现可能不同。有时候你在交互式环境里测试没问题,跑
.py文件却乱码,多是因为交互式环境的stdout编码和文件运行时的stdout编码设置不同。遇到这种情况,先在代码里print(sys.stdout.encoding)看看实际值是多少。chardet不是万能的。它适合做启发式检测,但短文本、混合编码内容误判率不低。真正讲究的方案是把数据来源的编码约定写死,比如“这个API返回UTF-8”、“这批CSV导出时是GBK”。这类约定最好写进注释,方便后来人。
6. 推荐的工具与实践基线
编码问题排查中比较顺手的工具,我再补充几个,按需自取。
chardet库用于检测未知编码的字节流,GitHub地址在官方文档里一搜就有,干净稳定。安装用pip install chardet。
系统命令:在Linux/macOS上用file -i filename可以快速看文件编码;Windows下没有标准等价命令,可以下载一个小工具或用Python代劳:
def detect_encoding(path): with open(path, "rb") as f: raw = f.read() import chardet return chardet.detect(raw)快捷键法:VS Code右下角可以看到文件编码,点击后可以“通过编码重新打开”或“通过编码保存”,调试时非常好用。注意保存文件时选择UTF-8不带BOM。
实用基线,我推荐按下面这个顺序做,很多问题能提前消灭:
- 源码文件统一UTF-8 without BOM。
- 环境变量设
PYTHONUTF8=1(Python 3.7+)和PYTHONIOENCODING=utf-8。 - 所有文件读写显式写
encoding="utf-8"。 - 所有网络请求拿到响应后先检查并校准
resp.encoding。 - 数据库连接串默认带
charset=utf8mb4(MySQL),PostgreSQL带client_encoding='UTF8'。
这样一套下来,90%的乱码问题不会出现,剩下的是一些历史数据和第三方库的个例,用上面那张速查表基本能定位。
最后说一句体会:编码乱码问题不是Python的问题,是所有跨平台、跨系统文本交换都会碰到的通用问题。Python只是把这个问题暴露得更直接而已。真正治本的思路永远不是“找一个万能的编码方案”,而是搞清楚你的字符串是什么编码、目标端要什么编码,然后显式告诉程序“按这个编码处理”。你把这两点拿住了,乱码在你手里就再也没有神秘感了。