Python str() 引发的 UnicodeEncodeError

起因

众所周知,Python 2 中的 UnicodeEncodeError 与 UnicodeDecodeError 是比较棘手的问题,有时候遇到这类问题的发生,总是一头雾水,感觉莫名其妙。甚至,《Fluent Python》的作者还提出了所谓“三明治模型”的东西来帮助解决此类问题(其实大可不必如此麻烦,后文有述)。

今天在线上遇到一个与此有关的小问题,感觉很有趣,水文一篇记录之。

Bug 转到我这里时,看到现象自然是UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)这类莫名其妙的提示。然后翻 log,迅速找到对应的代码行,大概类似下面这种:

thrift_obj = ThriftKeyValue(key=str(xx_obj.name))  # 出错行, xx_obj.name 是一个 str
复制代码

一开始,看见str(xx_obj.name),也不知道是手误,还是故意为之,反正是学不会这种操作(应该每个项目里面,或多或少都有这样的神奇代码吧)。

分析

看异常的字面意思,大致就是:有某个串,正在被 ASCII 编码器编码,但是显然该串超出了 ASCII 编码器所规定的范围,于是出错。于是推测:

  • 哪里应该有个什么Unicode串(什么串无所谓,反正只要超出 ASCII 的范围就行),这里应该是 xx_obj.name
  • 某处正在发生编码动作,而且是偷偷地在搞(最烦这种隐式转换了,Python 2 中很多),从代码看不出在哪里。

左看右看,应该是 str() 这个内置函数,于是简单地试了一下如下代码:

In [5]: u = u'中国'

In [6]: str(u)
---------------------------------------------------------------------------
UnicodeEncodeError                        Traceback (most recent call last)
<ipython-input-6-b3b94fb7b5a0> in <module>()
----> 1 str(u)

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
In [7]: b = u.encode('utf-8')

In [8]: str(b)
Out[8]: '\xe4\xb8\xad\xe5\x9b\xbd'


复制代码

果然如此。查阅文档一看,没啥有价值的信息,描述太模糊了:

class str(object='')
Return a string containing a nicely printable representation of an object. For strings, this returns the string itself. The difference with repr(object) is that str(object) does not always attempt to return a string that is acceptable to eval(); its goal is to return a printable string. If no argument is given, returns the empty string, ''.

For more information on strings see Sequence Types — str, unicode, list, tuple, bytearray, buffer, xrange which describes sequence functionality (strings are sequences), and also the string-specific methods described in the String Methods section. To output formatted strings use template strings or the % operator described in the String Formatting Operations section. In addition see the String Services section. See also unicode().
复制代码

我们的代码里面(Python 2),每个 py 文件都有这么一行:

from __future__ import unicode_literals, absolute_import
复制代码

所以我推测 xx_obj.name 是要给 unicode 串,打 log 一看,果然如此。

解决

至此,要么将 xx_obj.name 转化成 str() 能认识的东西,在这里至少不能是 unicode,应该是 bytes。不过我没有这么做,太丑陋了,二是改成这样:

thrift_obj = ThriftKeyValue(key=xx_obj.name) # 这里没必要调用 str() ,估计前面能跑正常,是因为 name 恰好总是 ASCII 字符
复制代码

Bug 修复,其他功能也表现正常。

总结

前文讲到,Python 2 中有较多这种隐式转换,而且也没啥文档说明,特别是加上 Windows环境和 print 操作时,报错信息更是看得人不明所以。《Fluent Python》中有讲到所谓“三明治模型”来解决这一问题,还是蛮有启发的。

不过,我一般遵循的原则是:只用 Unicode,让任何地方都是 Unicode。方式如下:

  • 所有 py 文件必须有如下文件头:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#

from __future__ import unicode_literals, absolute_import
复制代码
  • 接到外界的字节串(从网络,从文件等),先转成 Unicode,不过抽取成函数更好,免得重复编码:
API 的起名优点冗余,主要是为了做到 “见名知义”


class UnicodeUtils(object):
    @classmethod
    def get_unicode_str(cls, bytes_str, try_decoders=('utf-8', 'gbk', 'utf-16')):
        """转换成字符串(一般是Unicode)"""
        
        if not bytes_str:
            return u''

        if isinstance(bytes_str, (unicode,)):
            return bytes_str

        for decoder in try_decoders:
            try:
                unicode_str = bytes_str.decode(decoder)
            except UnicodeDecodeError:
                pass
            else:
                return unicode_str

        raise DecodeBytesFailedException('decode bytes failed. tried decoders: %s' % list(try_decoders))

    @classmethod
    def encode_to_bytes(cls, unicode_str, encoder='utf-8'):
        """转换成字节串"""
        
        if unicode_str is None:
            return b''

        if isinstance(unicode_str, unicode):
            return unicode_str.encode(encoding=encoder)
        else:
            u = cls.get_unicode(unicode_str)
            return u.encode(encoding=encoder)
复制代码
  • 送到外界的东西,全部转成 UTF-8 编码的字节串,见上面代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值