• Python字符编码详解


    注:本文转载自https://www.cnblogs.com/huxi/articles/1897271.html

     

    本文简单介绍了各种常用的字符编码的特点,并介绍了在python2.x中如何与编码问题作战 :) 
    请注意本文关于Python的内容仅适用于2.x,3.x中str和unicode有翻天覆地的变化,请查阅其他相关文档。 
    尊重作者的劳动,转载请注明作者及原文地址 >.<

    1. 字符编码简介

    1.1. ASCII

    ASCII(American Standard Code for Information Interchange),是一种单字节的编码。计算机世界里一开始只有英文,而单字节可以表示256个不同的字符,可以表示所有的英文字符和许多的控制符号。不过ASCII只用到了其中的一半(x80以下),这也是MBCS得以实现的基础。

    1.2. MBCS

    然而计算机世界里很快就有了其他语言,单字节的ASCII已无法满足需求。后来每个语言就制定了一套自己的编码,由于单字节能表示的字符太少,而且同时也需要与ASCII编码保持兼容,所以这些编码纷纷使用了多字节来表示字符,如GBxxx、BIGxxx等等,他们的规则是,如果第一个字节是x80以下,则仍然表示ASCII字符;而如果是x80以上,则跟下一个字节一起(共两个字节)表示一个字符,然后跳过下一个字节,继续往下判断。

    这里,IBM发明了一个叫Code Page的概念,将这些编码都收入囊中并分配页码,GBK是第936页,也就是CP936。所以,也可以使用CP936表示GBK。

    MBCS(Multi-Byte Character Set)是这些编码的统称。目前为止大家都是用了双字节,所以有时候也叫做DBCS(Double-Byte Character Set)。必须明确的是,MBCS并不是某一种特定的编码,Windows里根据你设定的区域不同,MBCS指代不同的编码,而Linux里无法使用MBCS作为编码。在Windows中你看不到MBCS这几个字符,因为微软为了更加洋气,使用了ANSI来吓唬人,记事本的另存为对话框里编码ANSI就是MBCS。同时,在简体中文Windows默认的区域设定里,指代GBK。

    1.3. Unicode

    后来,有人开始觉得太多编码导致世界变得过于复杂了,让人脑袋疼,于是大家坐在一起拍脑袋想出来一个方法:所有语言的字符都用同一种字符集来表示,这就是Unicode。

    最初的Unicode标准UCS-2使用两个字节表示一个字符,所以你常常可以听到Unicode使用两个字节表示一个字符的说法。但过了不久有人觉得256*256太少了,还是不够用,于是出现了UCS-4标准,它使用4个字节表示一个字符,不过我们用的最多的仍然是UCS-2。

    UCS(Unicode Character Set)还仅仅是字符对应码位的一张表而已,比如"汉"这个字的码位是6C49。字符具体如何传输和储存则是由UTF(UCS Transformation Format)来负责。

    一开始这事很简单,直接使用UCS的码位来保存,这就是UTF-16,比如,"汉"直接使用x6Cx49保存(UTF-16-BE),或是倒过来使用x49x6C保存(UTF-16-LE)。但用着用着美国人觉得自己吃了大亏,以前英文字母只需要一个字节就能保存了,现在大锅饭一吃变成了两个字节,空间消耗大了一倍……于是UTF-8横空出世。

    UTF-8是一种很别扭的编码,具体表现在他是变长的,并且兼容ASCII,ASCII字符使用1字节表示。然而这里省了的必定是从别的地方抠出来的,你肯定也听说过UTF-8里中文字符使用3个字节来保存吧?4个字节保存的字符更是在泪奔……(具体UCS-2是怎么变成UTF-8的请自行搜索)

    另外值得一提的是BOM(Byte Order Mark)。我们在储存文件时,文件使用的编码并没有保存,打开时则需要我们记住原先保存时使用的编码并使用这个编码打开,这样一来就产生了许多麻烦。(你可能想说记事本打开文件时并没有让选编码?不妨先打开记事本再使用文件 -> 打开看看)而UTF则引入了BOM来表示自身编码,如果一开始读入的几个字节是其中之一,则代表接下来要读取的文字使用的编码是相应的编码:

    BOM_UTF8 'xefxbbxbf' 
    BOM_UTF16_LE 'xffxfe' 
    BOM_UTF16_BE 'xfexff'

    并不是所有的编辑器都会写入BOM,但即使没有BOM,Unicode还是可以读取的,只是像MBCS的编码一样,需要另行指定具体的编码,否则解码将会失败。

    你可能听说过UTF-8不需要BOM,这种说法是不对的,只是绝大多数编辑器在没有BOM时都是以UTF-8作为默认编码读取。即使是保存时默认使用ANSI(MBCS)的记事本,在读取文件时也是先使用UTF-8测试编码,如果可以成功解码,则使用UTF-8解码。记事本这个别扭的做法造成了一个BUG:如果你新建文本文件并输入"姹塧"然后使用ANSI(MBCS)保存,再打开就会变成"汉a",你不妨试试 :)

    2. Python2.x中的编码问题

    2.1. str和unicode

    str和unicode都是basestring的子类。严格意义上说,str其实是字节串,它是unicode经过编码后的字节组成的序列。对UTF-8编码的str'汉'使用len()函数时,结果是3,因为实际上,UTF-8编码的'汉' == 'xE6xB1x89'。

    unicode才是真正意义上的字符串,对字节串str使用正确的字符编码进行解码后获得,并且len(u'汉') == 1。

    再来看看encode()和decode()两个basestring的实例方法,理解了str和unicode的区别后,这两个方法就不会再混淆了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # coding: UTF-8
     
    u = u'汉'
    print repr(u) # u'u6c49'
    s = u.encode('UTF-8')
    print repr(s) # 'xe6xb1x89'
    u2 = s.decode('UTF-8')
    print repr(u2) # u'u6c49'
     
    # 对unicode进行解码是错误的
    # s2 = u.decode('UTF-8')
    # 同样,对str进行编码也是错误的
    # u2 = s.encode('UTF-8')

    需要注意的是,虽然对str调用encode()方法是错误的,但实际上Python不会抛出异常,而是返回另外一个相同内容但不同id的str;对unicode调用decode()方法也是这样。很不理解为什么不把encode()和decode()分别放在unicode和str中而是都放在basestring中,但既然已经这样了,我们就小心避免犯错吧。

    2.2. 字符编码声明

    源代码文件中,如果有用到非ASCII字符,则需要在文件头部进行字符编码的声明,如下:

    1
    #-*- coding: UTF-8 -*-

    实际上Python只检查#、coding和编码字符串,其他的字符都是为了美观加上的。另外,Python中可用的字符编码有很多,并且还有许多别名,还不区分大小写,比如UTF-8可以写成u8。参见http://docs.python.org/library/codecs.html#standard-encodings

    另外需要注意的是声明的编码必须与文件实际保存时用的编码一致,否则很大几率会出现代码解析异常。现在的IDE一般会自动处理这种情况,改变声明后同时换成声明的编码保存,但文本编辑器控们需要小心 :)

    2.3. 读写文件

    内置的open()方法打开文件时,read()读取的是str,读取后需要使用正确的编码格式进行decode()。write()写入时,如果参数是unicode,则需要使用你希望写入的编码进行encode(),如果是其他编码格式的str,则需要先用该str的编码进行decode(),转成unicode后再使用写入的编码进行encode()。如果直接将unicode作为参数传入write()方法,Python将先使用源代码文件声明的字符编码进行编码然后写入。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # coding: UTF-8
     
    f = open('test.txt')
    s = f.read()
    f.close()
    print type(s) # <type 'str'>
    # 已知是GBK编码,解码成unicode
    u = s.decode('GBK')
     
    f = open('test.txt', 'w')
    # 编码成UTF-8编码的str
    s = u.encode('UTF-8')
    f.write(s)
    f.close()

    另外,模块codecs提供了一个open()方法,可以指定一个编码打开文件,使用这个方法打开的文件读取返回的将是unicode。写入时,如果参数是unicode,则使用open()时指定的编码进行编码后写入;如果是str,则先根据源代码文件声明的字符编码,解码成unicode后再进行前述操作。相对内置的open()来说,这个方法比较不容易在编码上出现问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # coding: GBK
     
    import codecs
     
    f = codecs.open('test.txt', encoding='UTF-8')
    u = f.read()
    f.close()
    print type(u) # <type 'unicode'>
     
    f = codecs.open('test.txt', 'a', encoding='UTF-8')
    # 写入unicode
    f.write(u)
     
    # 写入str,自动进行解码编码操作
    # GBK编码的str
    s = '汉'
    print repr(s) # 'xbaxba'
    # 这里会先将GBK编码的str解码为unicode再编码为UTF-8写入
    f.write(s)
    f.close()

    2.4. 与编码相关的方法

    sys/locale模块中提供了一些获取当前环境下的默认编码的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    # coding:gbk
     
    import sys
    import locale
     
    def p(f):
        print '%s.%s(): %s' % (f.__module__, f.__name__, f())
     
    # 返回当前系统所使用的默认字符编码
    p(sys.getdefaultencoding)
     
    # 返回用于转换Unicode文件名至系统文件名所使用的编码
    p(sys.getfilesystemencoding)
     
    # 获取默认的区域设置并返回元祖(语言, 编码)
    p(locale.getdefaultlocale)
     
    # 返回用户设定的文本数据编码
    # 文档提到this function only returns a guess
    p(locale.getpreferredencoding)
     
    # xbaxba是'汉'的GBK编码
    # mbcs是不推荐使用的编码,这里仅作测试表明为什么不应该用
    print r"'xbaxba'.decode('mbcs'):", repr('xbaxba'.decode('mbcs'))
     
    #在笔者的Windows上的结果(区域设置为中文(简体, 中国))
    #sys.getdefaultencoding(): gbk
    #sys.getfilesystemencoding(): mbcs
    #locale.getdefaultlocale(): ('zh_CN', 'cp936')
    #locale.getpreferredencoding(): cp936
    #'xbaxba'.decode('mbcs'): u'u6c49'

    3.一些建议

    3.1. 使用字符编码声明,并且同一工程中的所有源代码文件使用相同的字符编码声明。

    这点是一定要做到的。

    3.2. 抛弃str,全部使用unicode。

    按引号前先按一下u最初做起来确实很不习惯而且经常会忘记再跑回去补,但如果这么做可以减少90%的编码问题。如果编码困扰不严重,可以不参考此条。

    3.3. 使用codecs.open()替代内置的open()。

    如果编码困扰不严重,可以不参考此条。

    3.4. 绝对需要避免使用的字符编码:MBCS/DBCS和UTF-16。

    这里说的MBCS不是指GBK什么的都不能用,而是不要使用Python里名为'MBCS'的编码,除非程序完全不移植。

    Python中编码'MBCS'与'DBCS'是同义词,指当前Windows环境中MBCS指代的编码。Linux的Python实现中没有这种编码,所以一旦移植到Linux一定会出现异常!另外,只要设定的Windows系统区域不同,MBCS指代的编码也是不一样的。分别设定不同的区域运行2.4小节中的代码的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #中文(简体, 中国)
    #sys.getdefaultencoding(): gbk
    #sys.getfilesystemencoding(): mbcs
    #locale.getdefaultlocale(): ('zh_CN', 'cp936')
    #locale.getpreferredencoding(): cp936
    #'xbaxba'.decode('mbcs'): u'u6c49'
     
    #英语(美国)
    #sys.getdefaultencoding(): UTF-8
    #sys.getfilesystemencoding(): mbcs
    #locale.getdefaultlocale(): ('zh_CN', 'cp1252')
    #locale.getpreferredencoding(): cp1252
    #'xbaxba'.decode('mbcs'): u'xbaxba'
     
    #德语(德国)
    #sys.getdefaultencoding(): gbk
    #sys.getfilesystemencoding(): mbcs
    #locale.getdefaultlocale(): ('zh_CN', 'cp1252')
    #locale.getpreferredencoding(): cp1252
    #'xbaxba'.decode('mbcs'): u'xbaxba'
     
    #日语(日本)
    #sys.getdefaultencoding(): gbk
    #sys.getfilesystemencoding(): mbcs
    #locale.getdefaultlocale(): ('zh_CN', 'cp932')
    #locale.getpreferredencoding(): cp932
    #'xbaxba'.decode('mbcs'): u'uff7auff7a'

    可见,更改区域后,使用mbcs解码得到了不正确的结果,所以,当我们需要使用'GBK'时,应该直接写'GBK',不要写成'MBCS'。

    UTF-16同理,虽然绝大多数操作系统中'UTF-16'是'UTF-16-LE'的同义词,但直接写'UTF-16-LE'只是多写3个字符而已,而万一某个操作系统中'UTF-16'变成了'UTF-16-BE'的同义词,就会有错误的结果。实际上,UTF-16用的相当少,但用到的时候还是需要注意。

    --END--

    后续:(本人更青睐下面这个解释)

    转载自:https://www.cnblogs.com/kingcat/archive/2012/10/16/2726334.html

    Unicode(UTF-8, UTF-16)令人混淆的概念

    为啥需要Unicode

          我们知道计算机其实挺笨的,它只认识0101这样的字符串,当然了我们看这样的01串时肯定会比较头晕的,所以很多时候为了描述简单都用十进制,十六进制,八进制表示.实际上都是等价的,没啥太多不一样.其他啥文字图片之类的其他东东计算机不认识.那为了在计算机上表示这些信息就必须转换成一些数字.你肯定不能想怎么转换就怎么转,必须得有定些规则.于是刚开始的时候就有ASCII字符集(American Standard Code for Information Interchange, "美国信息交换标准码),它使用7 bits来表示一个字符,总共表示128个字符,我们一般都是用字节(byte,即8个01串)来作为基本单位.那么怎么当用一个字节来表示字符时第一个bit总是0,剩下的七个字节就来表示实际内容.后来IBM公司在此基础上进行了扩展,用8bit来表示一个字符,总共可以表示256个字符.也就是当第一个bit是0时仍表示之前那些常用的字符.当为1时就表示其他补充的字符.

            英文字母再加一些其他标点字符之类的也不会超过256个.一个字节表示主足够了.但其他一些文字不止这么多 ,像汉字就上万个.于是又出现了其他各种字符集.这样不同的字符集交换数据时就有问题了.可能你用某个数字表示字符A,但另外的字符集又是用另外一个数字表示A.这样交互起来就麻烦了.于是就出现了Unicode和ISO这样的组织来统一制定一个标准,任何一个字符只对应一个确定的数字.ISO取的名字叫UCS(Universal Character Set),Unicode取的名字就叫unicode了.

          总结起来为啥需要Unicodey就是为了适应全球化的发展,便于不同语言之间的兼容交互,而ASCII不再能胜任此任务了.

    Unicode详细介绍

    1.容易产生后歧义的两字节

            unicode的第一个版本是用两个字节(16bit)来表示所有字符

            .实际上这么说容易让人产生歧义,我们总觉得两个字节就代表保存在计算机中时是两个字节.于是任何字符如果用unicode表示的话保存下来都占两个字节.其实这种说法是错误的.

         其实Unicode涉及到两个步骤,首先是定义一个规范,给所有的字符指定一个唯一对应的数字,这完全是数学问题,可以跟计算机没半毛钱关系.第二步才是怎么把字符对应的数字保存在计算机中,这才涉及到实际在计算机中占多少字节空间.

         所以我们也可以这样理解,Unicode是用0至65535之间的数字来表示所有字符.其中0至127这128个数字表示的字符仍然跟ASCII完全一样.65536是2的16次方.这是第一步.第二步就是怎么把0至65535这些数字转化成01串保存到计算机中.这肯定就有不同的保存方式了.于是出现了UTF(unicode transformation format),有UTF-8,UTF-16.

    2.UTF-8 与UTF-16的区别

        UTF-16比较好理解,就是任何字符对应的数字都用两个字节来保存.我们通常对Unicode的误解就是把Unicode与UTF-16等同了.但是很显然如果都是英文字母这做有点浪费.明明用一个字节能表示一个字符为啥整两个啊.

       于是又有个UTF-8,这里的8非常容易误导人,8不是指一个字节,难道一个字节表示一个字符?实际上不是.当用UTF-8时表示一个字符是可变的,有可能是用一个字节表示一个字符,也可能是两个,三个.当然最多不能超过3个字节了.反正是根据字符对应的数字大小来确定.

       于是UTF-8和UTF-16的优劣很容易就看出来了.如果全部英文或英文与其他文字混合,但英文占绝大部分,用UTF-8就比UTF-16节省了很多空间.而如果全部是中文这样类似的字符或者混合字符中中文占绝大多数.UTF-16就占优势了,可以节省很多空间.另外还有个容错问题,等会再讲

      看的有点晕了吧,举个例子.假如中文字"汉"对应的unicode是6C49(这是用十六进制表示,用十进制表示是27721为啥不用十进制表示呢?很明显用十六进制表示要短点.其实都是等价的没啥不一样.就跟你说60分钟和1小时一样.).你可能会问当用程序打开一个文件时我们怎么知道那是用的UTF-8还是UTF-16啊.自然会有点啥标志,在文件的开头几个字节就是标志.

    EF BB BF 表示UTF-8

    FE FF 表示UTF-16.

    用UTF-16表示"汉"

    假如用UTF-16表示的话就是01101100   01001001(共16 bit,两个字节).程序解析的时候知道是UTF-16就把两个字节当成一个单元来解析.这个很简单.

    用UTF-8表示"汉"

    用UTF-8就有复杂点.因为此时程序是把一个字节一个字节的来读取,然后再根据字节中开头的bit标志来识别是该把1个还是两个或三个字节做为一个单元来处理.

    0xxxxxxx,如果是这样的01串,也就是以0开头后面是啥就不用管了XX代表任意bit.就表示把一个字节做为一个单元.就跟ASCII完全一样.

    110xxxxx 10xxxxxx.如果是这样的格式,则把两个字节当一个单元

    1110xxxx 10xxxxxx 10xxxxxx 如果是这种格式则是三个字节当一个单元.

    这是约定的规则.你用UTF-8来表示时必须遵守这样的规则.我们知道UTF-16不需要用啥字符来做标志,所以两字节也就是2的16次能表示65536个字符.

    而UTF-8由于里面有额外的标志信息,所有一个字节只能表示2的7次方128个字符,两个字节只能表示2的11次方2048个字符.而三个字节能表示2的16次方,65536个字符.

    由于"汉"的编码27721大于2048了所有两个字节还不够,只能用三个字节来表示.

    所有要用1110xxxx 10xxxxxx 10xxxxxx这种格式.把27721对应的二进制从左到右填充XXX符号(实际上不一定从左到右,也可以从右到左,这是涉及到另外一个问题.等会说.

    刚说到填充方式可以不一样,于是就出现了Big-Endian,Little-Endian的术语.Big-Endian就是从左到右,Little-Endian是从右到左.

    由上面我们可以看出UTF-8需要判断每个字节中的开头标志信息,所以如果一当某个字节在传送过程中出错了,就会导致后面的字节也会解析出错.而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力强.

    Unicode版本2

        前面说的都是unicode的第一个版本.但65536显然不算太多的数字,用它来表示常用的字符是没一点问题.足够了,但如果加上很多特殊的就也不够了.于是从1996年开始又来了第二个版本.用四个字节表示所有字符.这样就出现了UTF-8,UTF16,UTF-32.原理和之前肯定是完全一样的,UTF-32就是把所有的字符都用32bit也就是4个字节来表示.然后UTF-8,UTF-16就视情况而定了.UTF-8可以选择1至8个字节中的任一个来表示.而UTF-16只能是选两字节或四字节..由于unicode版本2的原理完全是一样的,就不多说了.

    前面说了要知道具体是哪种编码方式,需要判断文本开头的标志,下面是所有编码对应的开头标志

    EF BB BF    UTF-8
    FE FF     UTF-16/UCS-2, little endian
    FF FE     UTF-16/UCS-2, big endian
    FF FE 00 00  UTF-32/UCS-4, little endian.
    00 00 FE FF  UTF-32/UCS-4, big-endian.

    其中的UCS就是前面说的ISO制定的标准,和Unicode是完全一样的,只不过名字不一样.ucs-2对应utf-16,ucs-4对应UTF-32.UTF-8是没有对应的UCS

    https://www.cnblogs.com/Jack47/p/unicode-utf8-character_set-encoding.html

  • 相关阅读:
    JavaScript 数组
    Function类型
    javascript面向对象(一)
    javascript变量的作用域
    登陆验证
    注册验证
    php类
    二叉搜索树的 查询最小值
    二叉 搜索树查找最大值
    二叉搜索树 中查找是否存在该值
  • 原文地址:https://www.cnblogs.com/code-juggler/p/8074020.html
Copyright © 2020-2023  润新知