Unicode之殇
计算机数据存储传输与其数据含义—字节(byte)
乱码是所有程序员都会遇到的问题,这其中涉及到一个计算机的大学问–编码。始终要记住的是,计算机中的所有数据都是字节(byte)。字节本身并没有意义,除非赋予它某种意义,同一个字节数组可以被解释成不同的字符串,同一个字符串也可以存储成不同的字节数组。
由于计算机起源于美国,所以早期为了能够在计算机中存储英文字母,就建立了ASCII码,建立起 字符<->字节 的映射。但是后来256个字符已经不足以表示世界所有的字符,各国都使用了自己的编码方式,比如big5 GBK latin等,这样的后果是不同国家之间传输的字节序列都不能相互认识,需要一个大一统的编码方案,因此就出现了Unicode。
这里大部分文献都说Unicode是一套标准,不是具体实现。其实就是Unicode制定了一个参考表,这个表把所有的字符都包含进来,给每一个字符一个数值(code point)称之为代码值。就形成了一个 字符<->整数 的映射表,供所有国家和开发者参考。如果你要设计一套编码规则能够识别所有字符,那么你就可以参考这个映射表来做,也就是你的编码方案可以识别所有的字符。Unicode具体实现例如utf8,就是采用的变长编码,分别用1/2/3个不等长的字节表示一个字符,比如在Unicode表中,汉字“田”的code point是\u7530,对他的utf8实现是0xE7 0x94 0xB0三个字节。具体关于utf8以及各种常见编码如GBK的编码规则,可以参考这篇文章http://www.searchtb.com/2012/04/chinese_encode.html
Windows下的常见编码之ANSI、Unicode、Unicode Big endian、UTF-8
打开记事本,写入“严”,分别另存为这四种格式,然后再用UltraEdit打开,进入十六进制模式,就能看到文件实际存储的字节。
- ANSI:系统默认编码,一般中文系统就是GBK 0xD1CF
- Unicode:UTF-16 Little endian 0x4E25
- Unicode:UTF-16 Big endian 0x254E
- UTF-8: 0xE4 0xB8 0xA5
你可以到http://graphemica.com/%E4%B8%A5上进行查询验证
这里要注意的就是,文本本身的编码格式要和打开文本的软件使用的打开方式相互一致,否则就可能出现乱码。Windows平台下的文件一般会被加魔术数magic number,表明自己是什么格式。你看到UTF8前面的EF BB BF就表示是UTF8格式。
开发者工具流中的编码问题
在开发过程中,编码问题也无处不在。我们需要不断的变换看待代码的视角,程序员,编辑器,编译器/解释器,操作系统,程序本身,虚拟终端/控制台。
编辑器视角,注意代码编写<->编辑器的编码一致性。
- 你在写代码的时候,你的集成开发环境或者说代码编辑器支持什么编码格式以及当前使用的是什么格式,因为你写的代码对编辑器来说就是数据,他会以某种编码方式存储你的代码数据。
编译器解释器视角,注意代码文件<->编译器/解释器的编码一致性。
- 注意当你写完了代码,存为一个文件,此时,如果你使用的是编译型语言例如C语言,你需要把他作为数据给编译器例如GCC处理,此时GCC也要按照某种编码格式解析代码文件。另外,如果是Python这种脚本解释型语言,Python解释器也会以某种编码格式解析代码文件。
- 以Python为例,如果你的代码中申明编码是utf8,而你使用编辑器的默认编码却是gbk,即代码文件是以gbk存储的,那么Python解释器就会以utf8的方式读取你的gbk存储的代码,当代码中都只有ascii码的时候,不会出现错误,这仅仅时因为utf8和gbk的实现恰好都在0-128兼容ascii码,一旦你的代码中出现中文如’田’,他的gbk格式是0xcc0xef,Python解释器按照utf8并不认识。
程序自身视角,注意程序内部处理过程的编码一致性。
- 由于我们的程序本身也要处理数据,所以程序内部数据也是有编码格式的,特别是输入 处理 输出这三个过程中,你需要注意编码格式。程序的输入和输出都是字节,比如来自键盘或者网络字节流,然后你需要根据编码格式进行解码成你程序内部适合处理的格式。当你需要存储到磁盘或者传输到网络的时候,你需要编码成适合存储和传输的字节流。因此,所有通过网络传输的数据或者磁盘数据一般都会有附加的告知编码类型的元数据,例如HTTP头会有编码类型。
- 以Python为例,Python内部使用Unicode存储字符串和处理字符串(大部分采用utf16格式),如果你想按照逻辑意义上操作字符串(e.g. len() [:]),你必须要将输入的字节或者其他第三方库返回的字节数据encode成Unicode,然后处理,处理完成之后,再将Unicode字符串decode成相应编码的字节流。
操作系统视角,注意操作系统以及文件系统本身默认支持的编码。
- 由于绝大部分程序运行在操作系统之上,此时程序对于操作系统来说,就是一块二进制数据,操作系统将其动态执行过程抽象成进程process,虽然底层对程序透明,但是你需要熟悉操作系统以及文件系统支持的编码字符集合。比如你在新建一个文件的时候,文件名字的字符编码文件系统是否支持。并且文件系统对文件名一般都会有字符上的限制。
终端视角,注意对于交互式的程序,你也需要知道cmd或者shell默认支持的编码集合,以便于正确打印信息。
以上任意一个环节都有可能导致程序出现错误或者乱码,因此需要特别小心。分析出现问题的原因。
Python2 和 Python3的字符串编码问题
Python2
我们先来看看Python2对不同方式定义的字面字符串的处理模式:
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> str_byte = '田'
>>> type(str_byte)
<type 'str'>
>>> len(str_byte)
3
>>> str_byte
'\xe7\x94\xb0'
>>> repr(str_byte)
"'\\xe7\\x94\\xb0'"
>>> print(str_byte)
田
>>> str(str_byte)
'\xe7\x94\xb0'
>>> str_byte.__str__()
'\xe7\x94\xb0'
>>>
>>>
>>> str_byte.encode('utf-8')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe7 in position 0: ordinal not in range(128)
>>>
>>>
>>> str_b = b'田'
>>> type(str_b)
<type 'str'>
>>> len(str_b)
3
>>> str_b
'\xe7\x94\xb0'
>>> repr(str_b)
"'\\xe7\\x94\\xb0'"
>>> print(str_b)
田
>>> str(str_b)
'\xe7\x94\xb0'
>>> str_b.__str__()
'\xe7\x94\xb0'
在Python2中,直接定义一个字符串字面量,他的类型是type str,但这并不是逻辑意义上的字符串,因为很显然,按照程序逻辑上的意义,’田’的长度是1而不是3;
这说明Python2中的str类型实际上是byte-string,也就是单纯的字节数组对象,并且按照字节数组处理而不是逻辑意义上的字符串处理;
为什么这里Python2内部存储的str_byte的值是\xe7\x94\xb0,因为在你打出’田’字的时候,首先被输入法处理,此时输入法按照系统编码utf8(你可以在shell中使用env查看LANG的值)来存储’田’即\xe7\x94\xb0,然后送给Python2解释器和虚拟终端控制程序,Python2解释器就会拿到字节数组[s, t, r, _, b, y, t, e, ’ ‘, =, ’ ‘, \xe7, \x94, \xb0],对解释器来讲,这是一个字符串,Python2解释器按照默认编码sys.getdefaultencoding()=ascii解码成字符串,然后进行语法解析,知道这是字符串赋值语句,并把str_byte当成str对象来处理,内部存储为\xe7\x94\xb0;而虚拟终端控制程序直接以默认编码utf8方式看待\xe7\x94\xb0,解码是汉子’田’,找到显卡map的位图,然后将位图数据发送给显卡,显卡显示’田’。
另外,当再次输入str_byte,他本身其实会被解释为str(str_byte),而Python2中只有一个str(object=”)这个built in function,他又调用object.__str__()。(在Python3中,增加了str(object=b”,encoding=’utf-8’,error=’strict’),当没有encoding和error参数的时候,str(object)返回object.__str__();当有参数时,object需要是bytes-like object(e.g. bytes or bytearray));
再来看print(str_byte),print函数接受字节数组,然后按照系统默认编码打印出这个字符串,因为都是utf8,所以可以打印;
最后的str_b的加b前缀的写法和str_byte的不加前缀写法是等价的,都是byte-string。
下面看看__str__()和__repr__()的区别:
>>> str(777)
'777'
>>> repr(777)
'777'
>>>
>>> str('Python')
'Python'
>>> repr('Python')
"'Python'"
>>>
>>> eval('Python')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'Python' is not defined
>>> eval("'Python'")
'Python'
>>> eval(u'\u7530')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
田
^
SyntaxError: invalid syntax
>>> eval("u'\\u7530'")
u'\u7530'
>>> print(eval("u'\\u7530'"))
田
Python中str() print()都会调用object.__str__(), repr()会调用object.__repr__();
下面是__str__()和__repr__()的区别:
- __repr__() goal is to be unambiguous for computer and programmer,which python called official representation of objects, it is used for eval() that can evaluate Python’s expression
- __str__() goal is to be readable for human, which python called informal nicely printable string of objects
- Container’s __str__ uses contained objects’ __repr__
- Since __repr__ provides a backup for __str__, if you can only write one, start with __repr__, if you override __repr__, that’s ALSO used for __str__, but not vice versa
最后一种字符串字面量的写法:
>>> str_unicode = u'田'
>>> type(str_unicode)
<type 'unicode'>
>>> len(str_unicode)
1
>>> str_unicode
u'\u7530'
>>> repr(str_unicode)
"u'\\u7530'"
>>> str(str_unicode)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u7530' in position 0: ordinal not in range(128)
>>> print(str_unicode)
田
>>> str_unicode.__str__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u7530' in position 0: ordinal not in range(128)
>>> reload(sys)
<module 'sys' (built-in)>
>>> sys.setdefaultencoding('utf-8')
>>> str(str_unicode)
'\xe7\x94\xb0'
我们发现在字符串字面量前面加上u前缀后,str_unicode是unicode类型,并且使用len()求长度也得到了正确的逻辑长度1;
可是在使用str(object)作用到str_unicode时,结果尽然发生了ascii encode错误,这充分说明Python2内部在做强制转换,将unicode类型转换成byte-string类型,并且采用了系统默认编码。sys.getdefaultencoding()=’ascii’;当我们通过sys.setdefaultencoding设置成符合的utf8编码后,错误消失了,这说明我们推测正确;
这里显然是Python2解释器接读取到[s, t, r, _, u, n, i, c, o, d, e, ’ ‘, =, ’ ‘, u, \xe7, \x94, \xb0],然后解析到u表明是要作为unicode存储,因此str_unicode就存储成了Python2内部的Unicode码,也就是utf16,\u7530;也就是说python2 内部的Unicode字符串才是符合程序逻辑意义的字符串;
下面是unicode-string 和 byte-string的相互转换:
>>> print(str_unicode)
田
>>> print(str_unicode.encode('utf8'))
田
>>> print(str_unicode.encode('gbk'))
��
>>> type(str_unicode.encode('utf8'))
<type 'str'>
>>> type(str_unicode.encode('gbk'))
<type 'str'>
str_unicode经过encode后,类型又是str了,并且当byte-string和实际默认编码不符合时,例如gbk和默认的utf8不符合,就会出现乱码了;
Python3
>>> str_byte = '田'
>>> type(str_byte)
<class 'str'>
>>> len(str_byte)
1
>>> str_byte
'田'
>>> str(str_byte)
'田'
>>> repr(str_byte)
"'田'"
>>> str_unicode = u'田'
>>> type(str_unicode)
<class 'str'>
>>> str_unicode
'田'
>>> str(str_unicode)
'田'
>>> repr(str_unicode)
"'田'"
>>> print(str_unicode)
田
>>> print(str_byte)
田
>>> str_byte.__str__()
'田'
>>> str_unicode.__str__()
'田'
>>> str_u = u'\u7530'
>>> str_u
'田'
>>> str(str_u)
'田'
>>> repr(str_u)
"'田'"
>>> print(str_u)
田
>>> str_u_ = '\u7530'
>>> str_u_
'田'
>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
>>> type(str_unicode)
<class 'str'>
>>> type(str_u)
<class 'str'>
>>> type(str_u_)
<class 'str'>
>>> len(str_unicode)
1
>>> len(str_u)
1
>>> len(str_u_)
1
我们发现在Python3中,代码中书写的字符串字面常量都变成了class str类型,这个class str其实对应Python2中的type unicode类型。
再来寻找Python3中的byte-string类型:
>>> str_b = b'田'
File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.
>>> str_b = b'tian'
>>> type(str_b)
<class 'bytes'>
>>> str_b = b'\xe7\x94\xb0'
>>>len(str_b)
3
>>> str_b.decode('utf-8')
'田'
>>> type(str_b.decode('utf-8'))
<class 'str'>
>>> frame = bytearray()
>>> type(frame)
<class 'bytearray'>
>>> frame.append(0xe7)
>>> frame.append(0x94)
>>> frame.append(0xb0)
>>> frame.decode('utf-8')
'田'
那么Python2中的byte-string类型即type str在Python3中对应的是class bytes;不过,在Python3中,加b前缀的书写方式已经默认不能包含非ascii字符了;但是Python2和Python3都有bytearray类型;
总结
最后,我们理清一下Python的字符串演进过程:
演进过程:
×××××××××× | Python2 | Python3 |
---|---|---|
byte-string | type str | class bytes |
unicode-string | type unicode | class str |
bytearray | type bytearray | class bytearray |
书写方式:
×××××××× | byte-string | unicode-string | bytearray |
---|---|---|---|
Python2 | 无前缀u或加前缀b | 加前缀u | bytearray() |
Python3 | 加前缀b(只支持ascii) | 加或者不加u | bytearray() |
实际上bytearray和byte-string等价;
最后介绍Python2和Python3通用的字符串处理模型,叫Unicode三明治模型,他是由Ned Batchelder提出来的,他的这篇文章Pragmatic Unicode写的超级好,同时也是演讲。
Unicode sandwich
Bytes on the outside, unicode on the inside, Encode/decode at the edgespython pro tips:
- Unicode sandwich: keep all text in your program as Unicode, and convert as close to the edges as possible.
- Know what your strings are: you should be able to explain which of your strings are Unicode, which are bytes, and for your byte strings, what encoding they use.
- Test your Unicode support. Use exotic strings throughout your test suites to be sure you’re covering all the cases.
参考文献:
关于字符编码:
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
字符编码详解
关于Python字符编码:
Pragmatic Unicode
More About Unicode in Python 2 and 3
PEP 100
Why sys.setdefaultencoding() will break code
Unicode码表:
http://graphemica.com/unicode/characters