文章目录
平常主要是写C++,但有时候也需要用到python、shell等脚本来做一些事情,其中最常见的一种应用就是文本处理,而要处理文本不可避免地就要和编码打交道,python3和python2相比在编码方面已经有了很大的进步,但有时候还是会碰到相关问题,以前没怎么特别关注过这块,都是直接用,出了什么编码问题拿报错信息一搜照着改改就完事了,但没有彻底理解很容易走弯路,最近又碰到了一次编码问题,于是最近好好整理学习了下,实际上Python3编码相关的问题也分为好多类,这里就完整地总结一下python3里面最核心的编码相关概念和问题。
一、字符串编码和解码的基本概念
编码这个词搞计算机的人肯定都不陌生,这里讨论的编码特指字符串的编码解码,撇开特定计算机语言,通俗的来说字符串的编码和解码就是字符串和二进制字节串(以下用bytes代替,bytes也是python3中的一种类型)的互相转换,而我们常说的gb2312、utf-8等编码类型则是规定了不同的转换规则。
1.编码:指定编码类型将一个字符串转换成bytes。比如我们将字符串写入文件就需要写入bytes,因此需要编码。
2.解码:指定编码类型将一个bytes转成字符串。
总的来说,我们所说的编码问题本质上都是各种原因造成的编码和解码所用的编码类型不一致所引起的,要保证编码解码不出问题,核心就只有一点:
在对bytes解码的过程中使用和当初它编码时使用的相同编码类型,也就是控制编码解码所用的编码类型一致。
二、Python3各编码解码场景的编码类型一致性保证
只要是涉及到字符串(文本)的场景就避免不了编码解码,具体到python3这个场景里,个人认为可以分为三大类:
- 使用bytes.decode和str.encode函数手动操作bytes、字符串对象:这里涉及到的编码类型都可以直接指定。
- 执行过程读取脚本文件:Python是解释型的脚本语言,需要在执行的过程中不断地读取代码,在这个过程中就会有解码的问题,比如我们直接定义了一个字符串a=‘字符串’,这里的中文就涉及到编码解码,我们用编辑器写代码保存到文件里的本质就是使用某种编码类型进行编码后将bytes写入到文件,python3解释器在读取相应文件的时候也需要用相同的编码类型进行解码才能保证正确性。
- 文本的输入输出:也就是所谓的文本io,Python3里的的io包括三种主要类型: text I/O, binary I/O 和 raw I/O,其中text I/O就需要和编码解码打交道,我们操作的就是文本流,其中就包括我们熟悉的通过open打开文件,以及stdin和stdout的输入输出流。对于文本流,读取本质上就是将从流里获得的的字节码解码成字符串,而写入本质上就是将字符串编码成bytes写到流里。
在具体分析各个场景之前我们先了解下一些python3编码解码相关的默认值,在很多场景如果我们不指定都会使用默认值,因此有必要了解这些默认值在我们执行脚本的环境里是什么:
#python3解释器系统的默认编码
print(sys.getdefaultencoding())
#获取操作系统的locale配置,linux下和shell的locale命令一致。
#python也提供了一些api修改locale,这里不展开,有需要可以自行查阅文档。
print(locale.getdefaultlocale())
下面我们就来看看这几类场景如何保证编码解码所用的编码类型一致。
2.1 字符串和bytes之间转换
python3直接提供了编码解码函数用于字符串到字节码之间的互相转换,如下:
1.编码:str.encode(encoding=‘UTF-8’,errors=‘strict’),返回bytes 对象
2.解码:bytes.decode(encoding=“utf-8”, errors=“strict”),返回字符串
函数很好理解,这里就不多展开了,一句话总结就是指定编码类型对字符串进行编码得到bytes、对bytes进行解码得到字符串,二者均可明确指定编码类型,不太容易出错。
2.2 执行过程读取脚本
这个场景的编码解码涉及到以下两点:
- 编辑器在保存源码文件时编码所用的编码类型。
- python3解释器在读取源码文件时解码所用的编码类型。
对于编辑器保存时候的编码不用多说,都可以设置。对于后者,默认是使用sys.getdefaultencoding()所指明的编码类型,这个编码类型在linux平台下就是utf-8,如果我们想手动指定,可以通过在脚本开头添加#coding=xxx来指定,有一点需要特别注意,这个仅仅影响解释器读取脚本过程中的解码,很多人会误以为还会影响其他地方的编码设置。
2.3 读写文本文件
python经常用于处理文本,最常见的一种场景就是使用open打开文件从文件输入或者输出到文件,无论是读还是写,我们在打开文件的时候都可以用encoding指定编码类型,比如读取
with open('filename', 'r', encoding='utf-8') as file:
for line in file:
do sth
写入
with open('filename', 'w', encoding='utf-8') as file:
f.write(xxx)
print("字符串", file=f)
通过指定相同的encoding参数就可以保证一致,如果是读取外部生成的文件,也只需知道其编码便方便地指定。需要注意的是如果我们不指定,会使用和locale.getdefaultlocale()一致的编码。
2.4 文本流输入输出
操作标准的文本输入输出流本质上和操作文件是一样的也是一样的,比如我们常见的读取stdin
for line in sys.stdin:
do sth
但stdin却不像文件一样可以直接显示指定编码类型,如果碰到了相关的编码问题相对更容易让人疑惑,对于stdin、stdout、stderr,python3有一套默认的编码选择规则,比如python 3.7的官方文档解释如下:
可以看到,对于linux,默认使用的是locale encoding,如此一来,对于locale encoding不是utf-8的linux环境就很有可能出问题,毕竟现在我们保存文件最常用的编码就是utf-8,比如hadoop streaming场景,如果locale encoding不是utf-8,但是用于cat的文本文件是utf-8的,就会导致使用非utf-8编码类型去解码utf-8编码出来的字节码从而产生问题。
对于此类问题主要有以下几个方法可以解决:
- 一个很直接的方法是修改locale encoding,但很多时候执行python3脚本的机器我们并没有权限来做这件事情,而且修改这个影响面太大风险也很大,不是很推荐。
- 指定上面文档中提到的PYTHONIOENCODING环境变量,对于这个变量官方解释如下:
也就是在执行脚本前设置好即可指定stdin/stdout/stderr所用的编码类型,如下:
export PYTHONIOENCODING=utf-8
- 用TextIOWrapper指定编码来读取sys.stdin内部的raw buffer:
import io
import sys
input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
- Python 3.7以及之后的版本也可以在发生实质的read之前使用reconfigure来重新指定IO编码类型:
sys.stdin.reconfigure(encoding='utf-8')
三、总结
在碰到python3的编码问题时,首先要做地就是分析具体是哪块的编码解码不一致,进而针对性地修改解决,而不是用笼统的“python3编码问题”一通搜索修改,还是要理解真正的具体原因,这样才能够快速准确地解决问题。
参考:
1.python-3-how-to-specify-stdin-encoding
2.吐血总结,彻底明白 python3 编码原理