Python 3 的 surrogateescape
分类:技术
新项目中第一次开始作死使用 Python 3,在测试中遇到一件奇怪的事情。
系统中有一个邮件发送模块,直接在命令行中手动跑 Worker 的话邮件可以成功发送,而一旦用 supervisor 运行则无法发送邮件。日志中显示在邮件发送库中抛出了如下异常:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce6' in position 0: surrogates not allowed
Surrogate
所以,日志里这个 surrogate 是个啥?
Surrogate 是 Unicode 中位于 BMP 外的一组不会有对应字符的码位,Python 3 中使用这些码位来「代表」无法编码的字节。
如果你明白「锟斤拷」是怎么来的,那么就一定能理解 Python 的 replace
编解码错误处理机制:无法解码的字节会被替换成 U+FFFD
,无法编码的码位会被替换成 ?
。这样做有一个明显的缺点就是不可逆。Python 3 中新增的 surrogateescape
则是一种可逆的错误处理机制,利用 Surrogate 码位保存无法解码的字节,编码时则将其还原为对应的原始字节。
例子中的报错正是因为 0xE6
无法使用 ascii
解码,而被解为了 U+DCE6
。
'\udce6'.encode('ascii', 'surrogateescape')
>>> b'\xe6'
b'\xe6'.decode('ascii', 'surrogateescape')
>>> '\udce6'
知道了原因,用这种方法解码异常信息中的完整字符串就找到了罪魁祸首:邮件发送者的配置。
Python 3 的环境变量与编码
邮件发送者是使用 显示名称 <邮箱地址>
的格式在环境变量中设置的,显示名称部分包含了汉字。
Python 3 中,使用 os.getenv
或者 os.environ
获得到的环境变量的值都是 str
类型。在类 Unix 系统中,解码过程是使用 sys.getfilesystemencoding()
和 'surrogateescape'
错误处理来实现的。而 sys.getfilesystemencoding()
的取值与 nl_langinfo(COODESET)
相关,即 LC_CTYPE
环境变量。
所以,导致这部分环境变量包含 Surrogate 的原因就是 LC_TYPE
指定的编码不正确。
当环境变量 LC_CTYPE
、LC_ALL
、LANG
均没有显式定义时,各个操作系统有各自的 LC_CTYPE
的缺省值(比如 C
)。这种情况下,通过 sys.getfilesystemencoding()
通常会得到 ascii
,环境变量中包含非 ASCII 字符就会得到上面的 Surrogate。
真正的问题
到了这里,问题根源和解决方法基本上已经水落石出了。
前面说手动跑 Worker 可以但是 supervisor 跑 Worker 就不行,这是因为被 supervisor 管理的子进程并不会单独开 Shell 而是继承 supervisord
的 Shell,所以基本的环境变量也有不同。
解决方法便是在 supervisor 配置中修改 environment,加入 LC_CTYPE
或者 LC_ALL
或者 LANG
环境变量的配置。
附,LANG
、LC_ALL
、LC_TYPE
什么的之间到底是个啥关系:查找 LC_X
的值的过程是:LC_ALL
有值就用 LC_ALL
的值;否则 LC_X
有值就用 LC_X
的值;否则 LANG
有值就用 LANG
的值;否则就用操作系统的默认值。
以上。
感慨万千啊,上一次在你博客comment还是2014年。你今年这个blog只写了2篇文章,呵呵,加油!