V2EX 首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python 学习手册
Python Cookbook
Python 基础教程
Python Sites
PyPI - Python Package Index
http://www.simple-is-better.com/
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
Sponsored by
二向箔安全
​一对一的线上 web 安全培训服务
咨询微信:twosecurityrefer
Promoted by 二向箔安全
V2EX  ›  Python

Python 编码为什么那么蛋疼?

  •  2
     
  •   lzjun · 272 天前 · 5009 次点击
    这是一个创建于 272 天前的主题,其中的信息可能已经有所发展或是发生改变。

    据说,每个做 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   272 天前
    nice
        2
    hjc4869   272 天前 via Android   ♥ 6
    看标题以为是喷 python 2 ,进来才发现是编码科普文
        3
    fy   272 天前   ♥ 4
    为什么蛋疼? -> 请用 Python3 ,下一题。
        4
    glasslion   272 天前   ♥ 4
    编码一直是就是一件很蛋疼的事, 其他语言看上去不那么蛋疼,无非是:
    1. 不检查编码 /解码是否会报错。 手持两把锟斤拷, 口中直呼烫烫烫 就是这么来的。
    2. 只支持 utf-8 这一种编码
        5
    lzjun   272 天前
    @hjc4869

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

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

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

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

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

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

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

    如果用 python 读取到这个文件,就报错
        19
    hjc4869   272 天前 via Android
    @lzjun 其实用 byte[]来表示字符串也算是 UNIX 和 C 的遗毒了。现代语言在设计的时候基本都是以 code point 为单位,虽然 Java 等语言被 UCS-2 坑了…
        20
    thekll   272 天前 via iPhone
    很奇怪的 coding 方式。
    是说内存中保存 unicode code point , I/O 时再编码 /解码吗?
        21
    weyou   272 天前
    @everhythm .swp 是 binary 文件,你要用 rb 模式去读, 跟编码没有关系好不好?
        22
    Xrong   272 天前   ♥ 1
    关键是大伙都心想着先搞定功能吧,没空去了解一些编码的知识。
        23
    lzjun   272 天前 via iPhone
    @Xrong 不去了解基础知识,遇到问题有时 neng 卡半天,关键是下次还是不知道问题根源
        24
    lzjun   272 天前 via iPhone
    @hjc4869 为什么 UCS-2 算坑呢?
        25
    hjc4869   272 天前
    @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   272 天前 via Android
    Python 2 是 2000 年发布的,在编码上不与时俱进还搞成这样是不可原谅的
        27
    kikyous   272 天前 via Android
    收藏
        28
    hjc4869   272 天前
    @21grams Python 2 是兼容 Python 1 的吧
        29
    21grams   272 天前
    @hjc4869 #28 兼容也不会有什么困难吧
        30
    sagaxu   272 天前
    @hjc4869 要么变长不能 index ,要么定长浪费空间,总要折衷一下的
        31
    hjc4869   272 天前
    @sagaxu 然而 UTF-16 既浪费空间也不定长,唯一的优势就是处理简单,速度快
        32
    sagaxu   272 天前 via Android
    @hjc4869 utf16 就是 utf8 和 utf16 之间的折衷, utf16 可以容纳大部分常用字符, str 内部实现可以利用这一点,比如置一个标志位,没有超出 2 字节范围时,就直接定位到字节,超出时再遍历字节做定位。
        33
    21grams   272 天前
    @sagaxu #32 那不是还要遍历后才知道要不要设标志位,还是有额外开销。
        34
    ledzep2   272 天前
    y
        35
    ledzep2   272 天前
    手残了 不好意思。 其实 python2 编码解码蛮好用的, 比 c......
        36
    sagaxu   272 天前
    @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   272 天前
    @sagaxu 如果要遍历的话可以把每个 code point 的位置都找出来。这样不管有没有 non-BMP 字符都能 O(1)定位,直接取到 code point
        38
    jy01264313   272 天前
    我觉得还是想理解:字符集和编码的区别吧,别上来就混在一起讲,还是很晕
        39
    sagaxu   272 天前
    @hjc4869 记录每个字符的位置,同样需要额外的存储空间,而且实现会更复杂,所以一般用标志位加定长内部编码比较常见。
        40
    Xrong   272 天前
    @lzjun 我都了解好几回了,忘了再查,查了又忘大概是这节奏。说真的 Python 的编码确实蛋疼的一逼。
        41
    thekll   272 天前
    其实所谓的 unicode 也就是用 utf-16 或 utf-32 编码(与 python 版本和编译设置有关),类似 java 虚拟机内部统一用 utf-16 表示字符串。
        42
    PythonAnswer   272 天前
    用 py3 就再也没搞过编码问题了。
        43
    thekll   272 天前
    https://docs.python.org/2/howto/unicode.html
    这个文档中关于 code points 的 utf-8 编码字节值的范围描述似乎也有些问题, code point≥128 时对应的 utf-8 字节值并不全都是 128 到 255 之间.
        44
    Gsyc1   272 天前
    @qingshi 多谢指正
        45
    inisun   272 天前
    @gouchaoer PHP 的 UTF-8 也有 bom 的问题..
        46
    abcbuzhiming   271 天前
    python2 的编码问题和 C 语言一样, python3 开始编码走的和 java 这类语言一样的路线,搞清这两点就明白了
        47
    lzjun   271 天前
    @hjc4869 长知识了,感谢
        48
    thekoc   271 天前
        49
    LokiSharp   271 天前
    还有一个问题, Windows 下 python3 不能显示 utf-8
        50
    vjnjc   271 天前
    之前用 java 和另一个 phper 调试接口,我就记得 java 的 string 要去掉前面 2 个 byte ,后面的内容给 phper 才不会出错~~好像跟什么大头有关
        51
    realpg   271 天前
    @ledzep2 #35
    觉得半残是最难用的
    全残的 C 自己处理 everything 所有都一样
    这个半残的 py2 最闹心 总犯错
        52
    fy   271 天前
    @LokiSharp 这个是 cmd 的锅,没记错的话是只能显示本地编码支持的字符
        53
    LokiSharp   271 天前
    @fy cmd 默认属性里不能选 utf-8 必须 chcp 65001 ,然后,每次运行都要敲这个命令
        54
    keisuu   271 天前
    大赞,终于解决了内心的疑惑,已关注公众号
        55
    kuntang   271 天前
    如果 py3 不正确地使用字符编码,一样可能导致 UnicodeXXXError ,关键还是要懂原理
        56
    annielong   271 天前
    理解是理解,但是爬起来照样会遇到编码问题,还是要手工分析转码
        57
    zaishanfeng   271 天前 via Android
    其实你只需要用上 django 的两个方法就行了, 也可以把这两个方法提出来。 自此以后我再也没遇到过字符编码问题。
        58
    hosiet   271 天前 via Android
    我觉得万恶之首是隐式转换。某些开发者写代码时不注意,导致某些地方收到超出 ASCII 范围的数据就崩。另外各种库的参数究竟是 str 还是 unicode 如果不注意搞混也会出类似问题。
        59
    voostar   271 天前
    自从我用了 3 之后就解毒了
        60
    ltux   271 天前
    我觉得主要问题在于很多程序员是面向 stackoverflow 编程,遇到问题就满足于把代码改得“能跑”就行,不去深究问题的根儿在哪儿,所以今儿改好了明儿继续出问题。我不能说 Python2 的编码不蛋疼,但我觉得也不能说“不怪开发者”。
        61
    imcocc   271 天前 via Android
    看到楼主说用浅显的文字解释,接下来看到这么长的文字,我觉得还是升 3 吧
        62
    param   271 天前
    每个做 Python 开发的都被字符编码的问题搞晕过?
    我没用过 python2 ,最多是看过,一直用 python3 ,似乎没有被字符编码坑过。
        63
    ProjectSky   271 天前
    "手持两把锟斤拷, 口中直呼烫烫烫"
    莫名喜感。
        64
    lzjun   270 天前
    @param 新系统用 py3 最好,还有很多老系统根本没法迁移,你问问豆瓣迁移到 3 是个多大的工程
        65
    TanLian   270 天前
    确实是篇好文章!!!
        66
    ap010gi2e   269 天前
    看着挺容易理解的,挺好
    DigitalOcean
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   鸣谢   ·   1850 人在线   最高记录 3541   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.0 · 81ms · UTC 01:01 · PVG 09:01 · LAX 17:01 · JFK 20:01
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1