多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python字符编码与乱码排查完全指南:从原理到实战

Python字符编码与乱码排查完全指南:从原理到实战 1. 先把乱码这件事彻底说清楚写Python这几年我几乎每隔几天就会在群里看到有人发乱码截图。打开日志文件发现满屏的“锟斤拷”运行脚本控制台冒出一堆\uXXXX刚生成的CSV用Excel打开直接变乱码……这些场景我相信大部分Python开发者都遇到过。很多人把乱码当作“玄学”其实不是。乱码的本质就一句话数据本身没有错错的是解码方式和你写入时用的编码不一致。这篇博文我会从编码原理讲起把源码文件、终端输出、文件读写、网页抓取、第三方库这几个最常见的乱码战场全部过一遍最后给出一套可以直接抄作业的排查流程和编码规范。不管你是刚装好Python准备写第一个脚本的新手还是被某个生产环境乱码折磨已久的资深开发者这篇文章都能帮你省下不少时间。1.1 字符编码的来龙去脉要理解乱码先得搞清楚字符编码到底是怎么回事。打个比方字符编码就像一本“翻译手册”。你写下“你好”这两个汉字计算机不认识它只认0和1所以需要用一套规则把“你好”变成二进制数字这个规则就是编码。最早有ASCII编码用7个bit表示128个字符只覆盖英文字母、数字和基本符号没有中文的位置。后来国内有了GB2312、GBK用两个字节表示一个汉字。再后来国际标准Unicode出现了它给全世界几乎所有语言的字符都分配了一个唯一的编号叫作“码点”。而UTF-8是Unicode的一种存储方式它的特点是兼容ASCII英文用一个字节中文通常用三个字节这套设计让它在网络上成了事实标准。问题出在哪里呢如果一个文件保存的时候用的是GBK但读取的时候程序误以为它是UTF-8就会出现乱码。反过来也一样。现实世界的混乱在于Windows的记事本默认保存中文文本时在老版本里用的是ANSI也就是GBK而Linux、macOS以及绝大多数编程工具默认用UTF-8。两边不商量好乱码就是必然的。1.2 乱码的三类典型成因我把实际工作中遇到的乱码归纳成三种类型排查的时候先对号入座会快很多。第一种是编码识别错位也就是解码时猜错了编码。这种情况最常见比如一个GBK编码的文件被当成UTF-8去读中文就会变成一个个或者“锟斤拷”。注意“锟斤拷”这个特征很经典它是UTF-8的字节序列被GBK解码后产生的固定“乱码文案”见到它基本可以锁定是编码不匹配。第二种是转换丢失也就是在转码过程中字符本身就无法表示被替换成了替代符。比如把一个特殊符号从GBK转成没有对应字符的编码时程序会用?或者\ufffd代替。这种乱码是不可逆的原数据已经丢了再怎么调整解码方式也找不回来。第三种是显示层假设错误。有时候数据本身完全正常但终端模拟器、编辑器、浏览器用了错误的字符集去渲染导致看起来是乱码。比如一个UTF-8格式的网页浏览器却用GBK去解析就会出现一堆乱码这时候只需改一下显示编码就恢复了数据根本没有损坏。1.3 Python 2和Python 3在字符串类型上的根本差异Python 2的时代乱码问题比现在严重得多根源在于Python 2的str类型本质上是字节串而unicode类型才是真正的字符串。两者混用的时候Python会自动帮你做编码转换但这个“自动”往往用的是ASCII编码一遇到中文就直接抛UnicodeDecodeError非常折磨人。到了Python 3这个问题被彻底重构了。现在str就是Unicode字符串bytes是字节串两者严格区分。比如你用open()读取文件时如果不指定encoding参数Python 3默认会用locale.getpreferredencoding()在Windows中文系统上通常是GBK在其他平台通常是UTF-8。这就导致同一个Python脚本在不同操作系统上表现完全不一样乱码情况也千差万别。所以我的第一个建议是写代码时永远不要去依赖“系统默认编码”。凡是涉及文件读写、网络传输、数据库连接都显式地指定encodingutf-8或者实际数据的编码这一步能做到80%的乱码问题在源头就被堵死了。2. 从源头杜绝Python源码文件本身的编码问题很多初学者遇到的第一类乱码其实是源代码文件本身的编码不对导致整个脚本还没运行就出了乱码或者在运行时报语法错误。2.1 源码文件头声明的正确姿势在Python 2时代源码文件如果要包含中文必须写这样一行注释# -*- coding: utf-8 -*-这行声明告诉Python解释器“该文件以UTF-8编码读取”。到了Python 3默认源码编码就是UTF-8所以不写这行声明也能正常运行。但问题在于你的文件保存时真的是UTF-8吗我见过太多案例文件头声明了utf-8但实际保存时编辑器用的是GBK结果Python一加载就报SyntaxError: Non-UTF-8 code starting with \xc4。所以我现在的习惯是源码文件头依然保留# -*- coding: utf-8 -*-声明虽然Python 3已经不强制了但它能起到“标注身份”的作用告诉后来维护代码的人这个文件约定用UTF-8编码。同时在编辑器里设置“保存时使用UTF-8 without BOM”双保险。2.2 编辑器默认编码的设置方法这里重点说三个最常用的编辑器。VS Code里点击右下角的编码显示区域比如“UTF-8”下拉菜单里可以选“Save with Encoding”重新保存文件。为了让所有新文件默认走UTF-8可以在settings.json里加上{ files.encoding: utf8, files.autoGuessEncoding: false }第二项的autoGuessEncoding我建议关掉。虽然它能根据内容自动猜编码但猜错的时候反而会带来更多混乱关闭它可以确保你始终看到的是存储的真实编码。PyCharm的情况更简单右下角的文件编码图标可以切换当前文件的编码但重点是“File - Settings - Editor - File Encodings”把Global Encoding、Project Encoding和Default encoding for properties files三项都设为UTF-8。这里有一个PyCharm特有的坑它默认会在新建文件里写入一个文件头模板某些版本还默认使用系统编码如果不改设置Windows下新建的py文件就是GBK。还有一个很容易被忽略的场景Windows系统自带的记事本。老版本记事本保存文件时默认是ANSI也就是GBK后来有些版本默认是UTF-8 with BOM。这种不确定的默认行为会导致文件在不同机器之间流转时出问题。我的建议是不要用记事本写代码至少装一个VS Code或者Notepad3这类可以显式控制编码的编辑器。如果一定要用记事本保存时务必点“另存为”手动选择“UTF-8”编码。2.3 快速判断一个Python文件当前是什么编码不确定文件编码时可以用工具快速检测。在Linux/macOS下直接使用file命令file my_script.py # 输出my_script.py: Python script, UTF-8 Unicode text在Windows下可以用VS Code打开文件查看右下角的编码标识也可以用Notepad的“编码”菜单查看。但要注意这些工具显示的可能是“UTF-8-BOM”带BOM而Python解释器对带BOM的源码文件处理有点特殊PyCharm和VS Code能正常读取带BOM的UTF-8文件但某些Linux下的工具会把BOM当成字符处理导致字符串判断出错。我自己的习惯是所有Python源码文件一律保存为UTF-8 without BOM。UTF-8 with BOM版本的第一个字符其实是\ufeff零宽不换行空格如果这个字符混进了字符串比较或者文件路径拼接里会出现特别隐蔽的BUG。3. 终端输出乱码运行时最常见的战场代码本身没毛病但一运行控制台里的中文全是乱码。这是Python脚本乱码里出现频率最高的一类原因就在于Python程序的输出编码和终端窗口的显示编码对不上。3.1 Windows下cmd窗口乱码的典型场景在Windows中文系统上cmd窗口默认使用代码页936也就是GBK。而Python 3在读取环境时如果系统locale是中文stdout的编码也会被设置为GBK。按说两边都是GBK不会乱码。问题出在其中的一个环节改变之后。比如你通过PYTHONIOENCODINGutf-8环境变量强制Python以UTF-8输出这时cmd里显示就会乱码。再比如Python 3.7之后你用了sys.stdout.reconfigure(encodingutf-8)把stdout改成UTF-8但cmd窗口还停留在GBK输出照样乱。解决方案没有统一答案关键是要让两端保持一致。我实测下来比较好用的方案是import sys import io # 仅在Windows环境下生效 if sys.platform win32: sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)或者在终端里执行chcp 65001chcp 65001是把当前cmd窗口的代码页切换到UTF-8。但要注意chcp 65001在某些Windows版本上有历史遗留问题切换后字体渲染可能会异常而且如果程序本身不输出UTF-8反而会弄巧成拙。所以最快的排查思路是先不修改Python代码在cmd里执行chcp看看当前代码页是多少再对照Python的sys.stdout.encoding是否一致。3.2 PowerShell和Windows Terminal的乱码处理PowerShell的情况比cmd复杂一些。PowerShell 5.1默认对[Console]::OutputEncoding的设置比较特殊中文乱码的频率很高。而Windows Terminal虽然界面美观但新用户往往不知道默认配置文件里还涉及编码设置。我的经验是既然已经在用Python就尽量让PowerShell也走UTF-8。可以在PowerShell里执行[Console]::OutputEncoding [System.Text.Encoding]::UTF8注意这只能影响当前会话要永久生效可以写进PowerShell的$PROFILE文件里。另外还有个环境变量PYTHONIOENCODING把它设为utf-8后Python在将输出写入stdout时会使用UTF-8很多工具链比如CI系统、日志采集都会受益$env:PYTHONIOENCODING utf-83.3 Linux和macOS下的locale问题在Linux服务器上Python脚本输出乱码往往和locale环境变量有关。比如LANGC或者LANGPOSIX时Python的sys.stdout.encoding可能变成ANSI_X3.4-1968也就是ASCII这时候你print中文直接抛UnicodeEncodeError。解决办法是确保locale是正确的。在/etc/locale.conf或shell配置文件里设置export LANGen_US.UTF-8 export LC_ALLen_US.UTF-8LC_ALL优先级最高所以只需要它一个就能覆盖其他所有locale变量。如果系统里没有en_US.UTF-8这个locale需要先执行locale-gen生成。macOS这边相对简单默认locale通常是UTF-8但还是建议在终端里执行locale命令确认一下。3.4 重定向输出到文件时的乱码有时候终端里看起来正常但一旦用把输出重定向到文件再打开文件就乱码了。这是因为重定向后stdout不再是终端设备Python在非交互式输出时会改变编码策略或者找不到合适的编码。这个问题我遇到过很多次。解决方法是显式指定Python的IO编码PYTHONIOENCODINGutf-8 python my_script.py output.txt另外在脚本内部如果你要写日志建议不要依赖print加重定向而是直接用标准库logging并显式配置日志文件的encoding。这样至少日志文件的编码是确定的不会因为终端环境变化而变化。4. 文件读写与数据处理中的乱码实战终端输出解决之后文件读写的乱码问题就会暴露出来。这一部分涉及面很广我挑了几个最典型的场景来详细说。4.1 读写文本文件时显式指定编码Python内置的open()函数在Python 3里默认编码取决于locale这就埋了隐患。正确做法是with open(data.txt, r, encodingutf-8) as f: content f.read() with open(output.txt, w, encodingutf-8) as f: f.write(中文内容)读取时如果data.txt实际是GBK那么指定encodingutf-8会报UnicodeDecodeError这时候把编码换成gbk即可。但真实的项目里你往往不知道对方发来的文件是什么编码。这时可以分两步走第一步用二进制模式打开文件第二步用chardet库检测编码import chardet with open(unknown.txt, rb) as f: raw f.read() result chardet.detect(raw) print(result) # {encoding: GB2312, confidence: 0.99} text raw.decode(result[encoding])chardet是第三方库需要pip install chardet安装。它检测的confidence表示置信度0.99以上基本可靠但如果只有0.5左右建议你手动看一下文件内容的人工特征。注意一个问题chardet会把GBK返回为GB2312这两个编码非常接近用gb2312解码GBK内容通常没有问题因为GBK是GB2312的超集。4.2 CSV文件打开乱码的高频原因CSV乱码是群里问烂了的问题。常见场景是你用Python写了个CSVw模式下写入中文结果用Excel一打开中文全是乱码。原因在于Excel默认用系统ANSI编码中文Windows下是GBK去打开CSV文件如果你的文件是纯UTF-8Excel就不认识。解决方案是写入时使用utf-8-sig编码import csv with open(output.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([名称, 数量]) writer.writerow([苹果, 3])utf-8-sig会在文件开头写入BOMByte Order Mark也就是\ufeffExcel看到这个标记就会识别为UTF-8中文就能正常显示了。这里有个细节newline也很重要否则在Windows上CSV文件里会出现空行。反过来如果你要读取一个Excel导出的CSV文件发现中文乱码大概率是文件是用GBK存的。先用chardet检测或者直接从GBK解码with open(exported.csv, r, encodinggbk) as f: reader csv.reader(f) for row in reader: print(row)4.3 Linux下解压Windows压缩包乱码用户的热搜词里出现了“linux 解压文件乱码”这个场景非常典型。Windows下打包的ZIP文件文件名如果是中文使用的是GBK编码Linux下的unzip默认用UTF-8解码于是解压出来的文件名就全是乱码。常见解决方案有两个。第一个是用Python的zipfile模块来解压并手动指定文件名编码import zipfile with zipfile.ZipFile(archive.zip, r) as zf: for info in zf.infolist(): # 尝试用GBK解码文件名失败就回退到UTF-8 try: raw_name info.filename.encode(cp437).decode(gbk) except UnicodeDecodeError: raw_name info.filename print(raw_name) zf.extract(info, pathoutput_dir)第二个更省事的方式是使用unar工具macOS上叫ditto它能够自动识别压缩包内文件名的编码unar archive.zip我个人推荐先用unar搜不到再写Python脚本因为Python的cp437处理方式有时候也不完美它会根据ZIP文件内部标志位来决定是否做编码转换但Windows做的ZIP标志位经常不标准。4.4 网页爬虫获取到的内容乱码爬虫抓网页时遇到乱码也是高频问题。一般流程是用requests获取到response.content字节流然后手动解码。但网页实际使用的编码可能在HTTP头的charset里也可能在HTML的meta charset标签里两者还可能不一致。最省事的方案是用requests自带的response.encoding属性import requests resp requests.get(https://example.com) # 自动推断编码 resp.encoding resp.apparent_encoding text resp.text print(text)apparent_encoding底层也是chardet但它在某些网页上会猜错特别是中文页面。如果发现apparent_encoding给出的结果是ISO-8859-1或者ascii大概率是猜错了。这时直接看页面源码里的charset强制指定resp.encoding gbk有时候HTML的meta charset在新版本里写的是utf-8但旧站用的是gb2312二话不说直接chardet检测再解码即可。如果你抓的是一个JSON接口返回的中文乱码那多半也是接口返回的字节编码和requests默认的解码方式不匹配用resp.content.decode(utf-8)或者检测后的编码解码就行。5. 第三方库与工具链的乱码问题速查除了标准库Python生态里的一堆第三方库也会卷入乱码问题。这里挑几个有代表性的场景来梳理包括OCR库、JSON处理、以及一些常用的开发工具。5.1 PaddleOCR等文字识别类工具输出乱码用户的热搜词里有“paddleocr文字识别乱码”这个场景比较新也比较典型。PaddleOCR识别图片里的文字理论上输出的就应该是正常的字符串但实际使用中发现识别出的中文字符在命令行里显示出来是乱的。这里面有两层原因第一层是OCR模型本身识别的结果没问题但终端显示编码不对导致看起来乱码这属于前面说的“显示层假设错误”第二层是PaddleOCR在Windows上输出的stdout编码和终端不匹配尤其是如果你通过管道把输出接给其他程序时问题更容易出现。排查方法很简单把PaddleOCR识别出的文本写入文件而不是直接打印到终端from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) result ocr.ocr(test.png, clsTrue) # result是一个list里面存储的是识别结果 texts [line[1][0] for line in result[0]] with open(ocr_result.txt, w, encodingutf-8) as f: f.write(\n.join(texts))如果文件内容正常说明是终端的锅回去调终端编码如果文件内容也是乱码那就要检查PaddleOCR输出的字符串本身是否被错误转码了可以看看结果字符串的repr()输出print(repr(texts[0])) # 如果输出的是...\\u...或者...xxx...就能看出来问题出在哪5.2 JSON和Base64解码异常的乱码识别有时候你拿到一段JSON字符串里面中文是\u4e2d\u6587这种格式。这个不是乱码这是Unicode转义序列它表示的是“中文”这两个汉字。遇到这种情况直接用json.loads()解析即可Python会自动把\uXXXX转成真正的字符。真正会让人困惑的是Base64解码后的乱码。Base64本质上是字节编码解码后得到的是字节流如果你直接把字节流当字符串打印出现乱码是正常的因为字节流需要用正确的编码才能解读成文本。我见过很多同学在处理Base64时犯一个错误直接把解码后的bytes当str打印出来控制台上显示b...\xe4\xb8\xad...就以为乱码了。正确做法是import base64 encoded_str 5Lit5paH # 这是中文的Base64编码 raw_bytes base64.b64decode(encoded_str) text raw_bytes.decode(utf-8) # 指定编码 print(text)这里的decode(utf-8)是关键步骤。如果你解码后依然乱码那首先确认原始内容Base64之前是用什么编码的常见的有两种字符串先UTF-8编码再Base64或者先GBK编码再Base64。这两种结果完全不同用错编码解码就会乱码。5.3 常用开发工具乱码速查表用户的热搜词里有一串工具名我把它们整理成一个表格方便遇到问题时直接对照处理工具/场景乱码表现推荐处理方式VS Code 中文显示乱码打开文件看到“锟斤拷”或右下角点击编码 - “Reopen with Encoding” - 选择GBK或UTF-8在设置里固定UTF-8Windows记事本中文乱码打开UTF-8无BOM的文件显示乱码换用VS Code/Notepad3如果必须用记事本另存时选“UTF-8”PowerShell运行脚本中文乱码中文变成黑块或问号设置[Console]::OutputEncoding [System.Text.Encoding]::UTF8minicom串口乱码串口工具显示乱码在minicom设置里调整串口的编码/波特率确认设备端输出的字符编码Charles抓包中文乱码响应体中文乱码在Charles的Preferences - Display设置里选择文本编码通常选UTF-8Linux下unzip解压乱码解压出的文件名乱码用unar替代unzip或使用Python zipfile并指定GBK解码Jupyter Notebook中文乱码Output显示乱码%env PYTHONIOENCODINGutf-8或在~/.jupyter/jupyter_notebook_config.py中配置c.NotebookApp.extra_encodingMATLAB中文注释乱码2023版本打开旧文件显示乱码在MATLAB的“预设”-“字体”-“文本编码”里设置为UTF-8重新打开文件这张表不是让你背的而是提供一个排查方向。工具类乱码第一优先级永远是“搞清楚文件/字节流本身是什么编码”第二优先级才是“调整工具显示编码”。顺序不能反否则你会越调越乱。6. 一个完整的真实排查案例理论说了那么多最后用一个我印象特别深的真实案例把整个排查流程串起来。这个案例几乎涵盖了前面提到的所有知识点。6.1 问题的表象有一次帮朋友排查一个Python脚本脚本功能很简单读取一个文本文件把每一行都打印出来。但在Windows环境下运行终端输出全是乱码。朋友把脚本发给我我第一眼看到的是这样的# -*- coding: utf-8 -*- with open(data.txt, r) as f: for line in f: print(line)这段代码看起来没什么问题但运行后终端的中文就乱了。他说他在另外一台Linux机器上运行同样的代码就没问题唯独Windows上出问题。6.2 一步步排查我没有直接改代码而是按顺序做了三个检查。第一步检查data.txt本身的编码。用file命令在Linux下看或者直接在Windows上用VS Code打开发现这个文件是GBK编码而不是UTF-8。第二步检查Python在Windows上的默认编码。我让他写个小脚本打印出来import sys print(sys.getdefaultencoding()) # utf-8 print(sys.stdout.encoding) # cp936 (在中文Windows上通常是这个) print(open.__doc__) # 查看默认encoding参数说明在Windows中文系统上sys.stdout.encoding是cp936GBK而open()函数在Python 3里的默认编码取决于locale通常也是cp936。按说读取GBK文件再用GBK打印应该没问题但为什么乱码呢第三步也是最关键的一步我在data.txt的十六进制字节里发现了一个细节。文件开头有两个字节FF FE这是UTF-16 LE的BOM。也就是说这个文件实际上是以UTF-16编码保存的但被误命名为.txt。之前的分析基于“GBK文件”的假设全都不成立这就是典型的“错误假设导致误判”。6.3 解决与验证查明是UTF-16 LE编码后处理就非常简单了with open(data.txt, r, encodingutf-16) as f: for line in f: print(line.strip())Python内置的utf-16编码会自动读取BOM并判断字节序。运行后终端输出正常。这个案例给我们的核心启示是排查乱码时第一步永远是确认原始字节流的真实编码而不是先去改显示终端或者换编辑器。可以用chardet检测也可以用十六进制查看器看BOM甚至可以打开文件看一眼有无明显的编码特征。把这一步做扎实后面的一切都会顺理成章。7. 日常开发中减少乱码的几条实用经验踩过太多坑之后我慢慢总结出了一些方法论不算高深但确实让乱码频率降到了接近零。第一条统一字符编码为UTF-8并且把这当作团队协作的硬性约定。项目里的源码、配置文件、文档、脚本全部用UTF-8 without BOM保存。在Windows上开发跨平台部署时这一条能免掉一半的乱码问题。第二条在代码里显式指定编码。open()留白让Python猜默认编码看起来简洁实际是埋雷。open(filename, encodingutf-8)多写几个字符换来的是确定性。读取外部文件时如果不能确定编码先检测再处理。在处理网络响应时设置resp.encoding不要直接依赖猜测。第三条在Windows上把系统级编码统一设置为UTF-8。Windows 10的高版本系统支持“使用Unicode UTF-8提供全球语言支持”这一选项打开之后系统区域、记事本、PowerShell的默认编码都会切到UTF-8。但要注意这个设置会改变很多老软件的默认行为有兼容性风险建议在开发机上谨慎操作或者只针对开发环境使用。第四条面对乱码先复制粘贴原始字节不要凭肉眼看。肉眼看到乱码的时候信息已经丢了。正确的做法是把乱码内容复制出来用Python脚本或者xxd、hexdump这类工具查看十六进制再结合chardet判断编码。这一步能够帮你少走很多弯路。第五条关于print和日志尽量通过logging模块输出到文件而不是依赖print到控制台。logging的FileHandler可以显式指定encodingutf-8这样日志内容永远是确定的编码。而控制台print受终端环境影响太大排查起来很费劲。如果只是临时调试print没问题一旦涉及正式程序的输出就认真用logging。我在实际项目中体会最深的一点是乱码问题绝大多数时候不是程序写得不对而是程序对“外部环境”的错误假设。你假设文件是UTF-8假设终端是GBK假设网络响应是UTF-8……每一个假设都有可能出错。而解决乱码的根本思路就是把所有假设变成显式的、可验证的设置。显式指定编码明确读取来源的编码用工具检测真实编码排查时从原始字节出发。这套方法论一旦建立不管以后遇到什么新场景你都能快速定位问题而不是靠猜。
返回列表