Perl、Python的多字节字符处理方式是UCS(Universal Code Set),Ruby的多字节字符处理方式是CSI(Code Set Independent)。UCS的做法是,不管你读取的是哪一种编码的字节,读进程序以后都必须统一设定为某一种特定编码,因此程序内处理的实际字节可能会转换。而CSI的做法是读取的字节不需要转换,只是把一个字节串加上一个编码的属性。
一、字节和字符的理解问题
UCS是相对比较难理解的,必须清楚地明白字节和字符的区别。字节是具体的,就是一串0和1,字符却是抽象的,都不好说明到底是什么,只能说是我们想表示的某一个符号。UCS方式是把具体的字节读进来,转换成抽象的字符,然后把字符提供给编程者处理,这个时候理论上编程者是不去管字节、甚至不应该看到字节的。但是,抽象的字符也必须以具体的字节形式存在,否则计算机无法内部存储和处理。这就出现一个问题,从文件读取的是字节,内存里存储、处理的也是字节,却告诉编程者,没有字节了,你现在处理的是字符,好理解吗?
Perl是把一串字节加上utf8标记,告诉你有utf8标记的是字符,没有utf8标记的是字节。Perl靠修修补补,不动原有字符串架构而把多字节字符问题解决掉了,但是这个utf8标记的做法很别扭,纯粹实用主义。Python则是分成两个类,Py2里的str实际是字节串,unicode是字符串。这两个名称用得不好,到Py3里原来的str改叫bytes,原来的unicode改叫str,这才算名符其实,可是“Python3的str不是Python2的str,而是Python2的unicode”又把一些人绕晕了。Py2的时候,GvR好像自己都没把字节和字符的关系弄清楚,表现是str类和unicode类同时都有decode和encode两个方法。“编码”是将字符用字节来表示的动作,而“解码”是把字节还原成抽象的字符的动作,所以
encode应该是只有字符串类才有的,一个字符串encode的结果是返回一个字节串,而decode是只有字节串类才有的,一个字节串decode的结果是返回一个字符串。Py2里这事儿处理得糟糕,到Py3才改正确了,str类只有encode方法,bytes只有decode方法。
语言的设计者一开始都未必能把字节、字符的关系问题处理干净、甚至弄清楚,这个“字节”“字符”之间的区分虽然是科学的,但是对于一般人来说未必很实用。
Ruby的CSI方式,就把“抽象的字符”这么一个概念给去掉了。Ruby里的string,全部都是一串字节,但是是带上了encoding属性的字节串,因为encoding属性的存在,所以在任何时候它都能被看成是字符串。这样没有了Python里那种str和bytes的分野,字节和字符在Ruby里是完全统一的。甚至非文本的二进制字节串,都有一个encoding,叫做“ASCII-8BIT”。默认情况下对它的处理是按字符的,不过还有each_byte这样的方法是处理字节的。如果想专门处理字节,那么把这个字符串force_encoding('ASCII-8BIT')一下,按字符处理也就跟按字节处理完全重叠。
二、Ruby可以模拟UCS方式
UCS说到底就是读文件或别的IO对象的字节,然后统一转换为某一种编码的字节串给到编程者手上,但是不让你看到字节,只让你看到“字符”。Ruby当然也可以做到读取字节→统一转换→给你,只不过你还是可以看到字节,但是你可以视而不见,只看字符。
除了字符串有encoding属性外,Ruby的IO对象、File对象也有encoding属性,而且是两个。一个是external_encoding,一个是internal_encoding。external_encoding是说这个文件的实际字节流应该用什么编码来看待、处理,而internal_encoding是说读了这个文件的字节后再把它转换成别的什么编码的字节串。比如f = open("gbk_file.txt","r:gbk:utf-8")中的"r:gbk:utf-8",gbk就是外部编码,utf-8是内部编码,f对象每次读行或读字符时,GBK编码的字节就自动转换为UTF-8编码的了。“上下左右”在文件里是“\xC9\xCF\xCF\xC2\xD7\xF3\xD3\xD2”,读进来以后变成"\xE4\xB8\x8A\xE4\xB8\x8B\xE5\xB7\xA6\xE5\x8F\xB3"这样一串字节且带上了UTF-8的编码属性。
Encoding类有一个类方法default_internal=,设置为某一编码,然后你打开文件时如果不再另行设置internal_encoding,则所有的读入字节都会默认转换成default_internal的字节。这跟UCS的效果是一样的,但是比UCS灵活,不是强制性的。UCS的那个统一编码一般是固定的(Python好像可以改,但恐怕没人改),为了通用性,不是UTF-8就是UTF-16之类,反正必然是Unicode的一种;而Ruby里随你设置。比如我处理的全部是普通中文文本,有GBK的、Big5的、UTF-8的,我可以Encoding.default_internal="gbk",这样处理编码杂乱的文件时也不会出现问题,已经统一成GBK了。
两个不兼容编码的字符串互相操作会出错,这不是怪Ruby的问题。Ruby默认是CSI的,Encoding.default_internal默认是nil,也就是不转换字节,读进来的是什么字节,它就还是什么字节。Ruby也提供了模仿UCS的途径,你要想像UCS那样统一字节处理,当然可以,你要做的是考虑设置好一个默认的internal_encoding。Ruby是很自由的,依我看,是比Perl更加推崇There's more than one way to do it的。
三、Ruby的CSI方式的好处
这不是官方说法,只是我用的时候的一些感觉
1、可以不转换字节处理外部文件。比如你读取GBK编码的文件进行处理,并依然输出为GBK编码,UCS方式你就必须:读取GBK→转换成UCS→处理UCS→转换成GBK→输出GBK。而Ruby的CSI可以把两个转换步骤省掉,变成:读取GBK→处理GBK→输出GBK。这个一方面是要省点资源,但是Ruby本来就不是节约资源的主,所以我也不好意思拿这个来夸Ruby。更重要的好处,可能是能避免在两个转换阶段出现问题。
Python版我回答过这样一个问题: http://www.newsmth.net/bbstcon.php?board=Python&gid=76573
他读一个文件读不成功,为什么?因为里面有个“䜣”字,这个字GBK里有,但是Unicode尚未支持,这导致了在转换成UCS这个阶段出错了。在原文的情况下,这是个错字,手动改正再处理,但假如我就是要处理这么个字呢?Python是没法简单做到的;Ruby如果以模拟UCS的方式(就是加一个转换内部编码,open(file,'r:gbk:utf-8'))也是没法做到,但是用默认的CSI方式,读GBK就按GBK处理,不转换,是完全没问题的。
中文情况还好,像这种只是特殊情况,一般人很少遇到。但日文编码问题好像很复杂,甚至某种通用编码不跟ASCII兼容,转换来转换去搞不好就出错了。松本这样做字符串的编码,我以为有个想法是减少内部环节,也就能减少发生问题的可能性。
2、可以做到(UCS方式难以做到的)跟特定编码密切相关的事
s1 = "一".encode('gbk')
s2 = "一".encode('utf-8')
s1.succ # 得到gbk的“壹”字
s2.succ # 得到utf-8的“丁”字
字符串的succ方式是得到码表里的下一个字符,比如"a".succ得到"b","K".succ得到"L",这是按特定编码来算的,而GBK码表里的字序跟Unicode的字序是截然不同的,在GB编码里“一”字后面是“壹”,在Unicode里“一”字后面却是“丁”。GB的常用字是按音序排的,我现在想要按照GB编码,把常见的音节为yi的全取出来,该怎么办?
begin_char = "一".encode('gbk') # 第一个是“一”
end_char = "绎".encode('gbk') # 最后一个字是“绎”
(begin_char..end_char).each {|c| puts c}
拿某种UTF做UCS的就没法做到这一点
3、IO/File对象的external_encoding、internal_encoding设计非常之妙,完完全全可以动态设定编码,用一个set_encoding方法
Ruby的CSI方式,程序读文件时,是去按照这个file对象本身所具有的external_encoding属性理解字节,再按照这个file对象本身所具有的internal_encoding属性转换字节(如果internal_encoding设置为nil则不转换)。我可以去设想有这么一个文件,因为某种原因,是多编码字节混杂的畸形文件,比如前5行是UTF-8编码,后面是GBK编码,怎么处理?
file.set_encoding('utf-8')
5.times { line = file.readline }
file.set_encoding('gbk:utf-8') # :前面的是外部编码,后面的是转换成的内部编码
line = file.readline # 虽然原文件后面的编码是gbk的,但是也转换成utf-8了
可能比较实用一点的是ARGF读文件的问题,也许我有一堆中文文件,编码各不相同,你也可以想办法ARGF.set_encoding('gbk')或是set_encoding('utf-8')
ARGF这种Ruby是来自Perl,但是Perl好像也没法做到这么简便地动态改换读取的编码
四、Ruby的CSI方式的缺点
最大的缺点就是耗资源了,每个字符串都得加上一个encoding属性。不过松本从来想的是让程序员happy,而不是像Python那样节省资源(为什么Python里的list和tuple还得分开?还另弄一个array?好像是为了省资源吧,让程序员自己根据情况挑选用哪个)。
另一个看起来是缺点就是不同编码字符串互相操作时出错。我其实不觉得这是个缺点,说它是缺点的只是局限于UCS的思维方式,认为UCS才是正统,才是唯一正确的。你如果按CSI的方式去理解这个问题,这个就好像他用法语跟你讲话,你用日语回答他。而Ruby也提供了模拟UCS的方式,所以我认为没问题。