V2EX 首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  Python

Python 编码为什么那么蛋疼?

  •  2
     
  •   lzjun · 8 天前 · 4034 次点击

    据说,每个做 Python 开发的都被字符编码的问题搞晕过,最常见的错误就是 UnicodeEncodeError 、 UnicodeDecodeError ,你好像知道怎么解决,遗憾的是,错误又出现在其它地方,问题总是重蹈覆辙, str 到 unicode 之间的转换用 decode 还是 encode 方法还特不好记,老是混淆,问题究竟出在哪里?

    为了弄清楚这个问题,我决定从 python 字符串的构成以及字符编码的细节上进行深入浅出的分析

    字节与字符

    计算机存储的一切数据,文本字符、图片、视频、音频、软件都是由一串 01 的字节序列构成的,一个字节等于 8 个比特位。

    而字符就是一个符号,比如一个汉字、一个英文字母、一个数字、一个标点都可以称为一个字符。

    字节方便存储和网络传输,而字符用于显示,方便阅读。例如字符 "p" 存储到硬盘是一串二进制数据 01110000,占用一个字节的长度

    编码与解码

    我们用编辑器打开的文本,看到的一个个字符,最终保存在磁盘的时候都是以二进制字节序列形式存起来的。那么从字符到字节的转换过程就叫做编码( encode ),反过来叫做解码( decode ),两者是一个可逆的过程。编码是为了存储传输,解码是为了方便显示阅读。

    例如字符 "p" 经过编码处理保存到硬盘是一串二进制字节序列 01110000 ,占用一个字节的长度。字符 "禅" 有可能是以 "11100111 10100110 10000101" 占用 3 个字节的长度存储,为什么说是有可能呢?这个放到后面再说。

    Python 的编码为什么那么蛋疼?当然,这不能怪开发者。

    这是因为 Python2 使用 ASCII 字符编码作为默认编码方式,而 ASCII 不能处理中文,那么为什么不用 UTf-8 呢?因为 Guido 老爹为 Python 编写第一行代码是在 1989 年的冬天, 1991 年 2 月正式开源发布了第一个版本,而 Unicode 是 1991 年 10 月发布的,也就是说 Python 这门语言创立的时候 UTF-8 还没诞生,这是其一。

    Python 把字符串的类型还搞成两种, unicode 和 str ,以至于把开发者都弄糊涂了,这是其二。 python3 彻底把 字符串重新改造了,只保留一种类型,这是后话,以后再说。

    str 与 unicode

    Python2 把字符串分为 unicode 和 str 两种类型。本质上 str 是一串二进制字节序列,下面的示例代码可以看出 str 类型的 "禅" 打印出来是十六进制的 \xec\xf8 ,对应的二进制字节序列就是 '11101100 11111000'。

    >>> s = '禅'
    >>> s
    '\xec\xf8'
    >>> type(s)
    <type 'str'>
    

    而 unicode 类型的 u"禅" 对应的 unicode 符号是 u'\u7985'

    >>> u = u"禅"
    >>> u
    u'\u7985'
    >>> type(u)
    <type 'unicode'>
    

    我们要把 unicode 符号保存到文件或者传输到网络就需要经过编码处理转换成 str 类型,于是 python 提供了 encode 方法,从 unicode 转换到 str ,反之亦然。

    python2-str

    encode

    >>> u = u"禅"
    >>> u
    u'\u7985'
    >>> u.encode("utf-8")
    '\xe7\xa6\x85'
    

    decode

    >>> s = "禅"
    >>> s.decode("utf-8")
    u'\u7985'
    >>>
    

    不少初学者怎么也记不住 str 与 unicode 之间的转换用 encode 还是 decode ,如果你记住了 str 本质上其实是一串二进制数据,而 unicode 是字符(符号),编码( encode )就是把字符(符号)转换为 二进制数据的过程,因此 unicode 到 str 的转换要用 encode 方法,反过来就是用 decode 方法。

    encoding always takes a Unicode string and returns a bytes sequence, and decoding always takes a bytes sequence and returns a Unicode string".

    清楚了 str 与 unicode 之间的转换关系之后,我们来看看什么时候会出现 UnicodeEncodeError 、 UnicodeDecodeError 错误。

    UnicodeEncodeError

    UnicodeEncodeError 发生在 unicode 字符串转换成 str 字节序列的时候,来看一个例子,把一串 unicode 字符串保存到文件

    # -*- coding:utf-8 -*-
    def main():
        name = u'Python 之禅'
        f = open("output.txt", "w")
        f.write(name)
    

    错误日志

    UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-7: ordinal not in range(128)

    为什么会出现 UnicodeEncodeError ?

    因为调用 write 方法时, Python 会先判断字符串是什么类型,如果是 str ,就直接写入文件,不需要编码,因为 str 类型的字符串本身就是一串二进制的字节序列了。

    如果字符串是 unicode 类型,那么它会先调用 encode 方法把 unicode 字符串转换成二进制形式的 str 类型,才保存到文件,而 encode 方法会使用 python 默认的 ascii 码来编码

    相当于:

    >>> u"Python 之禅".encode("ascii")
    

    但是,我们知道 ASCII 字符集中只包含了 128 个拉丁字母,不包括中文字符,因此 出现了 'ascii' codec can't encode characters 的错误。要正确地使用 encode ,就必须指定一个包含了中文字符的字符集,比如: UTF-8 、 GBK 。

    >>> u"Python 之禅".encode("utf-8")
    'Python\xe4\xb9\x8b\xe7\xa6\x85'
    
    >>> u"Python 之禅".encode("gbk")
    'Python\xd6\xae\xec\xf8'
    

    所以要把 unicode 字符串正确地写入文件,就应该预先把字符串进行 UTF-8 或 GBK 编码转换。

    def main():
        name = u'Python 之禅'
        name = name.encode('utf-8')
        with open("output.txt", "w") as f:
        	f.write(name)
    

    当然,把 unicode 字符串正确地写入文件不止一种方式,但原理是一样的,这里不再介绍,把字符串写入数据库,传输到网络都是同样的原理

    UnicodeDecodeError

    UnicodeDecodeError 发生在 str 类型的字节序列解码成 unicode 类型的字符串时

    >>> a = u"禅"
    >>> a
    u'\u7985'
    >>> b = a.encode("utf-8")
    >>> b
    '\xe7\xa6\x85'
    >>> b.decode("gbk")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeDecodeError: 'gbk' codec can't decode byte 0x85 in position 2: incomplete multibyte sequence
    

    把一个经过 UTF-8 编码后生成的字节序列 '\xe7\xa6\x85' 再用 GBK 解码转换成 unicode 字符串时,出现 UnicodeDecodeError ,因为 (对于中文字符) GBK 编码只占用两个字节,而 UTF-8 占用 3 个字节,用 GBK 转换时,还多出一个字节,因此它没法解析。避免 UnicodeDecodeError 的关键是保持 编码和解码时用的编码类型一致。

    这也回答了文章开头说的字符 "禅",保存到文件中有可能占 3 个字节,有可能占 2 个字节,具体处决于 encode 的时候指定的编码格式是什么。

    再举一个 UnicodeDecodeError 的例子

    >>> x = u"Python"
    >>> y = "之禅"
    >>> x + y
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
    >>>
    

    str 与 unicode 字符串 执行 + 操作是, Python 会把 str 类型的字节序列隐式地转换成(解码)成 和 x 一样的 unicode 类型,但 Python 是使用默认的 ascii 编码来转换的,而 ASCII 中不包含中文,所以报错了。

    >>> y.decode('ascii')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
    

    正确地方式应该是显示地把 y 用 UTF-8 或者 GBK 进行解码。

    >>> x = u"Python"
    >>> y = "之禅"
    >>> y = y.decode("utf-8")
    >>> x + y
    u'Python\u4e4b\u7985'
    

    以上内容都是基于 Python2 来讲的,关于 Python3 的字符和编码将会另开一篇文章来写,保持关注。

    原文地址:https://mp.weixin.qq.com/s/LQrPmp2HMlw5C7izJIUHNQ
    作者:liuzhijun

    66 回复  |  直到 2017-03-23 13:30:51 +08:00
        1
    Ixizi   8 天前
    nice
        2
    hjc4869   8 天前 via Android   ♥ 6
    看标题以为是喷 python 2 ,进来才发现是编码科普文
        3
    fy   8 天前   ♥ 4
    为什么蛋疼? -> 请用 Python3 ,下一题。
        4
    glasslion   8 天前   ♥ 4
    编码一直是就是一件很蛋疼的事, 其他语言看上去不那么蛋疼,无非是:
    1. 不检查编码 /解码是否会报错。 手持两把锟斤拷, 口中直呼烫烫烫 就是这么来的。
    2. 只支持 utf-8 这一种编码
        5
    lzjun   8 天前
    @hjc4869

    Python 的编码为什么那么蛋疼?当然,这不能怪开发者。

    这是因为 Python2 使用 ASCII 字符编码作为默认编码方式,而 ASCII 不能处理中文,那么为什么不用 UTf-8 呢?因为 Guido 老爹为 Python 编写第一行代码是在 1989 年的冬天, 1991 年 2 月正式开源发布了第一个版本,而 Unicode 是 1991 年 10 月发布的,也就是说 Python 这门语言创立的时候 UTF-8 还没诞生,这是其一。

    Python 把字符串的类型还搞成两种, unicode 和 str ,以至于把开发者都弄糊涂了,这是其二
        6
    call43848   8 天前   ♥ 1
    神曰:“用 3 ”。
        7
    gimp   8 天前
    @glasslion

    “手持两把锟斤拷, 口中直呼烫烫烫” 笑了
        8
    Gsyc1   8 天前
    用 Python 3 ,字符串默认是 unicode 的
        9
    gouchaoer   8 天前   ♥ 1
    我到现在也没搞懂编码,我为什么要搞清楚编码呢?我用 java 用 php 就没操过心
        10
    lzjun   8 天前
    @gouchaoer 黑的漂亮,哈哈
        11
    janxin   8 天前
    用 python3
        12
    aploium   8 天前
    from __future__ import unicode_literals
        13
    sagaxu   8 天前
    @gouchaoer
    那是因为 php 只有 str 没有 unicode(php6 有,但夭折了),而 Java 只有 unicode 没有 str(str 用 byte[])。
        14
    qingshi   8 天前
    @Gsyc1 默认是 utf-8 吧
        15
    helloSwift   8 天前 via iPhone
    为什么你写了这么多,不去看看 Python3 呢(´・_・`)
        16
    keisuu   8 天前
    @helloSwift 楼主分析的是 python2 。

    看完全文,我算是了解为什么老是报编码错误的原因了。

    ps: python3 一样的有类似的错误吧。
        17
    lzjun   8 天前
    @helloSwift 啥意思? Python3 好呀
        18
    everhythm   8 天前
    python 3 还是有编码错呀

    比如你用 vim 打开 1 个文件,生成个 .swp 文件

    如果用 python 读取到这个文件,就报错
        19
    hjc4869   8 天前 via Android
    @lzjun 其实用 byte[]来表示字符串也算是 UNIX 和 C 的遗毒了。现代语言在设计的时候基本都是以 code point 为单位,虽然 Java 等语言被 UCS-2 坑了…
        20
    thekll   8 天前 via iPhone
    很奇怪的 coding 方式。
    是说内存中保存 unicode code point , I/O 时再编码 /解码吗?
        21
    weyou   8 天前
    @everhythm .swp 是 binary 文件,你要用 rb 模式去读, 跟编码没有关系好不好?
        22
    Xrong   8 天前   ♥ 1
    关键是大伙都心想着先搞定功能吧,没空去了解一些编码的知识。
        23
    lzjun   8 天前 via iPhone
    @Xrong 不去了解基础知识,遇到问题有时 neng 卡半天,关键是下次还是不知道问题根源
        24
    lzjun   8 天前 via iPhone
    @hjc4869 为什么 UCS-2 算坑呢?
        25
    hjc4869   8 天前
    @lzjun UCS-2 是按 16bit 为一个 code point 的,那个时期的新软件(如 Java, Windows NT, Mac OS X, Qt 等)因为定长编码的优势和支持 Unicode 的需求,几乎都用了它。但是后来由于 16bit 不能满足 Unicode 的新标准,于是不得不又改为变长编码( UTF-16 )。
    Java/C#里的 Character/char 最早是定义成 16bit 的 code point ,可以取 index 获得对应位置的 code point ,但现在不行了,比如取 emoji 就会取到半个字。
        26
    21grams   8 天前 via Android
    Python 2 是 2000 年发布的,在编码上不与时俱进还搞成这样是不可原谅的
        27
    kikyous   8 天前 via Android
    收藏
        28
    hjc4869   8 天前
    @21grams Python 2 是兼容 Python 1 的吧
        29
    21grams   8 天前
    @hjc4869 #28 兼容也不会有什么困难吧
        30
    sagaxu   8 天前
    @hjc4869 要么变长不能 index ,要么定长浪费空间,总要折衷一下的
        31
    hjc4869   8 天前
    @sagaxu 然而 UTF-16 既浪费空间也不定长,唯一的优势就是处理简单,速度快
        32
    sagaxu   8 天前 via Android
    @hjc4869 utf16 就是 utf8 和 utf16 之间的折衷, utf16 可以容纳大部分常用字符, str 内部实现可以利用这一点,比如置一个标志位,没有超出 2 字节范围时,就直接定位到字节,超出时再遍历字节做定位。
        33
    21grams   8 天前
    @sagaxu #32 那不是还要遍历后才知道要不要设标志位,还是有额外开销。
        34
    ledzep2   8 天前
    y
        35
    ledzep2   8 天前
    手残了 不好意思。 其实 python2 编码解码蛮好用的, 比 c......
        36
    sagaxu   8 天前
    @21grams 就算不要标志位,从字节数组构造一个 unicode 字符串出来,也是需要遍历的,不然怎么知道是否符合 unicode 规范?主流高级语言,字符串都是 immutable 的,所以标志算出来之后不需要重算,并没有增加什么开销。

    事实上 python 就是这么实现的 unicode 字符串
    enum PyUnicode_Kind {
    /* String contains only wstr byte characters. This is only possible
    when the string was created with a legacy API and _PyUnicode_Ready()
    has not been called yet. */
    PyUnicode_WCHAR_KIND = 0,
    /* Return values of the PyUnicode_KIND() macro: */
    PyUnicode_1BYTE_KIND = 1,
    PyUnicode_2BYTE_KIND = 2,
    PyUnicode_4BYTE_KIND = 4
    };
        37
    hjc4869   8 天前
    @sagaxu 如果要遍历的话可以把每个 code point 的位置都找出来。这样不管有没有 non-BMP 字符都能 O(1)定位,直接取到 code point
        38
    jy01264313   8 天前
    我觉得还是想理解:字符集和编码的区别吧,别上来就混在一起讲,还是很晕
        39
    sagaxu   8 天前
    @hjc4869 记录每个字符的位置,同样需要额外的存储空间,而且实现会更复杂,所以一般用标志位加定长内部编码比较常见。
        40
    Xrong   8 天前
    @lzjun 我都了解好几回了,忘了再查,查了又忘大概是这节奏。说真的 Python 的编码确实蛋疼的一逼。
        41
    thekll   8 天前
    其实所谓的 unicode 也就是用 utf-16 或 utf-32 编码(与 python 版本和编译设置有关),类似 java 虚拟机内部统一用 utf-16 表示字符串。
        42
    PythonAnswer   8 天前
    用 py3 就再也没搞过编码问题了。
        43
    thekll   8 天前
    https://docs.python.org/2/howto/unicode.html
    这个文档中关于 code points 的 utf-8 编码字节值的范围描述似乎也有些问题, code point≥128 时对应的 utf-8 字节值并不全都是 128 到 255 之间.
        44
    Gsyc1   8 天前
    @qingshi 多谢指正
        45
    inisun   7 天前
    @gouchaoer PHP 的 UTF-8 也有 bom 的问题..
        46
    abcbuzhiming   7 天前
    python2 的编码问题和 C 语言一样, python3 开始编码走的和 java 这类语言一样的路线,搞清这两点就明白了
        47
    lzjun   7 天前
    @hjc4869 长知识了,感谢
        48
    thekoc   7 天前
        49
    LokiSharp   7 天前
    还有一个问题, Windows 下 python3 不能显示 utf-8
        50
    vjnjc   7 天前
    之前用 java 和另一个 phper 调试接口,我就记得 java 的 string 要去掉前面 2 个 byte ,后面的内容给 phper 才不会出错~~好像跟什么大头有关
        51
    realpg   7 天前
    @ledzep2 #35
    觉得半残是最难用的
    全残的 C 自己处理 everything 所有都一样
    这个半残的 py2 最闹心 总犯错
        52
    fy   7 天前
    @LokiSharp 这个是 cmd 的锅,没记错的话是只能显示本地编码支持的字符
        53
    LokiSharp   7 天前
    @fy cmd 默认属性里不能选 utf-8 必须 chcp 65001 ,然后,每次运行都要敲这个命令
        54
    keisuu   7 天前
    大赞,终于解决了内心的疑惑,已关注公众号
        55
    kuntang   7 天前
    如果 py3 不正确地使用字符编码,一样可能导致 UnicodeXXXError ,关键还是要懂原理
        56
    annielong   7 天前
    理解是理解,但是爬起来照样会遇到编码问题,还是要手工分析转码
        57
    zaishanfeng   7 天前 via Android
    其实你只需要用上 django 的两个方法就行了, 也可以把这两个方法提出来。 自此以后我再也没遇到过字符编码问题。
        58
    hosiet   7 天前 via Android
    我觉得万恶之首是隐式转换。某些开发者写代码时不注意,导致某些地方收到超出 ASCII 范围的数据就崩。另外各种库的参数究竟是 str 还是 unicode 如果不注意搞混也会出类似问题。
        59
    voostar   7 天前
    自从我用了 3 之后就解毒了
        60
    ltux   7 天前
    我觉得主要问题在于很多程序员是面向 stackoverflow 编程,遇到问题就满足于把代码改得“能跑”就行,不去深究问题的根儿在哪儿,所以今儿改好了明儿继续出问题。我不能说 Python2 的编码不蛋疼,但我觉得也不能说“不怪开发者”。
        61
    imcocc   7 天前 via Android
    看到楼主说用浅显的文字解释,接下来看到这么长的文字,我觉得还是升 3 吧
        62
    param   7 天前
    每个做 Python 开发的都被字符编码的问题搞晕过?
    我没用过 python2 ,最多是看过,一直用 python3 ,似乎没有被字符编码坑过。
        63
    ProjectSky   7 天前
    "手持两把锟斤拷, 口中直呼烫烫烫"
    莫名喜感。
        64
    lzjun   6 天前
    @param 新系统用 py3 最好,还有很多老系统根本没法迁移,你问问豆瓣迁移到 3 是个多大的工程
        65
    TanLian   6 天前
    确实是篇好文章!!!
        66
    ap010gi2e   5 天前
    看着挺容易理解的,挺好
    DigitalOcean
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   鸣谢   ·   635 人在线   最高记录 2466   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.7.5 · 69ms · UTC 23:29 · PVG 07:29 · LAX 16:29 · JFK 19:29
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1