Python中文乱码解析:从字节与字符的本质区别入手
本文深入解析Python处理中文乱码的核心原理,重点区分“字符”与“字节”的概念。通过对比Python、Perl及Ruby在编码处理上的差异,解释str与unicode的区别,以及encode和decode的实际作用,帮助开发者从根本上理解并解决编码问题。
在Python2.x的开发环境中,解决中文乱码最常见且广为人知的做法是在代码文件头部添加#-*- coding:utf-8 –*-。
然而,为什么要执行这一操作?“字节”与“字符”究竟有何不同?接下来我们将对此进行详细探讨。
这里分享我对字符处理机制的个人理解。尽管我对Python编码处理的具体底层细节并非完全掌握,但粗略观察后发现,其原理与Perl语言颇为相似。
核心关键在于必须严格区分“字符”与“字节”这两个概念。“字符”属于抽象层面的概念,而“字节”则是具体的物理存储形式。
以汉字“中”为例,它在不同编码标准下对应的字节序列如下所示:
GBK Big5 UTF-8 UTF-16LE \xD6\xD0 \xA4\xA4 \xE4\xB8\xAD \x2D\x4E
所谓的“抽象”字符“中”,并非指代具体的\xD6\xD0、\xA4\xA4或其他任何字节序列。我们应将其理解为:在GBK编码体系中,由\xD6\xD0这组字节所指向的那个语言学意义上的字符(即能指对应所指);或者在UTF-8编码中,由\xE4\xB8\xAD所指向的那个字符。关键在于,它不是这些具体的字节本身。
问题在于,抽象的字符若要作为数据进行存储或网络传输,就必须具备具体的表现形式。也就是说,在程序内部实现中,若要存储“中”这个字符,必须选定特定的字节序列。你可以选择\xD6\xD0,也可以选择\xE4\xB8\xAD或\x2D\x4E。在Windows环境下,Python默认采用UTF-16LE编码,这意味着其内部“字符”的载体编码格式为UTF-16LE。
sys.setdefaultencoding(name) Set the current default string encoding used by the Unicode implementation.
根据文档描述,若我的理解无误,该函数的作用正是改变“字符”的载体编码。执行sys.setdefaultencoding('gbk')后,“中”这个字符在程序内部将不再由\x2D\x4E承载,而是转换为由\xD6\xD0来承载。
在Python2.x中,str与unicode有何区别?从字面意思看容易混淆,实际上,可以将str理解为“字节串”,而unicode理解为“字符串”(通常string被翻译为“字符串”,这在此处极易造成误解)。参考以下示例:
# -*- coding: gb2312 -*-
s = "张三李四"
print len(s) #=> 8
u = s.decode('gbk')
print len(u) #=> 4
假设我的脚本编码设置为GBK而非UTF-8,此时len(s)的结果为8,这代表这四个汉字实际占用的8个“字节”;而len(u)的结果为4,表示这里包含4个“字符”。
那么encode和decode的含义是什么?编码(encode)是将意义转换为符号的过程;解码(decode)则是将符号还原为意义的过程。在此语境下,encode应理解为将抽象字符转换为具体字节,而decode则是将具体字节还原为抽象字符。
当前的困境是:str类和unicode类都同时拥有encode和decode方法,这种设定令人难以接受。依据字节与字符的区分逻辑,encode方法理应仅归属于unicode类,而decode方法仅归属于str类。因为“意义”只能转换为“符号”,若“意义”再还原为“意义”,这一过程本身便缺乏逻辑意义。
如果我们尝试如下操作:
# -*- coding: gb2312 -*-
s = "张三李四"
u = s.decode('gbk') # 没问题,字节解码为字符,符号还原为意义
s2 = s.encode('gbk')
# 出错了!字节没法再编码成字节,除非s全部是ASCII字符,但是这样s2和s是完全等同的,这个操作有什么意义?
u2 = u.decode('gbk')
# 又出错了!也只能u只包含ASCII字符,u2和u也是完全等同,这个操作也没有意义
此处提及Perl的处理方式。我不确定Python处理编码的原理是否直接源自Perl,亦或是各门语言共同遵循的做法(尽管Ruby并未如此实现)。总体而言,Python2.x在此方面存在缺陷。
Perl中仅存在一种string类型,它实际上区分字符串和字节串(以UTF-8作为底层承载编码)。但与Python2.x区分str和unicode不同,Perl在string内部维护一个utf8标志位(flag)。当该flag为on时,该string被视为“字符”串;当flag为off时,则被视为“字节”串。其编码和解码函数定义如下:
$octets = encode(ENCODING, $string [, CHECK]) $string = decode(ENCODING, $octets [, CHECK])
$octets代表字节串,$string代表字符串。这意味着,encode仅对$string起作用,而decode仅对$octets起作用。这与Python中str和unicode两类均拥有两个方法(尽管其中一个往往无用)的情况不同。Larry Wall作为语言学家,其设计的这套字符与字节关系完全符合语言学中的“能指-所指”理论;相比之下,GvR可能对语言学涉猎较少,导致Python的处理方式不够精妙。
接下来探讨file.write为何会出现编码问题:
# -*- coding: gb2312 -*-
s = "张三李四"
u = s.decode('gbk')
f = open('text.txt','w')
f.write(u) # 出错!
f.write(u.encode('gbk')) # 这样才行
出错原因很简单:你试图输出的是“字符”,而非“字节”。前文已述,“字符”是抽象的,无法直接将抽象事物写入文件。尽管抽象字符下方必然有具体的承载字节,但Python似乎有意不让unicode底层的字节与IO操作耦合,这导致f.write(a_unicode)执行失败。当然,若a_unicode仅包含ASCII字符,操作可以成功,但这是一种捷径,一条让人愈发困惑的捷径。
那么u标记的意义何在?很简单,它自动完成从字节到字符的转换。
# -*- coding: gb2312 -*- s_or_u1 = "张三李四" print type(s_or_u1) #=> <type 'str'> s_or_u2 = u"张三李四" print type(s_or_u2) #=> <type 'unicode'>
u"张三李四"等效于"张三李四".decode(a_enc),其中a_enc即为#coding行所设定的gb2312编码。
不得不承认,(不知是否源自Perl)这套字符处理方式相当晦涩。字符与字节区分的概念本身就不易理解,而Python自身的细节处理也未做到位。Perl的处理已相当清晰,尚且难以理解,Python未处理干净则更成问题。此外,简要介绍Ruby的字符处理方式,它与Perl截然不同:
Ruby中不存在字符与字节的区分,所有字符串均为“带有一个编码属性的字节串”。由于没有抽象的字符概念,因此不存在字节到字符的转换,也就根本不需要decode方法。Ruby的String类仅包含encode方法。因为没有抽象的“字符”概念,Ruby的编码问题理论上应比Perl和Python更易于理解。此外,没有“字符”概念的另一个好处是:处理多字节文本无需经过中间转换。若要在Perl中处理中文字符,且源文件为GBK编码,实际必须先转换为UTF-8,Perl才能进行处理;Python则需先转化为UTF-16。对于海量文本而言,这一转换过程必然消耗资源。而Ruby无需此转换,可直接处理GBK或其他编码。这可能也是考虑了日文的实际情况,日文的shift-jis(?)作为本土编码,根本不与ASCII兼容,不像GBK与ASCII兼容。这样做便无需转换即可处理本地编码文档。如果说Perl的字符-字节区分是语言学家的学院派做法,那么Ruby则是契合多字节字符处理需求的实用派做法。
总结
以上即为本文关于python中文乱码不着急,先看懂字节和字符的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









