最近在弄一些数据处理, 用python生成csv文件, 但是excel每次打开都是乱码, 后来查到excel并不支持UTF-8编码, 必须转换成其他编码才能解析, 其实python的编码问题也是每个人都会碰到的问题, 以前也是一知半解, 所以今天特地总结一下, 一些基础知识也不赘述了, 主要是了解以下几个问题:

  1. 编辑器如何知道文件编码的?(限于linux或者mac系统, windows会在文件系统保存文件编码的信息)
  2. python编码的内部机制

1 编辑器如何知道文件编码

这里需要了解一下unicode是一套标准, 相当于一个map, 一个unicode编码对应一个唯一的字符, 而像UTF-8是一种编码, 相当于一套映射f(unicode) = UTF-8.

具体的假如我们有一个只保存test字符串的文件, 看一下在不同编码下的二进制字节

UTF-8
74 65 73 74

UTF-8 with BOM
EF BB BF 74 65 73 74

UTF-16 LE
74 00 65 00 73 00 74 00

UTF-16 LE with BOM
FF FE 74 65 73 74

UTF-16 BE
00 74 65 00 73 00 74 00

UTF-16 BE with BOM
FE FF 00 74 65 00 73 00 74 00

这个BOM就是定义大小端问题而产生的, 所以系统可以通过BOM的值来确定大小端的问题.

而为什么会有大小端的问题呢, 就是因为UTF编码的不同方式.

这个不在赘述UTF如何编码了, 重点在我们平常的文件格式一般是UTF-8, 而编辑器读取我猜测是与系统环境变量有关的, 比如我的系统环境默认是UTF-8, 所以编辑器会以UTF-8的编码格式打开文件(这里我测试了下, 如果代码保存成UTF-16的编码的话终端显示为乱码)

剩下的问题就是为什么UTF-8格式没有大小端的问题?

UTF-8的格式如下:

0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

可以看出来UTF-8的规律, 如果首字节以0开头,肯定是单字节编码,如果以110开头,肯定是双字节编码,如果是1110开头,肯定是三字节编码,以此类推. 所以在读取文件的时候通过头位(0, 110, 1110)就可以判断字符了, 不用大小端的标记了, 而UTF-16字符就不一样了因为74 0000 74无法判断了.

2 python编码机制

python的编码机制总是默认以ascII码来解析的, 并把它转化为unicode.

当我们正确的得到文件二进制形式的时候(比如74 65 73 74)会以ASCII码的形式转换为unicode, 因为UTF-8的编码兼容ASCII, 所以74 65 73 74转化为unicode的时候依然为74 65 73 74, 但是当我们在python文件中(不管是注释还是字符串里)加入中文字符(随便举个栗子E6 B1 11), 这里E6 B1因为都不在ASCII范围内所以会引起如下喜闻乐见的异常:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

还记得通常我们在头部添加的#coding=utf-8吗, 它就是要告诉python解析器不要用ASCII解码, 而是用UTF-8的编码来解码.

3 栗子

以下程序会报错

# coding=utf-8
s = u'我'
print s

output:
UnicodeEncodeError: 'ascii' codec can't encode character u'\u6211' in position 0: ordinal not in range(128)

这是为什么呢? 因为虽然我们定义了文件的UTF-8, 但是当python碰到u这个转义字符的时候依然会用默认的ASCII编码方式转换为unicode, http://blog.csdn.net/liuxingen/article/details/48930527 这里有详尽的解释.

也就是说整个文件已经被翻译成unicode了, 但是字符串字面值会被转换成Unicode来做语法分析,然后在解释开始之前被转换回它们初始的编码。也就是说在解析print的字符串之前会转换为默认(UTF-8)的形式, 但是因为sys.getdefaultencoding(), 也就是说解析器会把UTF-8编码转为unicode, 可以知道默认编码仍为ASCII, 所以会报错.

# coding=utf-8
import sys
print sys.getdefaultencoding()
reload(sys)
sys.setdefaultencoding('utf-8')
print sys.getdefaultencoding()
print u'我'

output:
ascii
utf-8

参考链接:

  1. 为什么UTF-8没有字节序问题?
  2. Python中的编码
  3. PEP 0263 -- Defining Python Source Code Encodings
  4. 文件编码格式
  5. mac, linux, windows的文件系统都用什么编码保存和读取文件名?