ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python字符编码全解:从str与bytes到乱码排查

Python字符编码全解:从str与bytes到乱码排查 先问一个最直接的问题你有没有在Python里print一句中文结果直接崩出一个UnicodeEncodeError或者读一个文件时看到UnicodeDecodeError: utf-8 codec cant decode byte 0xd1 0xa7 in position 0: invalid continuation byte如果你见过说明你已经和Python字符编码正面交锋了。这个主题是Python初学者最容易栽跟头的地方也是我早期写爬虫、处理日志、做数据分析时被折磨得最惨的一关。字符编码并不是Python独有的问题但Python的字符串模型、文件读写方式、终端输出机制让这个问题变得特别容易暴露出来。这篇内容不讲空话我会从编码的底层原理讲起然后落到Python 3的str与bytes、encode/decode、文件读写、终端乱码、爬虫解码和数据库乱码这些具体场景最后把我这几年踩坑总结的排查思路和规范全部摊开给你。适合刚学Python不久、被乱码搞到崩溃的新手也适合写过一阵子代码但从来没系统梳理过编码问题的同学。看完之后你至少能搞清楚乱码是怎么产生的、报错信息到底在说什么、以及遇到任何编码问题该从哪里下手。1. 字符编码到底在解决什么问题1.1 从ASCII说起计算机只认字节不认字符计算机底层只有0和1所有数据存储和传输最终都是字节。我们看到的字母、汉字、标点在计算机内部必须映射成数字这个映射规则就是字符编码。最早普及的编码是ASCII它用7个比特表示128个字符包含大小写英文字母、数字、常见符号和控制字符。因为英文只有26个字母加上符号和数字128个码位完全够用。但中文显然不可能用128个码位解决。汉字有几万个一个字节只有256种可能放不下。于是中国制定了GB2312、GBK、GB18030这些编码方案用两个字节甚至更多字节来表示一个汉字。日本有Shift_JIS韩国有EUC-KR每个国家和地区都搞了一套自己的编码规则。这就带来一个严重问题同一串字节用不同编码去解释得到的是完全不同的字符。比如字节0xd1 0xa7按GBK解释是“学”按Shift_JIS解释就是别的字符。这个阶段我用一个类比来帮你理解编码就是“翻译词典”。你拿中文词典去查英文单词查出来的意思大概率是错的。字符编码乱码的本质就是拿错了词典去翻译字节。1.2 Unicode把全球字符装进一张大表各个地区各自为政的编码方案导致跨语言、跨平台的文本交换极其痛苦。Unicode的出现就是为了解决这个问题它给世界上几乎所有文字系统的每个字符分配一个唯一的数字编号这个编号叫码点code point写作Uxxxx的形式。比如汉字“学”的码点是U5B66英文字母“A”的码点是U0041。这里要特别区分一个概念Unicode是字符集不是编码规则。它只确定了“每个字符的编号是多少”但没有规定“这个编号用几个字节、怎么存”。把码点变成字节的过程才叫编码比如UTF-8、UTF-16都是Unicode的具体编码方案。我举个例子字符串A学Unicode码点分别是U0041和U5B66。这串字符在内存里到底存成什么字节取决于你用哪种编码方案。UTF-8会把A存成1个字节0x41把“学”存成3个字节0xE5 0xAD 0xA6。同样是这串字符用GBK编码“学”就变成2个字节0xD1 0xA7。所以同样的字符串编码方案不同最终字节序列就不同反过来同样的字节序列解码方案不同得到的字符串就不同。1.3 UTF-8、UTF-16、GBK怎么选UTF-8是当前互联网的主流编码它的核心特点是变长编码。ASCII范围内的字符用1个字节表示和传统ASCII完全兼容欧洲文字大多用2字节常用的中、日、韩文字用3字节更生僻的字符用4字节。这种设计让英文文本的体积很小同时又能覆盖Unicode全部码点。UTF-8的具体编码规则其实不复杂码点在U0000到U007F之间用1字节格式是0xxxxxxx码点在U0080到U07FF之间用2字节格式是110xxxxx 10xxxxxx码点在U0800到UFFFF之间用3字节格式是1110xxxx 10xxxxxx 10xxxxxx码点大于UFFFF的用4字节格式是11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。拿“学”字验证一下U5B66的二进制是0101 1011 0110 0110共16位落在3字节区间按模板填充得到1110 0101 1010 1101 1010 0110也就是0xE5 0xAD 0xA6和实测一致。GBK则是固定双字节编码对中文来说存储效率比UTF-8高但只覆盖中文字符集遇到生僻字或表情符号就无能为力了。当前项目的通用原则很简单面向存储、传输、跨平台的场景无脑选UTF-8面向Windows本地老软件、Excel直接打开CSV这类场景才需要考虑GBK或带BOM的UTF-8。2. Python里的编码模型str与bytes的分界线2.1 Python 3的str是Unicode字符串bytes才是原始字节Python 3在设计上做了一个非常彻底的改动str类型表示的是Unicode字符串它内部存的是码点序列和你用的是什么编码方案无关bytes类型表示的是原始字节序列它只是一堆0到255之间的整数。这两个类型有天壤之别不能混用。我做一个小实验你马上就明白s 学 print(type(s)) # class str print(len(s)) # 1 b s.encode(utf-8) print(type(b)) # class bytes print(len(b)) # 3同一个字符str的len是1因为它是一个字符bytes的len是3因为UTF-8编码后占了3个字节。你拿着str去做文件写入、网络传输Python必须先把str编码成bytes反过来你从文件、网络、串口拿到bytes也必须先解码成str才能做文本处理。Python 2时代的str是字节串unicode是字符串两者混用时会偷偷做隐式转换经常出现“能跑但不一定对”的诡异现象。Python 3把这个模糊地带彻底砍掉了str和bytes运算直接抛TypeError逼着你明确表达自己到底在处理文本还是处理字节。我当时从Python 2迁移到Python 3最强烈的感受就是运行时报错变多了但乱码和逻辑错乱反而变少了。这类报错不是麻烦是在帮你暴露问题。2.2 encode/decode唯一的一座桥str和bytes之间只有一种转换方式str.encode()把字符串编码成字节bytes.decode()把字节解码成字符串。这两者互为逆操作。raw 你好Python data raw.encode(utf-8) print(data) # b\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8cPython text data.decode(utf-8) print(text) # 你好Python编码格式必须对应用什么编码就要用什么解码。你好Python.encode(utf-8).decode(gbk)的结果大概率是乱码或者直接报错。这里有个很多人没意识到的细节encode和解码时如果不传参数Python 3默认使用UTF-8。这在绝大多数Linux、macOS环境下没问题但在Windows环境下默认编码可能不是UTF-8就容易出幺蛾子。我在处理爬虫数据时经常遇到这种情况网页的Content-Type里写着charsetgbk但requests库默认按ISO-8859-1或者utf-8去处理结果就是满屏乱码。正确的做法是先拿到原始字节再按实际字符集解码绝不依赖对方声称的编码也不依赖自己的猜测。2.3 新版Python安装后的默认编码在变好说到“python安装”这个热搜词我必须提醒一点很多新手的第一个字符编码坑其实在安装Python那一刻就埋下了。Windows下安装Python时安装界面会有一堆勾选项比如“Add Python to PATH”“Associate files with Python”。如果你没把Python加到PATH后面在命令行输python直接提示找不到命令更不可能去配置环境变量这会直接影响后续处理编码问题的手段。更重要的是Python本身的默认编码策略。Python官方在持续推进“默认UTF-8”的方向。PEP 540引入了UTF-8 ModePython 3.7以上可以通过-X utf8命令行参数或环境变量PYTHONUTF81开启开启后Python在Windows下会把默认文本编码、标准输入输出编码全部切到UTF-8。Windows控制台如果再配合chcp 65001和TrueType字体中文输出就很少出乱码了。如果你用的是比较新的Python版本比如3.11、3.12源码文件的默认编码已经是UTF-8不再需要# -*- coding: utf-8 -*-这种声明。但旧代码、旧项目里可能还留着这种文件头声明这是为了保证在Python 2和早期Python 3环境下的兼容性不影响正常使用。我的建议是新项目直接用Python 3.12以上代码文件统一UTF-8Windows下如果频繁遇到控制台乱码就把PYTHONUTF81配到系统环境变量里。这是成本最低、收益最明显的根治法之一。3. 核心实操绕不开的encode/decode与文件读写3.1 看懂UnicodeDecodeError和UnicodeEncodeErrorPython报出的编码相关错误主要有两类。第一类是UnicodeDecodeError发生在你试图用某种编码去解码字节但字节序列不符合这种编码的规则时s 中文 data s.encode(gbk) data.decode(utf-8) # UnicodeDecodeError: utf-8 codec cant decode byte 0xd4 in position 0: invalid continuation byte0xd4是“中”字GBK编码的第一个字节按UTF-8的规则它不是合法的起始字节所以解码失败。第二类是UnicodeEncodeError发生在你试图把字符串编码成目标编码但目标编码的字符集不包含这个字符时s 中文 s.encode(gbk) # UnicodeEncodeError: gbk codec cant encode character \U0001f600 in position 2: illegal multibyte sequenceGBK里没有emoji强行编码就会炸。这类错误信息其实已经把原因说得很清楚了新手第一反应是找“怎么忽略这个错误”但要我说报错不是坏事它逼着你去搞清楚数据和编码的真实状态。调试时有两个API很有用ord()可以查看单个字符的码点比如ord(学)返回23398转成十六进制是0x5b66bytes.hex()可以把字节序列转成十六进制字符串方便你肉眼检查数据到底是什么。我排查乱码的第一步永远是先看字符的码点再看字节的十六进制以此判断是数据源头就错了还是某个环节用错了编码。3.2 open打开文件时永远显式声明encoding文件读写是字符编码问题最常见的爆发点。Python内置的open()函数在文本模式下需要指定编码如果不指定就依赖系统默认编码。Linux和macOS一般是UTF-8Windows一般是GBKPython 3.15之前。这就导致同一个脚本在Linux上跑得好好的拿到Windows上就乱码或者直接报错。正确写法是永远显式声明encoding参数# 写入文件 with open(data.txt, w, encodingutf-8) as f: f.write(中文内容) # 读取文件 with open(data.txt, r, encodingutf-8) as f: content f.read() print(content)如果你不确定一个文件到底是什么编码有一种相对稳妥的探测方式是使用第三方库charset-normalizer或chardet。它们会根据字节分布推断最可能的编码import charset_normalizer with open(unknown.txt, rb) as f: raw f.read() result charset_normalizer.from_bytes(raw).best() print(result.encoding)但这类工具只能给参考不能保证100%准确。最可靠的办法还是从源头确定编码文件是谁生成的生成时用的什么编码如果是你的同事给你一个CSV文件最有效的做法是直接问对方“这个文件导出的编码是什么”这比任何探测工具都靠谱。还有一类隐藏很深的坑是BOM。UTF-8文件带不带BOM文件头那三个字节EF BB BF在很多解析器眼里是有区别的。Python的utf-8编码不会自动去掉BOM读出来的字符串开头会多出一个\ufeff字符肉眼看不见但会导致比较、拼接出错写入时默认也不写BOM。针对这个情况Python提供了utf-8-sig这个编码读取时自动跳过BOM写入时自动加BOM。如果你的CSV文件要交给Windows上的Excel直接打开写入时用encodingutf-8-sig是最省心的方案。3.3 终端print中文乱码Windows控制台的编码问题这是一个极其经典、几乎每个Windows下写Python的人都会踩的坑代码、文件都是UTF-8print中文到控制台却变成乱码或者直接抛UnicodeEncodeError。原因在于Windows控制台默认代码页是GBK代码页936而Python 3在Windows上默认的输出编码曾经是控制台的代码页也可能受到环境变量影响。当你print的字符在GBK里没有对应编码时比如emoji就会直接炸。现在的Python版本其实已经做了很多改进Python 3.6以上在Windows上已经能更好地处理控制台编码但依然不是万无一失。如果你需要临时在脚本里强制标准输出使用UTF-8可以这样写import sys sys.stdout.reconfigure(encodingutf-8)注意reconfigure()是Python 3.7才有的方法。如果你用的操作系统控制台本身不支持UTF-8显示改Python侧的输出编码也没用还得把控制台代码页切到UTF-8命令行执行chcp 65001再把字体设置为TrueType字体。另一个更彻底的办法是在环境变量里设置PYTHONIOENCODINGutf-8这样Python的标准输入输出都会强制使用UTF-8不管控制台代码页是什么。我个人的建议是日常写脚本不要把终端输出编码当成一个需要硬编码的环节。如果只是分析数据、打印结果建议直接写到UTF-8的日志文件或CSV文件里再用电编辑器打开查看绕开控制台这个不稳定因素。3.4 爬虫和HTTP响应的字符集识别做爬虫时编码问题几乎是必考的。requests库拿到的resp.text是已经解码好的字符串它会优先使用HTTP响应头里的Content-Type字段指定的charset没有的话就用apparent_encoding或默认编码。但很多老网站的响应头写得不对或者干脆没有charset导致resp.text出现乱码。我处理这种问题有一个固定套路先拿原始字节再自己判断编码最后手动解码。import requests resp requests.get(https://example.com) # 先看响应头声明的编码 print(resp.encoding) # 拿原始字节自己判断 raw resp.content print(raw[:50])要看raw的十六进制和内容判断它到底是UTF-8还是GBK还是其他编码。如果你的目标是现代主流网站优先试UTF-8如果是老网站、政府网站、一些数据库导出的查询页面优先试GBK。判断依据也很简单如果一串字节在UTF-8解码时连续出现多个生僻字符或者直接报错那大概率是GBK。另外HTML页面本身会在meta charset...里声明编码这个值有时和HTTP响应头不一致。以哪个为准没有绝对答案取决于服务器实现。真遇到了我的做法是把两种都试一遍看哪种解码出来的人类可读文本更多。整个过程不靠猜而是靠观察字节特征和试解码结果做判断。4. 常见问题与排查技巧实录4.1 高频报错速查表我把平时最常见的报错和场景整理成一张表方便你直接对照排查报错/现象常见原因处理办法UnicodeDecodeError: utf-8 codec cant decode byte ...读取文件或数据时数据不是UTF-8编码确认数据真实编码改用正确编码解码UnicodeEncodeError: gbk codec cant encode character ...print或写入GBK环境时遇到GBK不支持的字符设置PYTHONIOENCODINGutf-8或改用UTF-8写入TypeError: a bytes-like object is required, not str把str传给只接受bytes的接口先执行str.encode()转成bytesTypeError: can only concatenate str (not bytes) to strstr和bytes直接拼接统一类型后再操作中文变成中文UTF-8字节被GBK解码后又按UTF-8显示数据本身是UTF-8先用UTF-8解码原始字节中文变成涓枃GBK字节被UTF-8解码用GBK解码原始字节print中文乱码Windows控制台代码页与输出编码不一致chcp 65001或设置PYTHONIOENCODINGutf-8表格里第三行和第四行虽然都是类型错误但方向相反一个是你要传str却给了bytes一个是拼接时两边的类型对不上。很多新手一看到TypeError就懵其实看后半句就能定位是哪边的问题。4.2 静默乱码最阴险的一类问题比报错更可怕的是不报错但内容错了这种“静默乱码”会让人完全摸不着头脑。典型场景是一串UTF-8字节被GBK解码后碰巧没有任何解码错误但显示的字符完全不对比如UTF-8的“中文”编码成0xE4 0xB8 0xAD 0xE6 0x96 0x87按GBK解码得到“涓枃”。这个过程不会抛任何异常你拿到的字符串长度、类型都是正常的但内容已经错了。这种情况最坑的地方在于你以为数据是好的结果做字符串匹配、正则提取的时候全部落空。排查这种问题一定要有“怀疑原始字节”的敏感度。我遇到过好几次“数据莫名其妙对不上”的案例最后发现都是某个中间环节把编码转错了。解决方向是把出错环节的数据导出来看它的十六进制人工判断它是哪一层编码的产物。另一个常见静默乱码出现在终端日志文件里。程序在Windows上以GBK输出日志日志文件被拷到Linux上按UTF-8打开看到的往往是乱码。如果日志文件要跨平台共享请在程序里统一用UTF-8编码日志如果历史日志已经是GBK用各种文本编辑器打开时手动选择GBK编码。4.3 数据库与CSV的中文乱码数据库连接字符串里charset参数是中文是否乱码的关键。以MySQL为例连接时设置charsetutf8mb4读写基本不会出问题。但要注意MySQL的utf8和utf8mb4是两码事utf8最多用3字节存储存不了4字节的emojiutf8mb4是真正的完整UTF-8支持。如果你的表里要存用户昵称指不定哪个昵称里就带着emoji不设置utf8mb4就会在写入时报错。连接代码大致是这样import pymysql conn pymysql.connect( host127.0.0.1, userroot, passwordpassword, databasetest, charsetutf8mb4 )但连接参数只是最终一环。数据库表本身的字符集、字段的字符集、服务端的character_set_server配置任何一个环节不匹配都可能出问题。排查思路是从客户端到服务端逐层确认连接字符串的charset、数据表的DEFAULT CHARSET、字段的CHARSET。CSV和Excel是另一个高频场景。直接用Excel打开UTF-8编码的CSV经常看到乱码因为Excel默认按GBK中文Windows去解析CSV。解决办法是写CSV时用encodingutf-8-sig让文件带上BOM标记import csv with open(data.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([姓名, 分数]) writer.writerow([张三, 95])加上BOM之后Excel打开时能识别出这是UTF-8编码的文件中文就能正常显示。这个技巧非常实用尤其是做数据分析、报表导出、给非程序员同事交付文件的时候。4.4 一套通用的排查思路面对任何字符编码问题我都有一个固定的排查流程按这个顺序来基本都能定位到问题在哪一层确定出问题的数据是什么形式是str还是bytes用type()确认。如果是str检查它的码点是否合理打印[hex(ord(c)) for c in text[:20]]看码点范围和预期是否一致。如果是bytes看它的十六进制内容打印data[:20].hex()判断它最可能属于哪种编码。比如看到e4 b8 ad开头基本就是UTF-8看到d6 d0开头大概率是GBK。在出现乱码的边界处打印数据文件读取后、网络请求后、数据库查询后每个环节都打印一份数据快照对比是在哪个环节开始变质的。根据对比结果在当前环节显式指定正确的编码继续往下追踪。这套流程的核心思路是编码问题不要靠肉眼看乱码猜要靠十六进制数据做判断。乱码肉眼看只能分辨“像不像中文”但十六进制能告诉你字节的真实组成帮你还原数据到底经历了什么。5. 我在实际项目中沉淀的统一编码规范5.1 写代码时固定几个原则这几年做了大量数据处理和爬虫项目我总结出几条写代码时的硬性规范基本能消灭90%的编码问题源码文件无条件统一UTF-8。新项目直接用Python 3.12以上源码文件本来就是UTF-8不需要额外处理。不需要在文件头部写# -*- coding: utf-8 -*-除非你要兼容老环境。所有open()的文本模式读写一律显式传encodingutf-8不依赖系统默认值。这个习惯能避免同一套代码在不同操作系统上行为不一致。所有对外交互的边界都做显式编码转换。从文件读、从网络收、从数据库取都是“字节进入程序”的边界向文件写、向网络发、向数据库存都是“字节离开程序”的边界。在这些边界显式声明编码程序内部则统一使用str。这样即使外部格式千奇百怪内部的数据流永远是干净的Unicode字符串排查问题时只需要盯着边界。这个思路有点像维护一套标准的“内部货币”外面不管是美元、日元还是人民币进入系统都先兑换成统一货币离开时再换回去。货币兑换自然会损失一些效率但换来的是内部逻辑的确定性和可维护性。5.2 推荐的调试工具与技巧遇到编码问题单个print往往不够直观。我常用的调试工具和手段有这几个repr()函数比print()更可靠因为它会把字符串里的转义字符显示出来。repr(中文\u5b66)和直接print(中文)在正常环境下看不出区别但遇到不可见字符、BOM、零宽字符时repr()能把问题暴露得很清楚。写一个小脚本专门用来检测文件编码。不需要写得多复杂就是把文件以二进制模式读进来打印前若干个字节的十六进制再尝试用UTF-8和GBK分别解码看哪个能成功、结果是否合理。with open(target.txt, rb) as f: raw f.read(100) print(raw.hex()) for enc in [utf-8, gbk]: try: print(enc, raw.decode(enc)) except UnicodeDecodeError: print(enc, decode failed)查看环境变量的默认编码也很有用import sys, locale print(sys.getdefaultencoding()) # 默认字符编码通常是 utf-8 print(sys.stdout.encoding) # 标准输出实际使用的编码 print(locale.getpreferredencoding(False)) # 系统偏好编码如果你在Windows下调试sys.stdout.encoding会告诉你Python认为自己应该用哪种编码往控制台输出。如果这里显示cp936你print中文遇到问题就不奇怪了。你可以通过设置PYTHONIOENCODINGutf-8来改变它也可以临时在代码里调用sys.stdout.reconfigure(encodingutf-8)。但真正常用的场景我会把调试的重心放在文件上“把数据写进UTF-8的临时文件再用编辑器查看”这样能绕开控制台产生的所有编码干扰。5.3 最后再分享一个小技巧在处理多来源文本数据时一个常见需求是“把这些数据全都清洗成一致的编码再入库”。我会在入库前做一次强制转换也就是把不确定来源的str先编码成UTF-8的bytes再解码为str这一步能拦截大量非法字符和无效代理项def normalize_text(text: str) - str: if isinstance(text, str): return text.encode(utf-8, errorsignore).decode(utf-8) return texterrorsignore会绕过无法编码的字符如果这不符合需求可以换成errorsreplace把无法处理的字符替换成占位符?。这样处理之后文本数据至少是合法的UTF-8字符串后续的匹配、存储都不会再因为编码问题崩溃。这套方法不复杂但它逼着你在一开始就把“编码边界”想清楚。字符编码这个主题说到底是三个问题的答案数据现在是什么形态目标是什么形态用什么规则做转换。把这三个问题想明白你不仅能解决乱码还能从根本上理解为什么Python会这样设计str和bytes。我从一个被unicode和str搞到怀疑人生的新手到现在顺手就能排查编码问题靠的也只不过是把原理和边界摸透了。希望这篇内容能帮你少走点弯路。
返回列表